editor_asset_installer.cpp 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761
  1. /**************************************************************************/
  2. /* editor_asset_installer.cpp */
  3. /**************************************************************************/
  4. /* This file is part of: */
  5. /* GODOT ENGINE */
  6. /* https://godotengine.org */
  7. /**************************************************************************/
  8. /* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
  9. /* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
  10. /* */
  11. /* Permission is hereby granted, free of charge, to any person obtaining */
  12. /* a copy of this software and associated documentation files (the */
  13. /* "Software"), to deal in the Software without restriction, including */
  14. /* without limitation the rights to use, copy, modify, merge, publish, */
  15. /* distribute, sublicense, and/or sell copies of the Software, and to */
  16. /* permit persons to whom the Software is furnished to do so, subject to */
  17. /* the following conditions: */
  18. /* */
  19. /* The above copyright notice and this permission notice shall be */
  20. /* included in all copies or substantial portions of the Software. */
  21. /* */
  22. /* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
  23. /* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
  24. /* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
  25. /* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
  26. /* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
  27. /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
  28. /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
  29. /**************************************************************************/
  30. #include "editor_asset_installer.h"
  31. #include "core/io/dir_access.h"
  32. #include "core/io/file_access.h"
  33. #include "core/io/zip_io.h"
  34. #include "editor/editor_file_system.h"
  35. #include "editor/editor_node.h"
  36. #include "editor/editor_string_names.h"
  37. #include "editor/gui/editor_file_dialog.h"
  38. #include "editor/gui/editor_toaster.h"
  39. #include "editor/progress_dialog.h"
  40. #include "editor/themes/editor_scale.h"
  41. #include "scene/gui/check_box.h"
  42. #include "scene/gui/label.h"
  43. #include "scene/gui/link_button.h"
  44. #include "scene/gui/separator.h"
  45. #include "scene/gui/split_container.h"
  46. void EditorAssetInstaller::_item_checked_cbk() {
  47. if (updating_source || !source_tree->get_edited()) {
  48. return;
  49. }
  50. updating_source = true;
  51. TreeItem *item = source_tree->get_edited();
  52. item->propagate_check(0);
  53. _fix_conflicted_indeterminate_state(source_tree->get_root(), 0);
  54. _update_confirm_button();
  55. _rebuild_destination_tree();
  56. updating_source = false;
  57. }
  58. // Determine parent state based on non-conflict children, to avoid indeterminate state, and allow toggle dir with conflicts.
  59. bool EditorAssetInstaller::_fix_conflicted_indeterminate_state(TreeItem *p_item, int p_column) {
  60. if (p_item->get_child_count() == 0) {
  61. return false;
  62. }
  63. bool all_non_conflict_checked = true;
  64. bool all_non_conflict_unchecked = true;
  65. bool has_conflict_child = false;
  66. bool has_indeterminate_child = false;
  67. TreeItem *child_item = p_item->get_first_child();
  68. while (child_item) {
  69. has_conflict_child |= _fix_conflicted_indeterminate_state(child_item, p_column);
  70. Dictionary child_meta = child_item->get_metadata(p_column);
  71. bool child_conflict = child_meta.get("is_conflict", false);
  72. if (child_conflict) {
  73. child_item->set_checked(p_column, false);
  74. has_conflict_child = true;
  75. } else {
  76. bool child_checked = child_item->is_checked(p_column);
  77. bool child_indeterminate = child_item->is_indeterminate(p_column);
  78. all_non_conflict_checked &= (child_checked || child_indeterminate);
  79. all_non_conflict_unchecked &= !child_checked;
  80. has_indeterminate_child |= child_indeterminate;
  81. }
  82. child_item = child_item->get_next();
  83. }
  84. if (has_indeterminate_child) {
  85. p_item->set_indeterminate(p_column, true);
  86. } else if (all_non_conflict_checked) {
  87. p_item->set_checked(p_column, true);
  88. } else if (all_non_conflict_unchecked) {
  89. p_item->set_checked(p_column, false);
  90. }
  91. if (has_conflict_child) {
  92. p_item->set_custom_color(p_column, get_theme_color(SNAME("error_color"), EditorStringName(Editor)));
  93. } else {
  94. p_item->clear_custom_color(p_column);
  95. }
  96. return has_conflict_child;
  97. }
  98. bool EditorAssetInstaller::_is_item_checked(const String &p_source_path) const {
  99. return file_item_map.has(p_source_path) && (file_item_map[p_source_path]->is_checked(0) || file_item_map[p_source_path]->is_indeterminate(0));
  100. }
  101. void EditorAssetInstaller::open_asset(const String &p_path, bool p_autoskip_toplevel) {
  102. package_path = p_path;
  103. asset_files.clear();
  104. Ref<FileAccess> io_fa;
  105. zlib_filefunc_def io = zipio_create_io(&io_fa);
  106. unzFile pkg = unzOpen2(p_path.utf8().get_data(), &io);
  107. if (!pkg) {
  108. EditorToaster::get_singleton()->popup_str(vformat(TTR("Error opening asset file for \"%s\" (not in ZIP format)."), asset_name), EditorToaster::SEVERITY_ERROR);
  109. return;
  110. }
  111. int ret = unzGoToFirstFile(pkg);
  112. while (ret == UNZ_OK) {
  113. //get filename
  114. unz_file_info info;
  115. char fname[16384];
  116. unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0);
  117. String source_name = String::utf8(fname);
  118. // Create intermediate directories if they aren't reported by unzip.
  119. // We are only interested in subfolders, so skip the root slash.
  120. int separator = source_name.find_char('/', 1);
  121. while (separator != -1) {
  122. String dir_name = source_name.substr(0, separator + 1);
  123. if (!dir_name.is_empty() && !asset_files.has(dir_name)) {
  124. asset_files.insert(dir_name);
  125. }
  126. separator = source_name.find_char('/', separator + 1);
  127. }
  128. if (!source_name.is_empty() && !asset_files.has(source_name)) {
  129. asset_files.insert(source_name);
  130. }
  131. ret = unzGoToNextFile(pkg);
  132. }
  133. unzClose(pkg);
  134. asset_title_label->set_text(asset_name);
  135. _check_has_toplevel();
  136. // Default to false, unless forced.
  137. skip_toplevel = p_autoskip_toplevel;
  138. skip_toplevel_check->set_block_signals(true);
  139. skip_toplevel_check->set_pressed(!skip_toplevel_check->is_disabled() && skip_toplevel);
  140. skip_toplevel_check->set_block_signals(false);
  141. _update_file_mappings();
  142. _rebuild_source_tree();
  143. _rebuild_destination_tree();
  144. popup_centered_clamped(Size2(620, 640) * EDSCALE);
  145. }
  146. void EditorAssetInstaller::_update_file_mappings() {
  147. mapped_files.clear();
  148. bool first = true;
  149. for (const String &E : asset_files) {
  150. if (first) {
  151. first = false;
  152. if (!toplevel_prefix.is_empty() && skip_toplevel) {
  153. continue;
  154. }
  155. }
  156. String path = E; // We're going to mutate it.
  157. if (!toplevel_prefix.is_empty() && skip_toplevel) {
  158. path = path.trim_prefix(toplevel_prefix);
  159. }
  160. mapped_files[E] = path;
  161. }
  162. }
  163. void EditorAssetInstaller::_rebuild_source_tree() {
  164. updating_source = true;
  165. source_tree->clear();
  166. TreeItem *root = source_tree->create_item();
  167. root->set_cell_mode(0, TreeItem::CELL_MODE_CHECK);
  168. root->set_checked(0, true);
  169. root->set_icon(0, get_theme_icon(SNAME("folder"), SNAME("FileDialog")));
  170. root->set_text(0, "/");
  171. root->set_editable(0, true);
  172. file_item_map.clear();
  173. HashMap<String, TreeItem *> directory_item_map;
  174. int num_file_conflicts = 0;
  175. first_file_conflict = nullptr;
  176. for (const String &E : asset_files) {
  177. String path = E; // We're going to mutate it.
  178. bool is_directory = false;
  179. if (path.ends_with("/")) {
  180. path = path.trim_suffix("/");
  181. is_directory = true;
  182. }
  183. TreeItem *parent_item;
  184. int separator = path.rfind_char('/');
  185. if (separator == -1) {
  186. parent_item = root;
  187. } else {
  188. String parent_path = path.substr(0, separator);
  189. HashMap<String, TreeItem *>::Iterator I = directory_item_map.find(parent_path);
  190. ERR_CONTINUE(!I);
  191. parent_item = I->value;
  192. }
  193. TreeItem *ti;
  194. if (is_directory) {
  195. ti = _create_dir_item(source_tree, parent_item, path, directory_item_map);
  196. } else {
  197. ti = _create_file_item(source_tree, parent_item, path, &num_file_conflicts);
  198. }
  199. file_item_map[E] = ti;
  200. }
  201. _update_conflict_status(num_file_conflicts);
  202. _update_confirm_button();
  203. updating_source = false;
  204. }
  205. void EditorAssetInstaller::_update_source_tree() {
  206. int num_file_conflicts = 0;
  207. first_file_conflict = nullptr;
  208. for (const KeyValue<String, TreeItem *> &E : file_item_map) {
  209. TreeItem *ti = E.value;
  210. Dictionary item_meta = ti->get_metadata(0);
  211. if ((bool)item_meta.get("is_dir", false)) {
  212. continue;
  213. }
  214. String asset_path = item_meta.get("asset_path", "");
  215. ERR_CONTINUE(asset_path.is_empty());
  216. bool target_exists = _update_source_item_status(ti, asset_path);
  217. if (target_exists) {
  218. if (first_file_conflict == nullptr) {
  219. first_file_conflict = ti;
  220. }
  221. num_file_conflicts += 1;
  222. }
  223. item_meta["is_conflict"] = target_exists;
  224. ti->set_metadata(0, item_meta);
  225. }
  226. _update_conflict_status(num_file_conflicts);
  227. _update_confirm_button();
  228. }
  229. bool EditorAssetInstaller::_update_source_item_status(TreeItem *p_item, const String &p_path) {
  230. ERR_FAIL_COND_V(!mapped_files.has(p_path), false);
  231. String target_path = target_dir_path.path_join(mapped_files[p_path]);
  232. bool target_exists = FileAccess::exists(target_path);
  233. if (target_exists) {
  234. p_item->set_custom_color(0, get_theme_color(SNAME("error_color"), EditorStringName(Editor)));
  235. p_item->set_tooltip_text(0, vformat(TTR("%s (already exists)"), target_path));
  236. p_item->set_checked(0, false);
  237. } else {
  238. p_item->clear_custom_color(0);
  239. p_item->set_tooltip_text(0, target_path);
  240. p_item->set_checked(0, true);
  241. }
  242. p_item->propagate_check(0);
  243. _fix_conflicted_indeterminate_state(p_item->get_tree()->get_root(), 0);
  244. return target_exists;
  245. }
  246. void EditorAssetInstaller::_rebuild_destination_tree() {
  247. destination_tree->clear();
  248. TreeItem *root = destination_tree->create_item();
  249. root->set_icon(0, get_theme_icon(SNAME("folder"), SNAME("FileDialog")));
  250. root->set_text(0, target_dir_path + (target_dir_path == "res://" ? "" : "/"));
  251. HashMap<String, TreeItem *> directory_item_map;
  252. for (const KeyValue<String, String> &E : mapped_files) {
  253. if (!_is_item_checked(E.key)) {
  254. continue;
  255. }
  256. String path = E.value; // We're going to mutate it.
  257. bool is_directory = false;
  258. if (path.ends_with("/")) {
  259. path = path.trim_suffix("/");
  260. is_directory = true;
  261. }
  262. TreeItem *parent_item;
  263. int separator = path.rfind_char('/');
  264. if (separator == -1) {
  265. parent_item = root;
  266. } else {
  267. String parent_path = path.substr(0, separator);
  268. HashMap<String, TreeItem *>::Iterator I = directory_item_map.find(parent_path);
  269. ERR_CONTINUE(!I);
  270. parent_item = I->value;
  271. }
  272. if (is_directory) {
  273. _create_dir_item(destination_tree, parent_item, path, directory_item_map);
  274. } else {
  275. int num_file_conflicts = 0; // Don't need it, but need to pass something.
  276. _create_file_item(destination_tree, parent_item, path, &num_file_conflicts);
  277. }
  278. }
  279. }
  280. TreeItem *EditorAssetInstaller::_create_dir_item(Tree *p_tree, TreeItem *p_parent, const String &p_path, HashMap<String, TreeItem *> &p_item_map) {
  281. TreeItem *ti = p_tree->create_item(p_parent);
  282. if (p_tree == source_tree) {
  283. ti->set_cell_mode(0, TreeItem::CELL_MODE_CHECK);
  284. ti->set_editable(0, true);
  285. ti->set_checked(0, true);
  286. ti->propagate_check(0);
  287. _fix_conflicted_indeterminate_state(ti->get_tree()->get_root(), 0);
  288. Dictionary meta;
  289. meta["asset_path"] = p_path + "/";
  290. meta["is_dir"] = true;
  291. meta["is_conflict"] = false;
  292. ti->set_metadata(0, meta);
  293. }
  294. ti->set_text(0, p_path.get_file() + "/");
  295. ti->set_icon(0, get_theme_icon(SNAME("folder"), SNAME("FileDialog")));
  296. p_item_map[p_path] = ti;
  297. return ti;
  298. }
  299. TreeItem *EditorAssetInstaller::_create_file_item(Tree *p_tree, TreeItem *p_parent, const String &p_path, int *r_conflicts) {
  300. TreeItem *ti = p_tree->create_item(p_parent);
  301. if (p_tree == source_tree) {
  302. ti->set_cell_mode(0, TreeItem::CELL_MODE_CHECK);
  303. ti->set_editable(0, true);
  304. bool target_exists = _update_source_item_status(ti, p_path);
  305. if (target_exists) {
  306. if (first_file_conflict == nullptr) {
  307. first_file_conflict = ti;
  308. }
  309. *r_conflicts += 1;
  310. }
  311. Dictionary meta;
  312. meta["asset_path"] = p_path;
  313. meta["is_dir"] = false;
  314. meta["is_conflict"] = target_exists;
  315. ti->set_metadata(0, meta);
  316. }
  317. String file = p_path.get_file();
  318. String extension = file.get_extension().to_lower();
  319. if (extension_icon_map.has(extension)) {
  320. ti->set_icon(0, extension_icon_map[extension]);
  321. } else {
  322. ti->set_icon(0, generic_extension_icon);
  323. }
  324. ti->set_text(0, file);
  325. return ti;
  326. }
  327. void EditorAssetInstaller::_update_conflict_status(int p_conflicts) {
  328. if (p_conflicts >= 1) {
  329. asset_conflicts_link->set_text(vformat(TTRN("%d file conflicts with your project and won't be installed", "%d files conflict with your project and won't be installed", p_conflicts), p_conflicts));
  330. asset_conflicts_link->show();
  331. asset_conflicts_label->hide();
  332. } else {
  333. asset_conflicts_link->hide();
  334. asset_conflicts_label->show();
  335. }
  336. }
  337. void EditorAssetInstaller::_update_confirm_button() {
  338. TreeItem *root = source_tree->get_root();
  339. get_ok_button()->set_disabled(!root || (!root->is_checked(0) && !root->is_indeterminate(0)));
  340. }
  341. void EditorAssetInstaller::_toggle_source_tree(bool p_visible, bool p_scroll_to_error) {
  342. source_tree_vb->set_visible(p_visible);
  343. show_source_files_button->set_pressed_no_signal(p_visible); // To keep in sync if triggered by something else.
  344. if (p_visible) {
  345. show_source_files_button->set_button_icon(get_editor_theme_icon(SNAME("Back")));
  346. } else {
  347. show_source_files_button->set_button_icon(get_editor_theme_icon(SNAME("Forward")));
  348. }
  349. if (p_visible && p_scroll_to_error && first_file_conflict) {
  350. source_tree->scroll_to_item(first_file_conflict, true);
  351. }
  352. }
  353. void EditorAssetInstaller::_check_has_toplevel() {
  354. // Check if the file structure has a distinct top-level directory. This is typical
  355. // for archives generated by GitHub, etc, but not for manually created ZIPs.
  356. toplevel_prefix = "";
  357. skip_toplevel_check->set_pressed(false);
  358. skip_toplevel_check->set_disabled(true);
  359. skip_toplevel_check->set_tooltip_text(TTR("This asset doesn't have a root directory, so it can't be ignored."));
  360. if (asset_files.is_empty()) {
  361. return;
  362. }
  363. String first_asset;
  364. for (const String &E : asset_files) {
  365. if (first_asset.is_empty()) { // Checking the first file/directory.
  366. if (!E.ends_with("/")) {
  367. return; // No directories in this asset.
  368. }
  369. // We will match everything else against this directory.
  370. first_asset = E;
  371. continue;
  372. }
  373. if (!E.begins_with(first_asset)) {
  374. return; // Found a file or a directory that doesn't share the same base path.
  375. }
  376. }
  377. toplevel_prefix = first_asset;
  378. skip_toplevel_check->set_disabled(false);
  379. skip_toplevel_check->set_tooltip_text(TTR("Ignore the root directory when extracting files."));
  380. }
  381. void EditorAssetInstaller::_set_skip_toplevel(bool p_checked) {
  382. if (skip_toplevel == p_checked) {
  383. return;
  384. }
  385. skip_toplevel = p_checked;
  386. _update_file_mappings();
  387. _update_source_tree();
  388. _rebuild_destination_tree();
  389. }
  390. void EditorAssetInstaller::_open_target_dir_dialog() {
  391. if (!target_dir_dialog) {
  392. target_dir_dialog = memnew(EditorFileDialog);
  393. target_dir_dialog->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_DIR);
  394. target_dir_dialog->set_title(TTR("Select Install Folder"));
  395. target_dir_dialog->set_current_dir(target_dir_path);
  396. target_dir_dialog->connect("dir_selected", callable_mp(this, &EditorAssetInstaller::_target_dir_selected));
  397. add_child(target_dir_dialog);
  398. }
  399. target_dir_dialog->popup_file_dialog();
  400. }
  401. void EditorAssetInstaller::_target_dir_selected(const String &p_target_path) {
  402. if (target_dir_path == p_target_path) {
  403. return;
  404. }
  405. target_dir_path = p_target_path;
  406. _update_file_mappings();
  407. _update_source_tree();
  408. _rebuild_destination_tree();
  409. }
  410. void EditorAssetInstaller::ok_pressed() {
  411. _install_asset();
  412. }
  413. void EditorAssetInstaller::_install_asset() {
  414. Ref<FileAccess> io_fa;
  415. zlib_filefunc_def io = zipio_create_io(&io_fa);
  416. unzFile pkg = unzOpen2(package_path.utf8().get_data(), &io);
  417. if (!pkg) {
  418. EditorToaster::get_singleton()->popup_str(vformat(TTR("Error opening asset file for \"%s\" (not in ZIP format)."), asset_name), EditorToaster::SEVERITY_ERROR);
  419. return;
  420. }
  421. Vector<String> failed_files;
  422. int ret = unzGoToFirstFile(pkg);
  423. ProgressDialog::get_singleton()->add_task("uncompress", TTR("Uncompressing Assets"), file_item_map.size());
  424. Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES);
  425. for (int idx = 0; ret == UNZ_OK; ret = unzGoToNextFile(pkg), idx++) {
  426. unz_file_info info;
  427. char fname[16384];
  428. ret = unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0);
  429. if (ret != UNZ_OK) {
  430. break;
  431. }
  432. String source_name = String::utf8(fname);
  433. if (!_is_item_checked(source_name)) {
  434. continue;
  435. }
  436. HashMap<String, String>::Iterator E = mapped_files.find(source_name);
  437. if (!E) {
  438. continue; // No remapped path means we don't want it; most likely the root.
  439. }
  440. String target_path = target_dir_path.path_join(E->value);
  441. Dictionary asset_meta = file_item_map[source_name]->get_metadata(0);
  442. bool is_dir = asset_meta.get("is_dir", false);
  443. if (is_dir) {
  444. if (target_path.ends_with("/")) {
  445. target_path = target_path.substr(0, target_path.length() - 1);
  446. }
  447. da->make_dir_recursive(target_path);
  448. } else {
  449. Vector<uint8_t> uncomp_data;
  450. uncomp_data.resize(info.uncompressed_size);
  451. unzOpenCurrentFile(pkg);
  452. unzReadCurrentFile(pkg, uncomp_data.ptrw(), uncomp_data.size());
  453. unzCloseCurrentFile(pkg);
  454. // Ensure that the target folder exists.
  455. da->make_dir_recursive(target_path.get_base_dir());
  456. Ref<FileAccess> f = FileAccess::open(target_path, FileAccess::WRITE);
  457. if (f.is_valid()) {
  458. f->store_buffer(uncomp_data.ptr(), uncomp_data.size());
  459. } else {
  460. failed_files.push_back(target_path);
  461. }
  462. ProgressDialog::get_singleton()->task_step("uncompress", target_path, idx);
  463. }
  464. }
  465. ProgressDialog::get_singleton()->end_task("uncompress");
  466. unzClose(pkg);
  467. if (failed_files.size()) {
  468. String msg = vformat(TTR("The following files failed extraction from asset \"%s\":"), asset_name) + "\n\n";
  469. for (int i = 0; i < failed_files.size(); i++) {
  470. if (i > 10) {
  471. msg += "\n" + vformat(TTR("(and %s more files)"), itos(failed_files.size() - i));
  472. break;
  473. }
  474. msg += "\n" + failed_files[i];
  475. }
  476. if (EditorNode::get_singleton() != nullptr) {
  477. EditorNode::get_singleton()->show_warning(msg);
  478. }
  479. } else {
  480. if (EditorNode::get_singleton() != nullptr) {
  481. EditorNode::get_singleton()->show_warning(vformat(TTR("Asset \"%s\" installed successfully!"), asset_name), TTR("Success!"));
  482. }
  483. }
  484. EditorFileSystem::get_singleton()->scan_changes();
  485. }
  486. void EditorAssetInstaller::set_asset_name(const String &p_asset_name) {
  487. asset_name = p_asset_name;
  488. }
  489. String EditorAssetInstaller::get_asset_name() const {
  490. return asset_name;
  491. }
  492. void EditorAssetInstaller::_notification(int p_what) {
  493. switch (p_what) {
  494. case NOTIFICATION_THEME_CHANGED: {
  495. if (show_source_files_button->is_pressed()) {
  496. show_source_files_button->set_button_icon(get_editor_theme_icon(SNAME("Back")));
  497. } else {
  498. show_source_files_button->set_button_icon(get_editor_theme_icon(SNAME("Forward")));
  499. }
  500. asset_conflicts_link->add_theme_color_override(SceneStringName(font_color), get_theme_color(SNAME("error_color"), EditorStringName(Editor)));
  501. generic_extension_icon = get_editor_theme_icon(SNAME("Object"));
  502. extension_icon_map.clear();
  503. {
  504. extension_icon_map["bmp"] = get_editor_theme_icon(SNAME("ImageTexture"));
  505. extension_icon_map["dds"] = get_editor_theme_icon(SNAME("ImageTexture"));
  506. extension_icon_map["exr"] = get_editor_theme_icon(SNAME("ImageTexture"));
  507. extension_icon_map["hdr"] = get_editor_theme_icon(SNAME("ImageTexture"));
  508. extension_icon_map["jpg"] = get_editor_theme_icon(SNAME("ImageTexture"));
  509. extension_icon_map["jpeg"] = get_editor_theme_icon(SNAME("ImageTexture"));
  510. extension_icon_map["png"] = get_editor_theme_icon(SNAME("ImageTexture"));
  511. extension_icon_map["svg"] = get_editor_theme_icon(SNAME("ImageTexture"));
  512. extension_icon_map["tga"] = get_editor_theme_icon(SNAME("ImageTexture"));
  513. extension_icon_map["webp"] = get_editor_theme_icon(SNAME("ImageTexture"));
  514. extension_icon_map["wav"] = get_editor_theme_icon(SNAME("AudioStreamWAV"));
  515. extension_icon_map["ogg"] = get_editor_theme_icon(SNAME("AudioStreamOggVorbis"));
  516. extension_icon_map["mp3"] = get_editor_theme_icon(SNAME("AudioStreamMP3"));
  517. extension_icon_map["scn"] = get_editor_theme_icon(SNAME("PackedScene"));
  518. extension_icon_map["tscn"] = get_editor_theme_icon(SNAME("PackedScene"));
  519. extension_icon_map["escn"] = get_editor_theme_icon(SNAME("PackedScene"));
  520. extension_icon_map["dae"] = get_editor_theme_icon(SNAME("PackedScene"));
  521. extension_icon_map["gltf"] = get_editor_theme_icon(SNAME("PackedScene"));
  522. extension_icon_map["glb"] = get_editor_theme_icon(SNAME("PackedScene"));
  523. extension_icon_map["gdshader"] = get_editor_theme_icon(SNAME("Shader"));
  524. extension_icon_map["gdshaderinc"] = get_editor_theme_icon(SNAME("TextFile"));
  525. extension_icon_map["gd"] = get_editor_theme_icon(SNAME("GDScript"));
  526. if (ClassDB::class_exists("CSharpScript")) {
  527. extension_icon_map["cs"] = get_editor_theme_icon(SNAME("CSharpScript"));
  528. } else {
  529. // Mark C# support as unavailable.
  530. extension_icon_map["cs"] = get_editor_theme_icon(SNAME("ImportFail"));
  531. }
  532. extension_icon_map["res"] = get_editor_theme_icon(SNAME("Resource"));
  533. extension_icon_map["tres"] = get_editor_theme_icon(SNAME("Resource"));
  534. extension_icon_map["atlastex"] = get_editor_theme_icon(SNAME("AtlasTexture"));
  535. // By default, OBJ files are imported as Mesh resources rather than PackedScenes.
  536. extension_icon_map["obj"] = get_editor_theme_icon(SNAME("MeshItem"));
  537. extension_icon_map["txt"] = get_editor_theme_icon(SNAME("TextFile"));
  538. extension_icon_map["md"] = get_editor_theme_icon(SNAME("TextFile"));
  539. extension_icon_map["rst"] = get_editor_theme_icon(SNAME("TextFile"));
  540. extension_icon_map["json"] = get_editor_theme_icon(SNAME("TextFile"));
  541. extension_icon_map["yml"] = get_editor_theme_icon(SNAME("TextFile"));
  542. extension_icon_map["yaml"] = get_editor_theme_icon(SNAME("TextFile"));
  543. extension_icon_map["toml"] = get_editor_theme_icon(SNAME("TextFile"));
  544. extension_icon_map["cfg"] = get_editor_theme_icon(SNAME("TextFile"));
  545. extension_icon_map["ini"] = get_editor_theme_icon(SNAME("TextFile"));
  546. }
  547. } break;
  548. }
  549. }
  550. EditorAssetInstaller::EditorAssetInstaller() {
  551. VBoxContainer *vb = memnew(VBoxContainer);
  552. add_child(vb);
  553. // Status bar.
  554. HBoxContainer *asset_status = memnew(HBoxContainer);
  555. vb->add_child(asset_status);
  556. Label *asset_label = memnew(Label);
  557. asset_label->set_text(TTR("Asset:"));
  558. asset_label->set_theme_type_variation("HeaderSmall");
  559. asset_status->add_child(asset_label);
  560. asset_title_label = memnew(Label);
  561. asset_status->add_child(asset_title_label);
  562. // File remapping controls.
  563. HBoxContainer *remapping_tools = memnew(HBoxContainer);
  564. vb->add_child(remapping_tools);
  565. show_source_files_button = memnew(Button);
  566. show_source_files_button->set_toggle_mode(true);
  567. show_source_files_button->set_tooltip_text(TTR("Open the list of the asset contents and select which files to install."));
  568. remapping_tools->add_child(show_source_files_button);
  569. show_source_files_button->connect(SceneStringName(toggled), callable_mp(this, &EditorAssetInstaller::_toggle_source_tree).bind(false));
  570. Button *target_dir_button = memnew(Button);
  571. target_dir_button->set_text(TTR("Change Install Folder"));
  572. target_dir_button->set_tooltip_text(TTR("Change the folder where the contents of the asset are going to be installed."));
  573. remapping_tools->add_child(target_dir_button);
  574. target_dir_button->connect(SceneStringName(pressed), callable_mp(this, &EditorAssetInstaller::_open_target_dir_dialog));
  575. remapping_tools->add_child(memnew(VSeparator));
  576. skip_toplevel_check = memnew(CheckBox);
  577. skip_toplevel_check->set_text(TTR("Ignore asset root"));
  578. skip_toplevel_check->set_tooltip_text(TTR("Ignore the root directory when extracting files."));
  579. skip_toplevel_check->connect(SceneStringName(toggled), callable_mp(this, &EditorAssetInstaller::_set_skip_toplevel));
  580. remapping_tools->add_child(skip_toplevel_check);
  581. remapping_tools->add_spacer();
  582. asset_conflicts_label = memnew(Label);
  583. asset_conflicts_label->set_theme_type_variation("HeaderSmall");
  584. asset_conflicts_label->set_text(TTR("No files conflict with your project"));
  585. remapping_tools->add_child(asset_conflicts_label);
  586. asset_conflicts_link = memnew(LinkButton);
  587. asset_conflicts_link->set_theme_type_variation("HeaderSmallLink");
  588. asset_conflicts_link->set_v_size_flags(Control::SIZE_SHRINK_CENTER);
  589. asset_conflicts_link->set_tooltip_text(TTR("Show contents of the asset and conflicting files."));
  590. asset_conflicts_link->set_visible(false);
  591. remapping_tools->add_child(asset_conflicts_link);
  592. asset_conflicts_link->connect(SceneStringName(pressed), callable_mp(this, &EditorAssetInstaller::_toggle_source_tree).bind(true, true));
  593. // File hierarchy trees.
  594. HSplitContainer *tree_split = memnew(HSplitContainer);
  595. tree_split->set_v_size_flags(Control::SIZE_EXPAND_FILL);
  596. vb->add_child(tree_split);
  597. source_tree_vb = memnew(VBoxContainer);
  598. source_tree_vb->set_h_size_flags(Control::SIZE_EXPAND_FILL);
  599. source_tree_vb->set_visible(show_source_files_button->is_pressed());
  600. tree_split->add_child(source_tree_vb);
  601. Label *source_tree_label = memnew(Label);
  602. source_tree_label->set_text(TTR("Contents of the asset:"));
  603. source_tree_label->set_theme_type_variation("HeaderSmall");
  604. source_tree_vb->add_child(source_tree_label);
  605. source_tree = memnew(Tree);
  606. source_tree->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
  607. source_tree->set_v_size_flags(Control::SIZE_EXPAND_FILL);
  608. source_tree->connect("item_edited", callable_mp(this, &EditorAssetInstaller::_item_checked_cbk));
  609. source_tree_vb->add_child(source_tree);
  610. VBoxContainer *destination_tree_vb = memnew(VBoxContainer);
  611. destination_tree_vb->set_h_size_flags(Control::SIZE_EXPAND_FILL);
  612. tree_split->add_child(destination_tree_vb);
  613. Label *destination_tree_label = memnew(Label);
  614. destination_tree_label->set_text(TTR("Installation preview:"));
  615. destination_tree_label->set_theme_type_variation("HeaderSmall");
  616. destination_tree_vb->add_child(destination_tree_label);
  617. destination_tree = memnew(Tree);
  618. destination_tree->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
  619. destination_tree->set_v_size_flags(Control::SIZE_EXPAND_FILL);
  620. destination_tree->connect("item_edited", callable_mp(this, &EditorAssetInstaller::_item_checked_cbk));
  621. destination_tree_vb->add_child(destination_tree);
  622. // Dialog configuration.
  623. set_title(TTR("Configure Asset Before Installing"));
  624. set_ok_button_text(TTR("Install"));
  625. set_hide_on_ok(true);
  626. }