project.vala 23 KB

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