project.vala 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807
  1. /*
  2. * Copyright (c) 2012-2025 Daniele Bartolini et al.
  3. * SPDX-License-Identifier: GPL-3.0-or-later
  4. */
  5. namespace Crown
  6. {
  7. public enum ImportResult
  8. {
  9. SUCCESS, ///< Data imported successfully.
  10. ERROR, ///< Error during import or elsewhere.
  11. CANCEL, ///< User cancelled the import.
  12. }
  13. public delegate void Import(ImportResult result);
  14. public class Project
  15. {
  16. public const string LEVEL_EDITOR_TEST_NAME = "_level_editor_test";
  17. public delegate void ImporterDelegate(Import import_result
  18. , Database database
  19. , string destination_dir
  20. , SList<string> filenames
  21. , Gtk.Window? parent_window
  22. );
  23. [Compact]
  24. public struct ImporterData
  25. {
  26. public unowned ImporterDelegate delegate;
  27. public Gee.ArrayList<string> extensions;
  28. public double order;
  29. public Gtk.FileFilter _filter;
  30. public unowned Import import_result;
  31. ImporterData()
  32. {
  33. delegate = null;
  34. extensions = new Gee.ArrayList<string>();
  35. order = 0.0;
  36. _filter = new Gtk.FileFilter();
  37. import_result = null;
  38. }
  39. public bool can_import_extension(string extension)
  40. {
  41. string e = extension.down();
  42. foreach (var ext in extensions) {
  43. if (e == ext)
  44. return true;
  45. }
  46. return false;
  47. }
  48. public bool can_import_filenames(GLib.SList<string> filenames)
  49. {
  50. foreach (var filename in filenames) {
  51. if (!can_import_extension(path_extension(filename)))
  52. return false;
  53. }
  54. return true;
  55. }
  56. }
  57. // Data
  58. public File? _source_dir;
  59. public File _toolchain_dir;
  60. public File _data_dir;
  61. public File _user_dir;
  62. public File _level_editor_test_level;
  63. public File _level_editor_test_package;
  64. public string _platform;
  65. public Database _files;
  66. public Gee.HashMap<string, Guid?> _map;
  67. public ImporterData _all_extensions_importer_data;
  68. public Gee.ArrayList<ImporterData?> _importers;
  69. public bool _data_compiled;
  70. public Hashtable _data_index;
  71. public signal void file_added(string type, string name, uint64 size, uint64 mtime);
  72. public signal void file_changed(string type, string name, uint64 size, uint64 mtime);
  73. public signal void file_removed(string type, string name);
  74. public signal void tree_added(string name);
  75. public signal void tree_removed(string name);
  76. public signal void project_reset();
  77. public signal void project_loaded();
  78. public Project()
  79. {
  80. #if CROWN_PLATFORM_WINDOWS
  81. _platform = "windows";
  82. #else
  83. _platform = "linux";
  84. #endif
  85. _files = new Database(this);
  86. _map = new Gee.HashMap<string, Guid?>();
  87. _all_extensions_importer_data = ImporterData();
  88. _all_extensions_importer_data.delegate = import_all_extensions;
  89. _importers = new Gee.ArrayList<ImporterData?>();
  90. _data_compiled = false;
  91. _data_index = new Hashtable();
  92. }
  93. public void data_compiled()
  94. {
  95. _data_compiled = true;
  96. try {
  97. string index_path = Path.build_filename(_data_dir.get_path(), "data_index.sjson");
  98. _data_index = SJSON.load_from_path(index_path);
  99. } catch (JsonSyntaxError e) {
  100. loge(e.message);
  101. }
  102. }
  103. public uint64 mtime(string type, string name)
  104. {
  105. var path = ResourceId.path(type, name);
  106. Guid id = _map[path];
  107. string mtime = _files.get_property_string(id, "mtime");
  108. return uint64.parse(mtime);
  109. }
  110. public void reset()
  111. {
  112. project_reset();
  113. _source_dir = null;
  114. _files.reset();
  115. _map.clear();
  116. }
  117. public bool is_loaded()
  118. {
  119. return _source_dir != null;
  120. }
  121. public void load(string source_dir)
  122. {
  123. reset();
  124. _source_dir = File.new_for_path(source_dir);
  125. _data_dir = File.new_for_path(_source_dir.get_path() + "_" + _platform);
  126. _level_editor_test_level = File.new_for_path(Path.build_filename(_source_dir.get_path(), LEVEL_EDITOR_TEST_NAME + ".level"));
  127. _level_editor_test_package = File.new_for_path(Path.build_filename(_source_dir.get_path(), LEVEL_EDITOR_TEST_NAME + ".package"));
  128. // Cleanup source directory from previous runs' garbage
  129. delete_garbage();
  130. _user_dir = GLib.File.new_for_path(GLib.Path.build_filename(Crown._data_dir.get_path(), "projects", StringId64(source_dir).to_string()));
  131. try {
  132. _user_dir.make_directory_with_parents();
  133. } catch (Error e) {
  134. /* Nobody cares */
  135. }
  136. project_loaded();
  137. }
  138. public void set_toolchain_dir(string toolchain_dir)
  139. {
  140. _toolchain_dir = File.new_for_path(toolchain_dir);
  141. }
  142. public void create_initial_files(string source_dir)
  143. {
  144. // Write boot.config
  145. {
  146. string text = "// Lua script to launch on boot"
  147. + "\nboot_script = \"core/game/boot\""
  148. + "\n"
  149. + "\n// Package to load on boot"
  150. + "\nboot_package = \"boot\""
  151. + "\n"
  152. + "\nwindow_title = \"New Project\""
  153. + "\n"
  154. + "\n// Linux-only configs"
  155. + "\nlinux = {"
  156. + "\n renderer = {"
  157. + "\n resolution = [ 1280 720 ]"
  158. + "\n }"
  159. + "\n}"
  160. + "\n"
  161. + "\n// Windows-only configs"
  162. + "\nwindows = {"
  163. + "\n renderer = {"
  164. + "\n resolution = [ 1280 720 ]"
  165. + "\n }"
  166. + "\n}"
  167. + "\n"
  168. ;
  169. string path = Path.build_filename(source_dir, "boot.config");
  170. FileStream fs = FileStream.open(path, "wb");
  171. if (fs != null)
  172. fs.write(text.data);
  173. }
  174. // Write boot.package
  175. {
  176. string text = "lua = ["
  177. + "\n \"core/game/boot\""
  178. + "\n]"
  179. + "\nshader = ["
  180. + "\n \"core/shaders/default\""
  181. + "\n]"
  182. + "\nphysics_config = ["
  183. + "\n \"global\""
  184. + "\n]"
  185. + "\nunit = ["
  186. + "\n \"core/units/camera\""
  187. + "\n]"
  188. + "\n"
  189. ;
  190. string path = Path.build_filename(source_dir, "boot.package");
  191. FileStream fs = FileStream.open(path, "wb");
  192. if (fs != null)
  193. fs.write(text.data);
  194. }
  195. // Write global.physics_config
  196. {
  197. string text = "materials = {"
  198. + "\n default = { friction = 0.8 rolling_friction = 0.5 restitution = 0.81 }"
  199. + "\n}"
  200. + "\n"
  201. + "\ncollision_filters = {"
  202. + "\n no_collision = { collides_with = [] }"
  203. + "\n default = { collides_with = [ \"default\" ] }"
  204. + "\n}"
  205. + "\n"
  206. + "\nactors = {"
  207. + "\n static = { dynamic = false }"
  208. + "\n dynamic = { dynamic = true }"
  209. + "\n keyframed = { dynamic = true kinematic = true disable_gravity = true }"
  210. + "\n}"
  211. + "\n"
  212. ;
  213. string path = Path.build_filename(source_dir, "global.physics_config");
  214. FileStream fs = FileStream.open(path, "wb");
  215. if (fs != null)
  216. fs.write(text.data);
  217. }
  218. // Write main.lua
  219. {
  220. string text = "require \"core/game/camera\""
  221. + "\n"
  222. + "\nGame = Game or {"
  223. + "\n sg = nil,"
  224. + "\n pw = nil,"
  225. + "\n rw = nil,"
  226. + "\n camera = nil,"
  227. + "\n}"
  228. + "\n"
  229. + "\nGameBase.game = Game"
  230. + "\nGameBase.game_level = nil"
  231. + "\n"
  232. + "\nfunction Game.level_loaded()"
  233. + "\n Device.enable_resource_autoload(true)"
  234. + "\n"
  235. + "\n Game.sg = World.scene_graph(GameBase.world)"
  236. + "\n Game.pw = World.physics_world(GameBase.world)"
  237. + "\n Game.rw = World.render_world(GameBase.world)"
  238. + "\n Game.camera = FPSCamera(GameBase.world, GameBase.camera_unit)"
  239. + "\nend"
  240. + "\n"
  241. + "\nfunction Game.update(dt)"
  242. + "\n -- Stop the engine when the 'ESC' key is released"
  243. + "\n if Keyboard.released(Keyboard.button_id(\"escape\")) then"
  244. + "\n Device.quit()"
  245. + "\n end"
  246. + "\n"
  247. + "\n -- Update camera"
  248. + "\n local delta = Vector3.zero()"
  249. + "\n if Mouse.pressed(Mouse.button_id(\"right\")) then move = true end"
  250. + "\n if Mouse.released(Mouse.button_id(\"right\")) then move = false end"
  251. + "\n if move then delta = Mouse.axis(Mouse.axis_id(\"cursor_delta\")) end"
  252. + "\n Game.camera:update(dt, delta.x, delta.y)"
  253. + "\nend"
  254. + "\n"
  255. + "\nfunction Game.render(dt)"
  256. + "\nend"
  257. + "\n"
  258. + "\nfunction Game.shutdown()"
  259. + "\nend"
  260. + "\n"
  261. ;
  262. string path = Path.build_filename(source_dir, "main.lua");
  263. FileStream fs = FileStream.open(path, "wb");
  264. if (fs != null)
  265. fs.write(text.data);
  266. }
  267. }
  268. public int create_script(string directory, string name, bool empty)
  269. {
  270. string script_name = Path.build_filename(directory, name);
  271. string script_path = script_name + ".lua";
  272. string path = this.absolute_path(script_path);
  273. string behavior_name = camel_case(script_name.replace("/", " ")).replace(" ", "");
  274. FileStream fs = FileStream.open(path, "wb");
  275. if (fs != null) {
  276. if (empty) {
  277. return fs.puts("\n");
  278. } else {
  279. string text = "-- Note: the following table must be global and uniquely named."
  280. + "\nBehavior = Behavior or {"
  281. + "\n data = {}"
  282. + "\n}"
  283. + "\n"
  284. + "\nlocal data = Behavior.data"
  285. + "\n"
  286. + "\nfunction Behavior.spawned(world, units)"
  287. + "\n if data[world] == nil then"
  288. + "\n data[world] = {}"
  289. + "\n end"
  290. + "\n"
  291. + "\n for uu = 1, #units do"
  292. + "\n local unit = units[uu]"
  293. + "\n"
  294. + "\n -- Store instance-specific data."
  295. + "\n if data[world][unit] == nil then"
  296. + "\n -- data[world][unit] = {}"
  297. + "\n end"
  298. + "\n"
  299. + "\n -- Do something with the unit."
  300. + "\n end"
  301. + "\nend"
  302. + "\n"
  303. + "\nfunction Behavior.update(world, dt)"
  304. + "\n -- Called once per frame."
  305. + "\nend"
  306. + "\n"
  307. + "\nfunction Behavior.unspawned(world, units)"
  308. + "\n -- Cleanup."
  309. + "\n for uu = 1, #units do"
  310. + "\n if data[world][units] then"
  311. + "\n data[world][units] = nil"
  312. + "\n end"
  313. + "\n end"
  314. + "\nend"
  315. + "\n"
  316. + "\nreturn Behavior"
  317. + "\n"
  318. ;
  319. return fs.puts(text.replace("Behavior", behavior_name));
  320. }
  321. }
  322. return -1;
  323. }
  324. public int create_unit(string directory, string name)
  325. {
  326. string unit_path = Path.build_filename(directory, name + ".unit");
  327. string path = this.absolute_path(unit_path);
  328. Database db = new Database(this, null);
  329. Guid unit_id = Guid.new_guid();
  330. Unit unit = Unit(db, unit_id);
  331. unit.create_empty();
  332. return db.dump(path, unit_id);
  333. }
  334. public int create_state_machine(string directory, string name, string? skeleton_name)
  335. {
  336. string resource_name = Path.build_filename(directory, name);
  337. Database db = new Database(this, null);
  338. Guid machine_id = Guid.new_guid();
  339. StateMachineResource machine;
  340. if (skeleton_name == null)
  341. machine = StateMachineResource.sprite(db, machine_id, null);
  342. else
  343. machine = StateMachineResource.mesh(db, machine_id, skeleton_name);
  344. return machine.save(this, resource_name);
  345. }
  346. public int create_material(string directory, string name)
  347. {
  348. string resource_name = Path.build_filename(directory, name);
  349. Database db = new Database(this, null);
  350. Guid material_id = Guid.new_guid();
  351. MaterialResource material_resource = MaterialResource.mesh(db, material_id);
  352. return material_resource.save(this, resource_name);
  353. }
  354. // Returns the absolute path to the source directory.
  355. public string source_dir()
  356. {
  357. if (_source_dir == null)
  358. return "";
  359. else
  360. return _source_dir.get_path();
  361. }
  362. // Returns the absolute path to the toolchain directory.
  363. public string toolchain_dir()
  364. {
  365. return _toolchain_dir.get_path();
  366. }
  367. // Returns the absolute path to the data directory.
  368. public string data_dir()
  369. {
  370. return _data_dir.get_path();
  371. }
  372. // Returns the absolute path to the user-specific data for this project.
  373. public string user_dir()
  374. {
  375. return _user_dir.get_path();
  376. }
  377. public string platform()
  378. {
  379. return _platform;
  380. }
  381. public string name()
  382. {
  383. string sd = source_dir();
  384. return sd.substring(sd.last_index_of_char(GLib.Path.DIR_SEPARATOR) + 1);
  385. }
  386. public bool path_is_within_source_dir(string path)
  387. {
  388. GLib.File file = GLib.File.new_for_path(path);
  389. return file.has_prefix(_source_dir);
  390. }
  391. public void delete_garbage()
  392. {
  393. try {
  394. _level_editor_test_level.delete();
  395. _level_editor_test_package.delete();
  396. } catch (GLib.Error e) {
  397. // Ignored
  398. }
  399. }
  400. /// Converts the @a resource_id to its corresponding human-readable @a
  401. /// resource_name. It returns true if the conversion is successful, otherwise
  402. /// it returns false and sets @a resource_name to the value of @a resource_id.
  403. public bool resource_id_to_name(out string resource_name, string resource_id)
  404. {
  405. Value? name = _data_index[resource_id];
  406. if (name != null) {
  407. resource_name = (string)name;
  408. return true;
  409. }
  410. resource_name = resource_id;
  411. return false;
  412. }
  413. public Database files()
  414. {
  415. return _files;
  416. }
  417. public void add_file(string path, uint64 size, uint64 mtime)
  418. {
  419. string type = path_extension(path);
  420. string name = type == "" ? path : path.substring(0, path.last_index_of("."));
  421. Guid id = Guid.new_guid();
  422. _files.create(id, OBJECT_TYPE_FILE);
  423. _files.set_property_string(id, "path", path);
  424. _files.set_property_string(id, "type", type);
  425. _files.set_property_string(id, "name", name);
  426. _files.set_property_string(id, "size", size.to_string());
  427. _files.set_property_string(id, "mtime", mtime.to_string());
  428. _files.add_to_set(GUID_ZERO, "data", id);
  429. _map[path] = id;
  430. file_added(type, name, size, mtime);
  431. }
  432. public void change_file(string path, uint64 size, uint64 mtime)
  433. {
  434. string type = path_extension(path);
  435. string name = type == "" ? path : path.substring(0, path.last_index_of("."));
  436. Guid id = _map[path];
  437. _files.set_property_string(id, "size", size.to_string());
  438. _files.set_property_string(id, "mtime", mtime.to_string());
  439. _data_compiled = false;
  440. file_changed(type, name, size, mtime);
  441. }
  442. public void remove_file(string path)
  443. {
  444. if (!_map.has_key(path)) {
  445. logw("remove_file: map does not contain path: %s".printf(path));
  446. return;
  447. }
  448. Guid id = _map[path];
  449. file_removed(_files.get_property_string(id, "type"), _files.get_property_string(id, "name"));
  450. _files.remove_from_set(GUID_ZERO, "data", id);
  451. _files.destroy(id);
  452. _map.unset(path);
  453. }
  454. public void add_tree(string path)
  455. {
  456. tree_added(path);
  457. }
  458. public void remove_tree(string path)
  459. {
  460. tree_removed(path);
  461. }
  462. public string resource_filename(string absolute_path)
  463. {
  464. string prefix = _source_dir.get_path();
  465. if (absolute_path.has_prefix(_toolchain_dir.get_path() + "/core"))
  466. prefix = _toolchain_dir.get_path();
  467. return File.new_for_path(prefix).get_relative_path(File.new_for_path(absolute_path));
  468. }
  469. public string absolute_path(string resource_path)
  470. {
  471. string prefix = _source_dir.get_path();
  472. if (resource_path.has_prefix("core/") || resource_path == "core")
  473. prefix = _toolchain_dir.get_path();
  474. return Path.build_filename(prefix, resource_path);
  475. }
  476. public static void import_all_extensions(Import import_result
  477. , Database database
  478. , string destination_dir
  479. , SList<string> filenames
  480. , Gtk.Window? parent_window
  481. )
  482. {
  483. Project project = database._project;
  484. Gee.ArrayList<string> paths = new Gee.ArrayList<string>();
  485. foreach (var item in filenames)
  486. paths.add(item);
  487. paths.sort((a, b) => {
  488. int ext_a = a.last_index_of_char('.');
  489. int ext_b = b.last_index_of_char('.');
  490. return strcmp(a[ext_a : a.length], b[ext_b : b.length]);
  491. });
  492. while (paths.size != 0) {
  493. // Find importer for the first file in the list of selected filenames.
  494. ImporterData? importer = project.find_importer_for_path(paths[0]);
  495. if (importer == null) {
  496. import_result(ImportResult.ERROR);
  497. return;
  498. }
  499. // Create the list of all filenames importable by importer.
  500. Gee.ArrayList<string> importables = new Gee.ArrayList<string>();
  501. var cur = paths.list_iterator();
  502. for (var has_next = cur.next(); has_next; has_next = cur.next()) {
  503. string path = paths[cur.index()];
  504. if (importer.can_import_extension(path_extension(path))) {
  505. importables.add(path);
  506. cur.remove();
  507. }
  508. }
  509. // If importables is empty, filenames must have been filled with
  510. // un-importable filenames...
  511. if (importables.size == 0) {
  512. import_result(ImportResult.ERROR);
  513. return;
  514. }
  515. // Convert importables to SList<string> to be used as delegate param.
  516. SList<string> importables_list = new SList<string>();
  517. foreach (var item in importables)
  518. importables_list.append(item);
  519. importer.delegate(import_result, database, destination_dir, importables_list, parent_window);
  520. }
  521. }
  522. public class FileFilterFuncData
  523. {
  524. public string extension;
  525. public FileFilterFuncData(string ext)
  526. {
  527. extension = ext;
  528. }
  529. public bool handler(Gtk.FileFilterInfo info)
  530. {
  531. return info.filename.down().has_suffix("." + extension);
  532. }
  533. }
  534. // Returns a Gtk.FileFilter based on file @a extensions list.
  535. public Gtk.FileFilter create_gtk_file_filter(string name, Gee.ArrayList<string> extensions)
  536. {
  537. Gtk.FileFilter filter = new Gtk.FileFilter();
  538. string extensions_comma_separated = "";
  539. foreach (var ext in extensions) {
  540. extensions_comma_separated += "*.%s, ".printf(ext);
  541. FileFilterFuncData data = new FileFilterFuncData(ext);
  542. filter.add_custom(Gtk.FileFilterFlags.FILENAME, data.handler);
  543. }
  544. filter.set_filter_name(name + " (%s)".printf(extensions_comma_separated[0 : -2]));
  545. return filter;
  546. }
  547. public void register_importer_internal(string name, ref ImporterData data)
  548. {
  549. data._filter = create_gtk_file_filter(name, data.extensions);
  550. _importers.add(data);
  551. _importers.sort((a, b) => { return a.order < b.order ? -1 : 1; });
  552. // Skip duplicated extensions.
  553. foreach (string ext in data.extensions) {
  554. if (!_all_extensions_importer_data.extensions.contains(ext))
  555. _all_extensions_importer_data.extensions.add(ext);
  556. }
  557. _all_extensions_importer_data._filter = create_gtk_file_filter("All", _all_extensions_importer_data.extensions);
  558. }
  559. // Registers an @a importer for importing source data with the given @a
  560. // extensions. @a order is used to establish precedence when distinct importers
  561. // support similar extensions; lower values have higher precedence.
  562. public void register_importer(string name, string[] extensions, ImporterDelegate importer, Import import_result, double order)
  563. {
  564. ImporterData data = ImporterData();
  565. data.delegate = importer;
  566. data.extensions.add_all_array(extensions);
  567. data.order = order;
  568. data.import_result = import_result;
  569. register_importer_internal(name, ref data);
  570. }
  571. // Returns the preferable importer (lowest order values) which can import files
  572. // with the given @a extension.
  573. public ImporterData? find_importer_for_extension(string extension)
  574. {
  575. foreach (var imp in _importers) {
  576. if (imp.can_import_extension(extension))
  577. return imp;
  578. }
  579. return null;
  580. }
  581. public ImporterData? find_importer_for_path(string path)
  582. {
  583. return find_importer_for_extension(path_extension(path));
  584. }
  585. public bool is_type_importable(string type)
  586. {
  587. return find_importer_for_extension(type) != null;
  588. }
  589. public void import_filenames(string? destination_dir
  590. , GLib.SList<string> filenames
  591. , Import import_result
  592. , ImporterDelegate? importer
  593. , Database database
  594. , Gtk.Window? parent_window = null
  595. )
  596. {
  597. GLib.SList<string> _filenames = new GLib.SList<string>();
  598. foreach (var s in filenames)
  599. _filenames.append(s);
  600. if (destination_dir != null) {
  601. importer(import_result, database, this.absolute_path(destination_dir), filenames, parent_window);
  602. } else {
  603. Gtk.FileChooserDialog fcd = new Gtk.FileChooserDialog("Select destination folder..."
  604. , parent_window
  605. , Gtk.FileChooserAction.SELECT_FOLDER
  606. , "Cancel"
  607. , Gtk.ResponseType.CANCEL
  608. , "Select"
  609. , Gtk.ResponseType.ACCEPT
  610. );
  611. try {
  612. fcd.set_current_folder_file(GLib.File.new_for_path(this.source_dir()));
  613. } catch (GLib.Error e) {
  614. loge(e.message);
  615. }
  616. fcd.response.connect((response_id) => {
  617. if (response_id == Gtk.ResponseType.ACCEPT)
  618. importer(import_result, database, fcd.get_file().get_path(), _filenames, parent_window);
  619. fcd.destroy();
  620. });
  621. fcd.show_all();
  622. }
  623. }
  624. public void import(string? destination_dir
  625. , string[] files
  626. , Import import_result
  627. , Database database
  628. , Gtk.Window? parent_window = null
  629. )
  630. {
  631. GLib.SList<string> filenames = new GLib.SList<string>();
  632. if (files.length == 0) {
  633. Gtk.FileChooserDialog fcd = new Gtk.FileChooserDialog("Import..."
  634. , parent_window
  635. , Gtk.FileChooserAction.OPEN
  636. , "Cancel"
  637. , Gtk.ResponseType.CANCEL
  638. , "Open"
  639. , Gtk.ResponseType.ACCEPT
  640. );
  641. fcd.select_multiple = true;
  642. fcd.add_filter(_all_extensions_importer_data._filter);
  643. fcd.set_filter(_all_extensions_importer_data._filter);
  644. foreach (var importer in _importers)
  645. fcd.add_filter(importer._filter);
  646. fcd.response.connect((response_id) => {
  647. if (response_id == Gtk.ResponseType.ACCEPT) {
  648. foreach (var f in fcd.get_files())
  649. filenames.append(f.get_path());
  650. // Find importer callback.
  651. unowned ImporterDelegate? importer = null;
  652. foreach (var imp in _importers) {
  653. if (imp._filter == fcd.get_filter() && imp.can_import_filenames(filenames)) {
  654. importer = imp.delegate;
  655. break;
  656. }
  657. }
  658. // Fallback if no importer found.
  659. if (importer == null)
  660. importer = _all_extensions_importer_data.delegate;
  661. import_filenames(destination_dir, filenames, import_result, importer, database, parent_window);
  662. }
  663. fcd.destroy();
  664. });
  665. fcd.show_all();
  666. } else {
  667. foreach (var f in files)
  668. filenames.append(f);
  669. import_filenames(destination_dir, filenames, import_result, _all_extensions_importer_data.delegate, database, parent_window);
  670. }
  671. }
  672. public void delete_tree(GLib.File file) throws Error
  673. {
  674. GLib.FileEnumerator fe = file.enumerate_children("standard::*"
  675. , GLib.FileQueryInfoFlags.NOFOLLOW_SYMLINKS
  676. );
  677. GLib.FileInfo info = null;
  678. while ((info = fe.next_file()) != null) {
  679. GLib.File subfile = file.resolve_relative_path(info.get_name());
  680. if (info.get_file_type() == GLib.FileType.DIRECTORY)
  681. delete_tree(subfile);
  682. else
  683. subfile.delete();
  684. }
  685. file.delete();
  686. }
  687. }
  688. } /* namespace Crown */