project.vala 23 KB

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