project.vala 21 KB

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