project.vala 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818
  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 trigger = { trigger = true }"
  211. + "\n}"
  212. + "\n"
  213. ;
  214. string path = Path.build_filename(source_dir, "global.physics_config");
  215. FileStream fs = FileStream.open(path, "wb");
  216. if (fs != null)
  217. fs.write(text.data);
  218. }
  219. // Write main.lua
  220. {
  221. string text = "require \"core/game/camera\""
  222. + "\n"
  223. + "\nGame = Game or {"
  224. + "\n scene_graph = nil,"
  225. + "\n physics_world = nil,"
  226. + "\n render_world = nil,"
  227. + "\n camera = nil,"
  228. + "\n}"
  229. + "\n"
  230. + "\nGameBase.game = Game"
  231. + "\nGameBase.game_level = nil"
  232. + "\n"
  233. + "\nfunction Game.level_loaded()"
  234. + "\n Device.enable_resource_autoload(true)"
  235. + "\n"
  236. + "\n Game.scene_graph = World.scene_graph(GameBase.world)"
  237. + "\n Game.physics_world = World.physics_world(GameBase.world)"
  238. + "\n Game.render_world = World.render_world(GameBase.world)"
  239. + "\n Game.camera = FPSCamera(GameBase.world, GameBase.camera_unit)"
  240. + "\nend"
  241. + "\n"
  242. + "\nfunction Game.update(dt)"
  243. + "\n -- Stop the engine when the 'ESC' key is released."
  244. + "\n if Keyboard.released(Keyboard.button_id(\"escape\")) then"
  245. + "\n Device.quit()"
  246. + "\n end"
  247. + "\n"
  248. + "\n -- Update camera."
  249. + "\n local dx = Keyboard.button(Keyboard.button_id(\"d\")) - Keyboard.button(Keyboard.button_id(\"a\"))"
  250. + "\n local dy = Keyboard.button(Keyboard.button_id(\"w\")) - Keyboard.button(Keyboard.button_id(\"s\"))"
  251. + "\n local cursor_delta = Vector3.zero()"
  252. + "\n if Mouse.pressed(Mouse.button_id(\"right\")) then move = true end"
  253. + "\n if Mouse.released(Mouse.button_id(\"right\")) then move = false end"
  254. + "\n if move then cursor_delta = Mouse.axis(Mouse.axis_id(\"cursor_delta\")) end"
  255. + "\n Game.camera:rotate(dt, cursor_delta.x, cursor_delta.y)"
  256. + "\n Game.camera:move(dt, dx, dy)"
  257. + "\nend"
  258. + "\n"
  259. + "\nfunction Game.render(dt)"
  260. + "\nend"
  261. + "\n"
  262. + "\nfunction Game.shutdown()"
  263. + "\nend"
  264. + "\n"
  265. ;
  266. string path = Path.build_filename(source_dir, "main.lua");
  267. FileStream fs = FileStream.open(path, "wb");
  268. if (fs != null)
  269. fs.write(text.data);
  270. }
  271. }
  272. public int create_script(string directory, string name, bool empty)
  273. {
  274. string script_name = Path.build_filename(directory, name);
  275. string script_path = script_name + ".lua";
  276. string path = this.absolute_path(script_path);
  277. string behavior_name = camel_case(script_name.replace("/", " ")).replace(" ", "");
  278. FileStream fs = FileStream.open(path, "wb");
  279. if (fs != null) {
  280. if (empty) {
  281. return fs.puts("\n");
  282. } else {
  283. string text = "-- Note: the following table must be global and uniquely named."
  284. + "\nBehavior = Behavior or {"
  285. + "\n data = {}"
  286. + "\n}"
  287. + "\n"
  288. + "\nlocal data = Behavior.data"
  289. + "\n"
  290. + "\n-- Called after units are spawned into a world."
  291. + "\nfunction Behavior.spawned(world, units)"
  292. + "\n if data[world] == nil then"
  293. + "\n data[world] = {}"
  294. + "\n end"
  295. + "\n"
  296. + "\n local world_data = data[world]"
  297. + "\n"
  298. + "\n for _, unit in pairs(units) do"
  299. + "\n -- Store instance-specific data."
  300. + "\n if world_data[unit] == nil then"
  301. + "\n world_data[unit] = {}"
  302. + "\n end"
  303. + "\n end"
  304. + "\nend"
  305. + "\n"
  306. + "\n-- Called once per frame."
  307. + "\nfunction Behavior.update(world, dt)"
  308. + "\n local world_data = data[world]"
  309. + "\n"
  310. + "\n for unit, unit_data in pairs(world_data) do"
  311. + "\n -- Update unit."
  312. + "\n end"
  313. + "\nend"
  314. + "\n"
  315. + "\n-- Called before units are unspawned from a world."
  316. + "\nfunction Behavior.unspawned(world, units)"
  317. + "\n local world_data = data[world]"
  318. + "\n"
  319. + "\n -- Cleanup."
  320. + "\n for _, unit in pairs(units) do"
  321. + "\n if world_data[unit] then"
  322. + "\n world_data[unit] = nil"
  323. + "\n end"
  324. + "\n end"
  325. + "\nend"
  326. + "\n"
  327. + "\nreturn Behavior"
  328. + "\n"
  329. ;
  330. return fs.puts(text.replace("Behavior", behavior_name));
  331. }
  332. }
  333. return -1;
  334. }
  335. public int create_unit(string directory, string name)
  336. {
  337. string unit_path = Path.build_filename(directory, name + ".unit");
  338. string path = this.absolute_path(unit_path);
  339. Database db = new Database(this, null);
  340. Guid unit_id = Guid.new_guid();
  341. Unit unit = Unit(db, unit_id);
  342. unit.create_empty();
  343. return db.dump(path, unit_id);
  344. }
  345. public int create_state_machine(string directory, string name, string? skeleton_name)
  346. {
  347. string resource_name = Path.build_filename(directory, name);
  348. Database db = new Database(this, null);
  349. Guid machine_id = Guid.new_guid();
  350. StateMachineResource machine;
  351. if (skeleton_name == null)
  352. machine = StateMachineResource.sprite(db, machine_id, null);
  353. else
  354. machine = StateMachineResource.mesh(db, machine_id, skeleton_name);
  355. return machine.save(this, resource_name);
  356. }
  357. public int create_material(string directory, string name)
  358. {
  359. string resource_name = Path.build_filename(directory, name);
  360. Database db = new Database(this, null);
  361. Guid material_id = Guid.new_guid();
  362. MaterialResource material_resource = MaterialResource.mesh(db, material_id);
  363. return material_resource.save(this, resource_name);
  364. }
  365. // Returns the absolute path to the source directory.
  366. public string source_dir()
  367. {
  368. if (_source_dir == null)
  369. return "";
  370. else
  371. return _source_dir.get_path();
  372. }
  373. // Returns the absolute path to the toolchain directory.
  374. public string toolchain_dir()
  375. {
  376. return _toolchain_dir.get_path();
  377. }
  378. // Returns the absolute path to the data directory.
  379. public string data_dir()
  380. {
  381. return _data_dir.get_path();
  382. }
  383. // Returns the absolute path to the user-specific data for this project.
  384. public string user_dir()
  385. {
  386. return _user_dir.get_path();
  387. }
  388. public string platform()
  389. {
  390. return _platform;
  391. }
  392. public string name()
  393. {
  394. string sd = source_dir();
  395. return sd.substring(sd.last_index_of_char(GLib.Path.DIR_SEPARATOR) + 1);
  396. }
  397. public bool path_is_within_source_dir(string path)
  398. {
  399. GLib.File file = GLib.File.new_for_path(path);
  400. return file.has_prefix(_source_dir);
  401. }
  402. public void delete_garbage()
  403. {
  404. try {
  405. _level_editor_test_level.delete();
  406. _level_editor_test_package.delete();
  407. } catch (GLib.Error e) {
  408. // Ignored
  409. }
  410. }
  411. /// Converts the @a resource_id to its corresponding human-readable @a
  412. /// resource_name. It returns true if the conversion is successful, otherwise
  413. /// it returns false and sets @a resource_name to the value of @a resource_id.
  414. public bool resource_id_to_name(out string resource_name, string resource_id)
  415. {
  416. Value? name = _data_index[resource_id];
  417. if (name != null) {
  418. resource_name = (string)name;
  419. return true;
  420. }
  421. resource_name = resource_id;
  422. return false;
  423. }
  424. public Database files()
  425. {
  426. return _files;
  427. }
  428. public void add_file(string path, uint64 size, uint64 mtime)
  429. {
  430. string type = path_extension(path);
  431. string name = type == "" ? path : path.substring(0, path.last_index_of("."));
  432. Guid id = Guid.new_guid();
  433. _files.create(id, OBJECT_TYPE_FILE);
  434. _files.set_property_string(id, "path", path);
  435. _files.set_property_string(id, "type", type);
  436. _files.set_property_string(id, "name", name);
  437. _files.set_property_string(id, "size", size.to_string());
  438. _files.set_property_string(id, "mtime", mtime.to_string());
  439. _files.add_to_set(GUID_ZERO, "data", id);
  440. _map[path] = id;
  441. file_added(type, name, size, mtime);
  442. }
  443. public void change_file(string path, uint64 size, uint64 mtime)
  444. {
  445. string type = path_extension(path);
  446. string name = type == "" ? path : path.substring(0, path.last_index_of("."));
  447. Guid id = _map[path];
  448. _files.set_property_string(id, "size", size.to_string());
  449. _files.set_property_string(id, "mtime", mtime.to_string());
  450. _data_compiled = false;
  451. file_changed(type, name, size, mtime);
  452. }
  453. public void remove_file(string path)
  454. {
  455. if (!_map.has_key(path)) {
  456. logw("remove_file: map does not contain path: %s".printf(path));
  457. return;
  458. }
  459. Guid id = _map[path];
  460. file_removed(_files.get_property_string(id, "type"), _files.get_property_string(id, "name"));
  461. _files.remove_from_set(GUID_ZERO, "data", id);
  462. _files.destroy(id);
  463. _map.unset(path);
  464. }
  465. public void add_tree(string path)
  466. {
  467. tree_added(path);
  468. }
  469. public void remove_tree(string path)
  470. {
  471. tree_removed(path);
  472. }
  473. public string resource_filename(string absolute_path)
  474. {
  475. string prefix = _source_dir.get_path();
  476. if (absolute_path.has_prefix(_toolchain_dir.get_path() + "/core"))
  477. prefix = _toolchain_dir.get_path();
  478. return File.new_for_path(prefix).get_relative_path(File.new_for_path(absolute_path));
  479. }
  480. public string absolute_path(string resource_path)
  481. {
  482. string prefix = _source_dir.get_path();
  483. if (resource_path.has_prefix("core/") || resource_path == "core")
  484. prefix = _toolchain_dir.get_path();
  485. return Path.build_filename(prefix, resource_path);
  486. }
  487. public static void import_all_extensions(Import import_result
  488. , Database database
  489. , string destination_dir
  490. , SList<string> filenames
  491. , Gtk.Window? parent_window
  492. )
  493. {
  494. Project project = database._project;
  495. Gee.ArrayList<string> paths = new Gee.ArrayList<string>();
  496. foreach (var item in filenames)
  497. paths.add(item);
  498. paths.sort((a, b) => {
  499. int ext_a = a.last_index_of_char('.');
  500. int ext_b = b.last_index_of_char('.');
  501. return strcmp(a[ext_a : a.length], b[ext_b : b.length]);
  502. });
  503. while (paths.size != 0) {
  504. // Find importer for the first file in the list of selected filenames.
  505. ImporterData? importer = project.find_importer_for_path(paths[0]);
  506. if (importer == null) {
  507. import_result(ImportResult.ERROR);
  508. return;
  509. }
  510. // Create the list of all filenames importable by importer.
  511. Gee.ArrayList<string> importables = new Gee.ArrayList<string>();
  512. var cur = paths.list_iterator();
  513. for (var has_next = cur.next(); has_next; has_next = cur.next()) {
  514. string path = paths[cur.index()];
  515. if (importer.can_import_extension(path_extension(path))) {
  516. importables.add(path);
  517. cur.remove();
  518. }
  519. }
  520. // If importables is empty, filenames must have been filled with
  521. // un-importable filenames...
  522. if (importables.size == 0) {
  523. import_result(ImportResult.ERROR);
  524. return;
  525. }
  526. // Convert importables to SList<string> to be used as delegate param.
  527. SList<string> importables_list = new SList<string>();
  528. foreach (var item in importables)
  529. importables_list.append(item);
  530. importer.delegate(import_result, database, destination_dir, importables_list, parent_window);
  531. }
  532. }
  533. public class FileFilterFuncData
  534. {
  535. public string extension;
  536. public FileFilterFuncData(string ext)
  537. {
  538. extension = ext;
  539. }
  540. public bool handler(Gtk.FileFilterInfo info)
  541. {
  542. return info.filename.down().has_suffix("." + extension);
  543. }
  544. }
  545. // Returns a Gtk.FileFilter based on file @a extensions list.
  546. public Gtk.FileFilter create_gtk_file_filter(string name, Gee.ArrayList<string> extensions)
  547. {
  548. Gtk.FileFilter filter = new Gtk.FileFilter();
  549. string extensions_comma_separated = "";
  550. foreach (var ext in extensions) {
  551. extensions_comma_separated += "*.%s, ".printf(ext);
  552. FileFilterFuncData data = new FileFilterFuncData(ext);
  553. filter.add_custom(Gtk.FileFilterFlags.FILENAME, data.handler);
  554. }
  555. filter.set_filter_name(name + " (%s)".printf(extensions_comma_separated[0 : -2]));
  556. return filter;
  557. }
  558. public void register_importer_internal(string name, ref ImporterData data)
  559. {
  560. data._filter = create_gtk_file_filter(name, data.extensions);
  561. _importers.add(data);
  562. _importers.sort((a, b) => { return a.order < b.order ? -1 : 1; });
  563. // Skip duplicated extensions.
  564. foreach (string ext in data.extensions) {
  565. if (!_all_extensions_importer_data.extensions.contains(ext))
  566. _all_extensions_importer_data.extensions.add(ext);
  567. }
  568. _all_extensions_importer_data._filter = create_gtk_file_filter("All", _all_extensions_importer_data.extensions);
  569. }
  570. // Registers an @a importer for importing source data with the given @a
  571. // extensions. @a order is used to establish precedence when distinct importers
  572. // support similar extensions; lower values have higher precedence.
  573. public void register_importer(string name, string[] extensions, ImporterDelegate importer, Import import_result, double order)
  574. {
  575. ImporterData data = ImporterData();
  576. data.delegate = importer;
  577. data.extensions.add_all_array(extensions);
  578. data.order = order;
  579. data.import_result = import_result;
  580. register_importer_internal(name, ref data);
  581. }
  582. // Returns the preferable importer (lowest order values) which can import files
  583. // with the given @a extension.
  584. public ImporterData? find_importer_for_extension(string extension)
  585. {
  586. foreach (var imp in _importers) {
  587. if (imp.can_import_extension(extension))
  588. return imp;
  589. }
  590. return null;
  591. }
  592. public ImporterData? find_importer_for_path(string path)
  593. {
  594. return find_importer_for_extension(path_extension(path));
  595. }
  596. public bool is_type_importable(string type)
  597. {
  598. return find_importer_for_extension(type) != null;
  599. }
  600. public void import_filenames(string? destination_dir
  601. , GLib.SList<string> filenames
  602. , Import import_result
  603. , ImporterDelegate? importer
  604. , Database database
  605. , Gtk.Window? parent_window = null
  606. )
  607. {
  608. GLib.SList<string> _filenames = new GLib.SList<string>();
  609. foreach (var s in filenames)
  610. _filenames.append(s);
  611. if (destination_dir != null) {
  612. importer(import_result, database, this.absolute_path(destination_dir), filenames, parent_window);
  613. } else {
  614. Gtk.FileChooserDialog fcd = new Gtk.FileChooserDialog("Select destination folder..."
  615. , parent_window
  616. , Gtk.FileChooserAction.SELECT_FOLDER
  617. , "Cancel"
  618. , Gtk.ResponseType.CANCEL
  619. , "Select"
  620. , Gtk.ResponseType.ACCEPT
  621. );
  622. try {
  623. fcd.set_current_folder_file(GLib.File.new_for_path(this.source_dir()));
  624. } catch (GLib.Error e) {
  625. loge(e.message);
  626. }
  627. fcd.response.connect((response_id) => {
  628. if (response_id == Gtk.ResponseType.ACCEPT)
  629. importer(import_result, database, fcd.get_file().get_path(), _filenames, parent_window);
  630. fcd.destroy();
  631. });
  632. fcd.show_all();
  633. }
  634. }
  635. public void import(string? destination_dir
  636. , string[] files
  637. , Import import_result
  638. , Database database
  639. , Gtk.Window? parent_window = null
  640. )
  641. {
  642. GLib.SList<string> filenames = new GLib.SList<string>();
  643. if (files.length == 0) {
  644. Gtk.FileChooserDialog fcd = new Gtk.FileChooserDialog("Import..."
  645. , parent_window
  646. , Gtk.FileChooserAction.OPEN
  647. , "Cancel"
  648. , Gtk.ResponseType.CANCEL
  649. , "Open"
  650. , Gtk.ResponseType.ACCEPT
  651. );
  652. fcd.select_multiple = true;
  653. fcd.add_filter(_all_extensions_importer_data._filter);
  654. fcd.set_filter(_all_extensions_importer_data._filter);
  655. foreach (var importer in _importers)
  656. fcd.add_filter(importer._filter);
  657. fcd.response.connect((response_id) => {
  658. if (response_id == Gtk.ResponseType.ACCEPT) {
  659. foreach (var f in fcd.get_files())
  660. filenames.append(f.get_path());
  661. // Find importer callback.
  662. unowned ImporterDelegate? importer = null;
  663. foreach (var imp in _importers) {
  664. if (imp._filter == fcd.get_filter() && imp.can_import_filenames(filenames)) {
  665. importer = imp.delegate;
  666. break;
  667. }
  668. }
  669. // Fallback if no importer found.
  670. if (importer == null)
  671. importer = _all_extensions_importer_data.delegate;
  672. import_filenames(destination_dir, filenames, import_result, importer, database, parent_window);
  673. }
  674. fcd.destroy();
  675. });
  676. fcd.show_all();
  677. } else {
  678. foreach (var f in files)
  679. filenames.append(f);
  680. import_filenames(destination_dir, filenames, import_result, _all_extensions_importer_data.delegate, database, parent_window);
  681. }
  682. }
  683. public void delete_tree(GLib.File file) throws Error
  684. {
  685. GLib.FileEnumerator fe = file.enumerate_children("standard::*"
  686. , GLib.FileQueryInfoFlags.NOFOLLOW_SYMLINKS
  687. );
  688. GLib.FileInfo info = null;
  689. while ((info = fe.next_file()) != null) {
  690. GLib.File subfile = file.resolve_relative_path(info.get_name());
  691. if (info.get_file_type() == GLib.FileType.DIRECTORY)
  692. delete_tree(subfile);
  693. else
  694. subfile.delete();
  695. }
  696. file.delete();
  697. }
  698. }
  699. } /* namespace Crown */