diff --git a/editor/export/export_template_manager.cpp b/editor/export/export_template_manager.cpp index e523a2ffd175..913ed9b1b0cf 100644 --- a/editor/export/export_template_manager.cpp +++ b/editor/export/export_template_manager.cpp @@ -30,768 +30,1151 @@ #include "export_template_manager.h" +#include "core/config/engine.h" +#include "core/error/error_list.h" #include "core/io/dir_access.h" #include "core/io/json.h" +#include "core/io/marshalls.h" #include "core/io/zip_io.h" #include "core/object/callable_mp.h" #include "core/os/os.h" -#include "core/templates/rb_set.h" #include "core/version.h" #include "editor/editor_node.h" #include "editor/editor_string_names.h" -#include "editor/export/editor_export_preset.h" +#include "editor/export/editor_export.h" #include "editor/file_system/editor_file_system.h" #include "editor/file_system/editor_paths.h" #include "editor/gui/editor_bottom_panel.h" -#include "editor/gui/editor_file_dialog.h" #include "editor/gui/progress_dialog.h" #include "editor/settings/editor_settings.h" #include "editor/themes/editor_scale.h" -#include "scene/gui/line_edit.h" +#include "scene/gui/box_container.h" +#include "scene/gui/button.h" +#include "scene/gui/item_list.h" +#include "scene/gui/label.h" #include "scene/gui/link_button.h" -#include "scene/gui/margin_container.h" -#include "scene/gui/menu_button.h" #include "scene/gui/option_button.h" -#include "scene/gui/separator.h" +#include "scene/gui/split_container.h" #include "scene/gui/tree.h" -#include "scene/main/http_request.h" +#include "scene/resources/style_box.h" +#include "scene/resources/texture.h" #include "servers/display/display_server.h" -enum DownloadsAvailability { - DOWNLOADS_AVAILABLE, - DOWNLOADS_NOT_AVAILABLE_IN_OFFLINE_MODE, - DOWNLOADS_NOT_AVAILABLE_FOR_DEV_BUILDS, - DOWNLOADS_NOT_AVAILABLE_FOR_DOUBLE_BUILDS, -}; +void ExportTemplateManager::_request_mirrors() { + mirrors_list->clear(); + mirrors_empty = true; + _update_install_button(); -static DownloadsAvailability _get_downloads_availability() { // Downloadable export templates are only available for stable and official alpha/beta/RC builds // (which always have a number following their status, e.g. "alpha1"). // Therefore, don't display download-related features when using a development version // (whose builds aren't numbered). - if (String(GODOT_VERSION_STATUS) == String("dev") || - String(GODOT_VERSION_STATUS) == String("alpha") || - String(GODOT_VERSION_STATUS) == String("beta") || - String(GODOT_VERSION_STATUS) == String("rc")) { - return DOWNLOADS_NOT_AVAILABLE_FOR_DEV_BUILDS; + if (!strcmp(GODOT_VERSION_STATUS, "dev") || !strcmp(GODOT_VERSION_STATUS, "beta") || !strcmp(GODOT_VERSION_STATUS, "rc")) { + _set_empty_mirror_list(); + mirrors_list->set_tooltip_text(TTRC("Official export templates aren't available for development builds.")); +#ifdef REAL_T_IS_DOUBLE + } else if (true) { + _set_empty_mirror_list(); + mirrors_list->set_tooltip_text(TTRC("Official export templates aren't available for double-precision builds.")); +#endif + } else if (!_is_online()) { + mirrors_list->set_tooltip_text(TTRC("Template downloading is disabled in offline mode.")); + } else { + mirrors_list->set_tooltip_text(String()); } -#ifdef REAL_T_IS_DOUBLE - return DOWNLOADS_NOT_AVAILABLE_FOR_DOUBLE_BUILDS; -#else + if (mirrors_list->get_tooltip_text().is_empty()) { + const String mirrors_metadata_url = vformat("https://godotengine.org/mirrorlist/%s.json", GODOT_VERSION_FULL_CONFIG); + mirrors_requester->request(mirrors_metadata_url); + } +} - const int network_mode = EDITOR_GET("network/connection/network_mode"); +void ExportTemplateManager::_mirrors_request_completed(int p_result, int p_response_code, const PackedStringArray &p_headers, const PackedByteArray &p_body) { + mirrors_list->clear(); - if (network_mode == EditorSettings::NETWORK_OFFLINE) { - return DOWNLOADS_NOT_AVAILABLE_IN_OFFLINE_MODE; + if (p_result != HTTPRequest::RESULT_SUCCESS || p_response_code != HTTPClient::RESPONSE_OK) { + String error = TTR("Error getting the list of mirrors.") + "\n"; + if (p_result == HTTPRequest::RESULT_SUCCESS && p_response_code == HTTPClient::RESPONSE_NOT_FOUND) { + // Response successful, but wrong address. + error += TTR("No mirrors found for this version. Template download is only available for official releases."); + } else { + error += vformat(TTR("Result: %d\nResponse code: %d"), p_result, p_response_code); + } + EditorNode::get_singleton()->show_warning(error); + _set_empty_mirror_list(); + return; } - return DOWNLOADS_AVAILABLE; -#endif -} + String response_json = String::utf8((const char *)p_body.ptr(), p_body.size()); -void ExportTemplateManager::_update_template_status() { - // Fetch installed templates from the file system. - Ref da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); - const String &templates_dir = EditorPaths::get_singleton()->get_export_templates_dir(); - - Error err = da->change_dir(templates_dir); - ERR_FAIL_COND_MSG(err != OK, "Could not access templates directory at '" + templates_dir + "'."); - - RBSet templates; - da->list_dir_begin(); - if (err == OK) { - String c = da->get_next(); - while (!c.is_empty()) { - if (da->current_is_dir() && !c.begins_with(".")) { - templates.insert(c); - } - c = da->get_next(); - } + JSON json; + Error err = json.parse(response_json); + if (err != OK) { + EditorNode::get_singleton()->show_warning(TTR("Error parsing JSON with the list of mirrors. Please report this issue!")); + _set_empty_mirror_list(); + return; } - da->list_dir_end(); - // Update the state of the current version. - String current_version = GODOT_VERSION_FULL_CONFIG; - current_value->set_text(current_version); + bool mirrors_available = false; - if (templates.has(current_version)) { - current_missing_label->hide(); - current_installed_label->show(); + Dictionary mirror_data = json.get_data(); + if (mirror_data.has("mirrors")) { + Array mirrors = mirror_data["mirrors"]; + for (const Variant &mirror : mirrors) { + Dictionary m = mirror; + ERR_CONTINUE(!m.has("url") || !m.has("name")); - current_installed_hb->show(); - current_version_exists = true; - } else { - current_installed_label->hide(); - current_missing_label->show(); + mirrors_list->add_item(m["name"]); + mirrors_list->set_item_metadata(-1, m["url"]); - current_installed_hb->hide(); - current_version_exists = false; + mirrors_available = true; + } + // Hard-coded for translation. Should match the up-to-date list of mirrors. + // TTR("Official Releases mirror") } - - if (is_downloading_templates) { - install_options_vb->hide(); - download_progress_hb->show(); + if (!mirrors_available) { + _set_empty_mirror_list(); } else { - download_progress_hb->hide(); - install_options_vb->show(); - - if (templates.has(current_version)) { - current_installed_path->set_text(templates_dir.path_join(current_version)); + mirrors_list->set_disabled(false); + open_mirror->set_disabled(false); + mirrors_empty = false; + + _update_install_button(); + if (!is_downloading()) { + // Some tree buttons won't show until mirrors are loaded. + _update_template_tree(); } } +} - // Update the list of other installed versions. - installed_table->clear(); - TreeItem *installed_root = installed_table->create_item(); +void ExportTemplateManager::_set_empty_mirror_list() { + mirrors_list->add_item(TTRC("No mirrors")); + mirrors_list->set_disabled(true); + open_mirror->set_disabled(true); + mirrors_empty = true; + _update_install_button(); +} - for (RBSet::Element *E = templates.back(); E; E = E->prev()) { - String version_string = E->get(); - if (version_string == current_version) { - continue; - } +String ExportTemplateManager::_get_current_mirror_url() const { + return mirrors_list->get_item_metadata(mirrors_list->get_selected()); +} - TreeItem *ti = installed_table->create_item(installed_root); - ti->set_text(0, version_string); +void ExportTemplateManager::_update_online_mode() { + offline_container->set_visible((int)EDITOR_GET("network/connection/network_mode") == EditorSettings::NETWORK_OFFLINE); -#ifndef ANDROID_ENABLED - ti->add_button(0, get_editor_theme_icon(SNAME("Folder")), OPEN_TEMPLATE_FOLDER, false, TTR("Open the folder containing these templates.")); -#endif - ti->add_button(0, get_editor_theme_icon(SNAME("Remove")), UNINSTALL_TEMPLATE, false, TTR("Uninstall these templates.")); + if (_is_online()) { + _update_install_button(); + } else { + mirrors_list->clear(); + _set_empty_mirror_list(); } } -void ExportTemplateManager::_download_current() { - if (is_downloading_templates) { - return; - } - is_downloading_templates = true; +bool ExportTemplateManager::_is_online() const { + return !offline_container->is_visible(); +} - install_options_vb->hide(); - download_progress_hb->show(); +void ExportTemplateManager::_force_online_mode() { + EditorSettings::get_singleton()->set_setting("network/connection/network_mode", EditorSettings::NETWORK_ONLINE); + EditorSettings::get_singleton()->notify_changes(); + EditorSettings::get_singleton()->save(); - if (mirrors_available) { - String mirror_url = _get_selected_mirror(); - if (mirror_url.is_empty()) { - _set_current_progress_status(TTR("There are no mirrors available."), true); - return; - } + _update_online_mode(); + _request_mirrors(); +} - _download_template(mirror_url, true); - } else if (!is_refreshing_mirrors) { - _set_current_progress_status(TTR("Retrieving the mirror list...")); - _refresh_mirrors(); +void ExportTemplateManager::_open_mirror() { + OS::get_singleton()->shell_open(_get_current_mirror_url()); +} + +void ExportTemplateManager::_delete_confirmed() { + const String selected_version = version_list->get_item_text(version_list->get_current()); + const String template_directory = _get_template_folder_path(selected_version); + + if (_item_is_file(item_to_delete)) { + OS::get_singleton()->move_to_trash(template_directory.path_join(item_to_delete->get_text(0))); + file_metadata.erase(item_to_delete->get_text(0)); + } else { + for (TreeItem *child = item_to_delete->get_first_child(); child; child = child->get_next()) { + if (!_get_file_metadata(child)->is_missing) { + OS::get_singleton()->move_to_trash(template_directory.path_join(child->get_text(0))); + } + file_metadata.erase(child->get_text(0)); + } } + _update_template_tree(); } -void ExportTemplateManager::_download_template(const String &p_url, bool p_skip_check) { - if (!p_skip_check && is_downloading_templates) { - return; +void ExportTemplateManager::_initialize_template_data() { + // Base templates. + { + TemplateInfo info; + info.name = "Windows x86_32"; + info.description = TTRC("32-bit build for Microsoft Windows, including console wrapper."); + info.file_list = { "windows_debug_x86_32.exe", "windows_debug_x86_32_console.exe", "windows_release_x86_32.exe", "windows_release_x86_32_console.exe" }; + template_data[TemplateID::WINDOWS_X86_32] = info; + } + { + TemplateInfo info; + info.name = "Windows x86_64"; + info.description = TTRC("64-bit build for Microsoft Windows, including console wrapper."); + info.file_list = { "windows_debug_x86_64.exe", "windows_debug_x86_64_console.exe", "windows_release_x86_64.exe", "windows_release_x86_64_console.exe" }; + template_data[TemplateID::WINDOWS_X86_64] = info; + } + { + TemplateInfo info; + info.name = "Windows arm64"; + info.description = TTRC("64-bit build for Microsoft Windows on ARM architecture, including console wrapper."); + info.file_list = { "windows_debug_arm64.exe", "windows_debug_arm64_console.exe", "windows_release_arm64.exe", "windows_release_arm64_console.exe" }; + template_data[TemplateID::WINDOWS_ARM64] = info; + } + + { + TemplateInfo info; + info.name = "Linux x86_32"; + info.description = TTRC("32-bit build for Linux systems."); + info.file_list = { "linux_debug.x86_32", "linux_release.x86_32" }; + template_data[TemplateID::LINUX_X86_32] = info; + } + { + TemplateInfo info; + info.name = "Linux x86_64"; + info.description = TTRC("64-bit build for Linux systems."); + info.file_list = { "linux_debug.x86_64", "linux_release.x86_64" }; + template_data[TemplateID::LINUX_X86_64] = info; + } + { + TemplateInfo info; + info.name = "Linux arm32"; + info.description = TTRC("32-bit build for Linux systems on ARM architecture."); + info.file_list = { "linux_debug.arm32", "linux_release.arm32" }; + template_data[TemplateID::LINUX_ARM32] = info; + } + { + TemplateInfo info; + info.name = "Linux arm64"; + info.description = TTRC("64-bit build for Linux systems on ARM architecture."); + info.file_list = { "linux_debug.arm64", "linux_release.arm64" }; + template_data[TemplateID::LINUX_ARM64] = info; } - is_downloading_templates = true; - install_options_vb->hide(); - download_progress_hb->show(); - download_progress_bar->show(); - download_progress_bar->set_indeterminate(true); + { + TemplateInfo info; + info.name = "macOS"; + info.description = TTRC("Universal build for macOS."); + info.file_list = { "macos.zip" }; + template_data[TemplateID::MACOS] = info; + } - _set_current_progress_status(TTR("Starting the download...")); + { + TemplateInfo info; + info.name = "Web"; + info.description = TTRC("Regular web build with threading support. Threads improve performance, but require \"cross-origin isolated\" website to run."); + info.file_list = { "web_debug.zip", "web_release.zip" }; + template_data[TemplateID::WEB] = info; + } + { + TemplateInfo info; + info.name = TTR("Web with Extensions"); + info.description = TTRC("Web build with support for GDExtextensions. Only useful if you use GDExtensions, otherwise it only increases build size."); + info.file_list = { "web_dlink_debug.zip", "web_dlink_release.zip" }; + template_data[TemplateID::WEB_EXTENSIONS] = info; + } + { + TemplateInfo info; + info.name = TTR("Web Single-Threaded"); + info.description = TTRC("Web build without threading support."); + info.file_list = { "web_nothreads_debug.zip", "web_nothreads_release.zip" }; + template_data[TemplateID::WEB_NOTHREADS] = info; + } + { + TemplateInfo info; + info.name = TTR("Web with Extensions Single-Threaded"); + info.description = TTRC("Web build with GDExtension support and no threading support."); + info.file_list = { "web_dlink_nothreads_debug.zip", "web_dlink_nothreads_release.zip" }; + template_data[TemplateID::WEB_EXTENSIONS_NOTHREADS] = info; + } - download_templates->set_download_file(EditorPaths::get_singleton()->get_cache_dir().path_join("tmp_templates.tpz")); - download_templates->set_use_threads(true); + { + TemplateInfo info; + info.name = "Android"; + info.description = TTRC("Basic Android APK template."); + info.file_list = { "android_debug.apk", "android_release.apk" }; + template_data[TemplateID::ANDROID] = info; + } + { + TemplateInfo info; + info.name = TTR("Android Source"); + info.description = TTRC("Template for Gradle builds for Android."); + info.file_list = { "android_source.zip" }; + template_data[TemplateID::ANDROID_SOURCE] = info; + } - const String proxy_host = EDITOR_GET("network/http_proxy/host"); - const int proxy_port = EDITOR_GET("network/http_proxy/port"); - download_templates->set_http_proxy(proxy_host, proxy_port); - download_templates->set_https_proxy(proxy_host, proxy_port); + { + TemplateInfo info; + info.name = "iOS"; + info.description = TTRC("Build for Apple's iOS."); + info.file_list = { "ios.zip" }; + template_data[TemplateID::IOS] = info; + } - Error err = download_templates->request(p_url); - if (err != OK) { - _set_current_progress_status(TTR("Error requesting URL:") + " " + p_url, true); - download_progress_hb->hide(); - return; + { + TemplateInfo info; + info.name = TTR("ICU Data"); + info.description = TTRC("Line breaking dictionaries for TextServer, used by certain languages."); + info.file_list = { "icudt_godot.dat" }; + template_data[TemplateID::ICU_DATA] = info; } - set_process(true); - _set_current_progress_status(TTR("Connecting to the mirror...")); + // Platforms. + { + PlatformInfo info; + info.name = "Windows"; + info.icon = _get_platform_icon("Windows Desktop"); + info.templates = { TemplateID::WINDOWS_X86_32, TemplateID::WINDOWS_X86_64, TemplateID::WINDOWS_ARM64 }; + info.group = TTR("Desktop", "Platform Group"); + platform_map[PlatformID::WINDOWS] = info; + } + { + PlatformInfo info; + info.name = "Linux"; + info.icon = _get_platform_icon("Linux"); + info.templates = { TemplateID::LINUX_X86_32, TemplateID::LINUX_X86_64, TemplateID::LINUX_ARM32, TemplateID::LINUX_ARM64 }; + info.group = TTR("Desktop", "Platform Group"); + platform_map[PlatformID::LINUX] = info; + } + { + PlatformInfo info; + info.name = "macOS"; + info.icon = _get_platform_icon("macOS"); + info.templates = { TemplateID::MACOS }; + info.group = TTR("Desktop", "Platform Group"); + platform_map[PlatformID::MACOS] = info; + } + { + PlatformInfo info; + info.name = "Android"; + info.icon = _get_platform_icon("Android"); + info.templates = { TemplateID::ANDROID, TemplateID::ANDROID_SOURCE }; + info.group = TTR("Mobile", "Platform Group"); + platform_map[PlatformID::ANDROID] = info; + } + { + PlatformInfo info; + info.name = "iOS"; + info.icon = _get_platform_icon("iOS"); + info.templates = { TemplateID::IOS }; + info.group = TTR("Mobile", "Platform Group"); + platform_map[PlatformID::IOS] = info; + } + { + PlatformInfo info; + info.name = "Web"; + info.icon = _get_platform_icon("Web"); + info.templates = { TemplateID::WEB, TemplateID::WEB_EXTENSIONS, TemplateID::WEB_NOTHREADS, TemplateID::WEB_EXTENSIONS_NOTHREADS }; + info.group = TTR("Web", "Platform Group"); + platform_map[PlatformID::WEB] = info; + } + { + PlatformInfo info; + info.name = TTR("Common"); + info.templates = { TemplateID::ICU_DATA }; + platform_map[PlatformID::COMMON] = info; + } - ProgressIndicator *indicator = EditorNode::get_bottom_panel()->get_progress_indicator(); - indicator->set_tooltip_text(TTRC("Downloading export templates...")); - indicator->set_value(0); - indicator->show(); + // Template directory status. + DirAccess::make_dir_recursive_absolute(_get_template_folder_path(VERSION_FULL_CONFIG)); + Ref templates_dir = DirAccess::open(EditorPaths::get_singleton()->get_export_templates_dir()); + ERR_FAIL_COND(templates_dir.is_null()); + + for (const String &dir : templates_dir->get_directories()) { + if (dir == GODOT_VERSION_FULL_CONFIG) { + version_list->add_item(dir); + version_list->set_item_custom_fg_color(-1, theme_cache.current_version_color); + version_list->select(version_list->get_item_count() - 1); + } else { + version_list->add_item(dir); + } + version_list->set_item_metadata(-1, dir); + } } -void ExportTemplateManager::_download_template_completed(int p_status, int p_code, const PackedStringArray &headers, const PackedByteArray &p_data) { - switch (p_status) { - case HTTPRequest::RESULT_CANT_RESOLVE: { - _set_current_progress_status(TTR("Can't resolve the requested address."), true); - } break; - case HTTPRequest::RESULT_BODY_SIZE_LIMIT_EXCEEDED: - case HTTPRequest::RESULT_CONNECTION_ERROR: - case HTTPRequest::RESULT_CHUNKED_BODY_SIZE_MISMATCH: - case HTTPRequest::RESULT_TLS_HANDSHAKE_ERROR: - case HTTPRequest::RESULT_CANT_CONNECT: { - _set_current_progress_status(TTR("Can't connect to the mirror."), true); - } break; - case HTTPRequest::RESULT_NO_RESPONSE: { - _set_current_progress_status(TTR("No response from the mirror."), true); - } break; - case HTTPRequest::RESULT_REQUEST_FAILED: { - _set_current_progress_status(TTR("Request failed."), true); - } break; - case HTTPRequest::RESULT_REDIRECT_LIMIT_REACHED: { - _set_current_progress_status(TTR("Request ended up in a redirect loop."), true); - } break; - default: { - if (p_code != 200) { - _set_current_progress_status(TTR("Request failed:") + " " + itos(p_code), true); - } else { - _set_current_progress_status(TTR("Download complete; extracting templates...")); - String path = download_templates->get_download_file(); - - is_downloading_templates = false; - bool ret = _install_file_selected(path, true); - if (ret) { - // Clean up downloaded file. - Ref da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); - Error err = da->remove(path); - if (err != OK) { - EditorNode::get_singleton()->add_io_error(TTR("Cannot remove temporary file:") + "\n" + path + "\n"); - } - } else { - EditorNode::get_singleton()->add_io_error(vformat(TTR("Templates installation failed.\nThe problematic templates archives can be found at '%s'."), path)); +void ExportTemplateManager::_update_template_tree() { + downloading_items.clear(); + + const String selected_version = version_list->get_item_text(version_list->get_current()); + Ref template_directory = DirAccess::open(_get_template_folder_path(selected_version)); + ERR_FAIL_COND(template_directory.is_null()); + + bool is_current_version = (selected_version == GODOT_VERSION_FULL_CONFIG); + HashMap> installed_template_files; + + for (const KeyValue &KV : platform_map) { + for (TemplateID id : KV.value.templates) { + for (const String &file : template_data[id].file_list) { + if (template_directory->file_exists(file)) { + installed_template_files[id].push_back(file); } } - } break; + } } - EditorNode::get_bottom_panel()->get_progress_indicator()->hide(); - set_process(false); + _fill_template_tree(available_templates_tree, installed_template_files, is_current_version); + _fill_template_tree(installed_templates_tree, installed_template_files, is_current_version); } -void ExportTemplateManager::_cancel_template_download() { - if (!is_downloading_templates) { - return; +void ExportTemplateManager::_update_template_tree_theme(Tree *p_tree) { + if (is_downloading()) { + // Prevents hiding progress bar. + Ref empty_style; + empty_style.instantiate(); + + p_tree->add_theme_style_override(SNAME("hovered"), empty_style); + p_tree->add_theme_style_override(SNAME("hovered_dimmed"), empty_style); + p_tree->add_theme_style_override(SNAME("selected"), empty_style); + p_tree->add_theme_style_override(SNAME("selected_focus"), empty_style); + p_tree->add_theme_style_override(SNAME("hovered_selected"), empty_style); + p_tree->add_theme_style_override(SNAME("hovered_selected_focus"), empty_style); + } else { + p_tree->remove_theme_style_override(SNAME("hovered")); + p_tree->remove_theme_style_override(SNAME("hovered_dimmed")); + p_tree->remove_theme_style_override(SNAME("selected")); + p_tree->remove_theme_style_override(SNAME("selected_focus")); + p_tree->remove_theme_style_override(SNAME("hovered_selected")); + p_tree->remove_theme_style_override(SNAME("hovered_selected_focus")); } - - download_templates->cancel_request(); - download_progress_hb->hide(); - install_options_vb->show(); - is_downloading_templates = false; } -void ExportTemplateManager::_refresh_mirrors() { - if (is_refreshing_mirrors) { - return; - } - is_refreshing_mirrors = true; - - String current_version = GODOT_VERSION_FULL_CONFIG; - const String mirrors_metadata_url = "https://godotengine.org/mirrorlist/" + current_version + ".json"; - request_mirrors->request(mirrors_metadata_url); -} +void ExportTemplateManager::_fill_template_tree(Tree *p_tree, const HashMap> &p_installed_template_files, bool p_is_current_version) { + bool is_installed_tree = (p_tree == installed_templates_tree); + bool is_available_tree = !is_installed_tree; // For readability. + const LocalVector empty_vector; -void ExportTemplateManager::_refresh_mirrors_completed(int p_status, int p_code, const PackedStringArray &headers, const PackedByteArray &p_data) { - if (p_status != HTTPRequest::RESULT_SUCCESS || p_code != 200) { - EditorNode::get_singleton()->show_warning(TTR("Error getting the list of mirrors.")); - is_refreshing_mirrors = false; - if (is_downloading_templates) { - _cancel_template_download(); - } - return; + if (p_tree->get_root()) { + _update_folding_cache(p_tree->get_root()); + p_tree->clear(); } - String response_json = String::utf8((const char *)p_data.ptr(), p_data.size()); + TreeItem *platform_parent = p_tree->create_item(); + _setup_item_text(platform_parent, String()); - JSON json; - Error err = json.parse(response_json); - if (err != OK) { - EditorNode::get_singleton()->show_warning(TTR("Error parsing JSON with the list of mirrors. Please report this issue!")); - is_refreshing_mirrors = false; - if (is_downloading_templates) { - _cancel_template_download(); - } + if (is_available_tree && !p_is_current_version) { + TreeItem *nodownloadsforyou = platform_parent->create_child(); + nodownloadsforyou->set_text(0, TTR("Downloads are only available for the current Godot version.")); + nodownloadsforyou->set_custom_color(0, get_theme_color(SNAME("font_disabled_color"), EditorStringName(Editor))); return; } - mirrors_list->clear(); - mirrors_list->add_item(TTR("Best available mirror"), 0); + String current_group; + for (const KeyValue &KV : platform_map) { + const PlatformInfo &template_platform = KV.value; - mirrors_available = false; + bool all_installed = true; + bool any_installed = false; + for (TemplateID id : template_platform.templates) { + if (p_installed_template_files.has(id) && !queued_templates.has(template_data[id].name)) { + any_installed = true; + } else { + all_installed = false; + } - Dictionary mirror_data = json.get_data(); - if (mirror_data.has("mirrors")) { - Array mirrors = mirror_data["mirrors"]; + if (any_installed && !all_installed) { + // Not going to change anymore. + break; + } + } - for (int i = 0; i < mirrors.size(); i++) { - Dictionary m = mirrors[i]; - ERR_CONTINUE(!m.has("url") || !m.has("name")); + if ((is_available_tree && all_installed) || (is_installed_tree && !any_installed)) { + continue; + } - mirrors_list->add_item(m["name"]); - mirrors_list->set_item_metadata(i + 1, m["url"]); + if (is_available_tree && template_platform.group != current_group) { + // Use platform groups only for available templates. + _apply_item_folding(platform_parent); + current_group = template_platform.group; - mirrors_available = true; + if (current_group.is_empty()) { + platform_parent = p_tree->get_root(); + } else { + platform_parent = p_tree->create_item(); + if (!is_downloading()) { + _set_item_type(platform_parent, TreeItem::CELL_MODE_CHECK); + } + _setup_item_text(platform_parent, current_group); + } } - } - if (!mirrors_available) { - EditorNode::get_singleton()->show_warning(TTR("No download links found for this version. Direct download is only available for official releases.")); - if (is_downloading_templates) { - _cancel_template_download(); + + TreeItem *platform_item = platform_parent->create_child(); + if (is_available_tree && !is_downloading()) { + _set_item_type(platform_item, TreeItem::CELL_MODE_CHECK); } - } + _setup_item_text(platform_item, template_platform.name); + platform_item->set_icon(0, template_platform.icon); + platform_item->set_icon_max_width(0, theme_cache.icon_width); - is_refreshing_mirrors = false; + for (TemplateID id : template_platform.templates) { + TemplateInfo &template_info = template_data[id]; - if (is_downloading_templates) { - String mirror_url = _get_selected_mirror(); - if (mirror_url.is_empty()) { - _set_current_progress_status(TTR("There are no mirrors available."), true); - return; - } + bool is_template_installed = p_installed_template_files.has(id); + if (!queued_templates.has(template_info.name)) { + if (is_template_installed == is_available_tree) { + continue; + } + } else if (is_installed_tree) { + continue; + } - _download_template(mirror_url, true); - } -} + const LocalVector &installed_files = is_template_installed ? p_installed_template_files[id] : empty_vector; -void ExportTemplateManager::_force_online_mode() { - EditorSettings::get_singleton()->set_setting("network/connection/network_mode", EditorSettings::NETWORK_ONLINE); - EditorSettings::get_singleton()->notify_changes(); - EditorSettings::get_singleton()->save(); + TreeItem *template_item; + if (template_platform.templates.size() == 1 && template_info.name == template_platform.name) { + // Single template with the same name as platform, so it can be skipped. + template_item = platform_item; + } else { + template_item = platform_item->create_child(); + } - popup_manager(); -} + if (is_available_tree) { + if (queued_templates.has(template_info.name)) { + _set_item_type(template_item, TreeItem::CELL_MODE_CUSTOM); + template_item->add_button(0, theme_cache.cancel_icon, (int)ButtonID::CANCEL); + template_item->set_button_tooltip_text(0, -1, TTR("Cancel downloading this template.")); + } else if (!is_downloading()) { + _set_item_type(template_item, TreeItem::CELL_MODE_CHECK); + } + } + _setup_item_text(template_item, template_info.name); + template_item->set_tooltip_text(0, TTR(template_info.description)); -bool ExportTemplateManager::_humanize_http_status(HTTPRequest *p_request, String *r_status, int *r_downloaded_bytes, int *r_total_bytes) { - *r_status = ""; - *r_downloaded_bytes = -1; - *r_total_bytes = -1; - bool success = true; + bool any_missing = false; + bool any_failed = false; + for (const String &file : template_info.file_list) { + FileMetadata *meta = _get_file_metadata(file); - switch (p_request->get_http_client_status()) { - case HTTPClient::STATUS_DISCONNECTED: - *r_status = TTR("Disconnected"); - success = false; - break; - case HTTPClient::STATUS_RESOLVING: - *r_status = TTR("Resolving"); - break; - case HTTPClient::STATUS_CANT_RESOLVE: - *r_status = TTR("Can't Resolve"); - success = false; - break; - case HTTPClient::STATUS_CONNECTING: - *r_status = TTR("Connecting..."); - break; - case HTTPClient::STATUS_CANT_CONNECT: - *r_status = TTR("Can't Connect"); - success = false; - break; - case HTTPClient::STATUS_CONNECTED: - *r_status = TTR("Connected"); - break; - case HTTPClient::STATUS_REQUESTING: - *r_status = TTR("Requesting..."); - break; - case HTTPClient::STATUS_BODY: - *r_status = TTR("Downloading"); - *r_downloaded_bytes = p_request->get_downloaded_bytes(); - *r_total_bytes = p_request->get_body_size(); + TreeItem *file_item = template_item->create_child(); + file_item->set_meta(FILE_META, true); - if (p_request->get_body_size() > 0) { - *r_status += " " + String::humanize_size(p_request->get_downloaded_bytes()) + "/" + String::humanize_size(p_request->get_body_size()); - } else { - *r_status += " " + String::humanize_size(p_request->get_downloaded_bytes()); + if (meta->download_status == DownloadStatus::FAILED) { + _add_fail_reason_button(file_item, file); + any_failed = true; + } + + if (is_available_tree && !is_downloading()) { + _set_item_type(file_item, TreeItem::CELL_MODE_CHECK); + } else if (meta->download_status != DownloadStatus::NONE || queued_files.has(file)) { + if (!_status_is_finished(meta->download_status)) { + _set_item_type(file_item, TreeItem::CELL_MODE_CUSTOM); + + file_item->add_button(0, theme_cache.cancel_icon, (int)ButtonID::CANCEL); + file_item->set_button_tooltip_text(0, -1, TTRC("Cancel downloading this file.")); + downloading_items.push_back(file_item); + + if (meta->download_status == DownloadStatus::NONE) { + meta->download_status = DownloadStatus::PENDING; + } + } + } + _setup_item_text(file_item, file); + + if (is_installed_tree) { + if (installed_files.has(file)) { + file_item->add_button(0, theme_cache.remove_icon, (int)ButtonID::REMOVE); + file_item->set_button_tooltip_text(0, -1, TTR("Remove this file.")); + } else { + file_item->set_custom_color(0, theme_cache.missing_file_color); + if (p_is_current_version && !is_downloading() && _can_download_templates()) { + file_item->add_button(0, theme_cache.install_icon, (int)ButtonID::DOWNLOAD); + file_item->set_button_tooltip_text(0, -1, TTR("Download this missing file.")); + } + meta->is_missing = true; + any_missing = true; + } + } } - break; - case HTTPClient::STATUS_CONNECTION_ERROR: - *r_status = TTR("Connection Error"); - success = false; - break; - case HTTPClient::STATUS_TLS_HANDSHAKE_ERROR: - *r_status = TTR("TLS Handshake Error"); - success = false; - break; + if (any_failed || any_missing) { + template_item->set_custom_color(0, theme_cache.incomplete_template_color); + if (any_failed) { + template_item->add_button(0, theme_cache.failure_icon, (int)ButtonID::NONE); + template_item->set_button_tooltip_text(0, -1, TTR("Some files have failed to download.")); + } + + if (any_missing && p_is_current_version && !is_downloading() && _can_download_templates()) { + template_item->add_button(0, theme_cache.repair_icon, (int)ButtonID::REPAIR); + template_item->set_button_tooltip_text(0, -1, TTR("Download missing template files.")); + } + } + if (is_installed_tree) { + template_item->add_button(0, theme_cache.remove_icon, (int)ButtonID::REMOVE); + template_item->set_button_tooltip_text(0, -1, TTR("Remove this template.")); + } + _apply_item_folding(template_item, true); + } + _apply_item_folding(platform_item); } - return success; + if (p_tree->get_root()->get_child_count() == 0) { + TreeItem *empty = p_tree->create_item(); + empty->set_text(0, is_available_tree ? TTR("All templates installed.") : TTR("No templates installed.")); + empty->set_custom_color(0, get_theme_color(SNAME("font_disabled_color"), EditorStringName(Editor))); + } } -void ExportTemplateManager::_set_current_progress_status(const String &p_status, bool p_error) { - download_progress_label->set_text(p_status); +void ExportTemplateManager::_update_install_button() { + if (is_downloading()) { + install_button->set_text(TTRC("Downloading templates...")); + install_button->set_disabled(true); + install_button->set_tooltip_text(String()); + return; + } - if (p_error) { - download_progress_bar->hide(); - download_progress_label->add_theme_color_override(SceneStringName(font_color), get_theme_color(SNAME("error_color"), EditorStringName(Editor))); + download_all_enabled = true; + for (TreeItem *item = available_templates_tree->get_root(); item; item = item->get_next_in_tree()) { + if (item->is_checked(0)) { + download_all_enabled = false; + break; + } + } + if (download_all_enabled) { + install_button->set_text(TTRC("Install All Templates")); } else { - download_progress_label->add_theme_color_override(SceneStringName(font_color), get_theme_color(SceneStringName(font_color), SNAME("Label"))); + install_button->set_text(TTRC("Install Selected Templates")); } -} -void ExportTemplateManager::_set_current_progress_value(float p_value, const String &p_status) { - if (!is_visible()) { - return; + install_button->set_disabled(!_can_download_templates()); + if (install_button->is_disabled()) { + if (mirrors_empty) { + install_button->set_tooltip_text(TTRC("No mirrors available for download.")); + } else if (!_is_online()) { + install_button->set_tooltip_text(TTRC("Download not available in offline mode.")); + } else { + install_button->set_tooltip_text(TTRC("Downloads are only available for the current Godot version.")); + } + } else { + install_button->set_tooltip_text(String()); } - download_progress_bar->show(); - download_progress_bar->set_indeterminate(false); - download_progress_bar->set_value(p_value); - download_progress_label->set_text(p_status); } -void ExportTemplateManager::_install_file() { - install_file_dialog->popup_file_dialog(); +bool ExportTemplateManager::_can_download_templates() { + const String selected_version = version_list->get_item_text(version_list->get_current()); + return !mirrors_empty && _is_online() && selected_version == GODOT_VERSION_FULL_CONFIG; } -bool ExportTemplateManager::_install_file_selected(const String &p_file, bool p_skip_progress) { - Ref io_fa; - zlib_filefunc_def io = zipio_create_io(&io_fa); - - unzFile pkg = unzOpen2(p_file.utf8().get_data(), &io); - if (!pkg) { - EditorNode::get_singleton()->show_warning(TTR("Can't open the export templates file.")); - return false; +void ExportTemplateManager::_update_folding_cache(TreeItem *p_item) { + folding_cache[_get_item_path(p_item)] = p_item->is_collapsed(); + if (p_item->get_cell_mode(0) == TreeItem::CELL_MODE_CHECK) { + if (p_item->is_indeterminate(0)) { + checked_cache[_get_item_path(p_item)] = 1; + } else { + checked_cache[_get_item_path(p_item)] = p_item->is_checked(0) ? 2 : 0; + } } - int ret = unzGoToFirstFile(pkg); + for (TreeItem *child = p_item->get_first_child(); child; child = child->get_next()) { + _update_folding_cache(child); + } +} - // Count them and find version. - int fc = 0; - String version; - String contents_dir; +String ExportTemplateManager::_get_template_folder_path(const String &p_version) const { + return EditorPaths::get_singleton()->get_export_templates_dir().path_join(p_version); +} - while (ret == UNZ_OK) { - unz_file_info info; - char fname[16384]; - ret = unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0); - if (ret != UNZ_OK) { - break; +Ref ExportTemplateManager::_get_platform_icon(const String &p_platform_name) { + for (int i = 0; i < EditorExport::get_singleton()->get_export_platform_count(); i++) { + Ref platform = EditorExport::get_singleton()->get_export_platform(i); + if (platform->get_name() == p_platform_name) { + return platform->get_logo(); } + } + return Ref(); +} - String file = String::utf8(fname); - - // Skip the __MACOSX directory created by macOS's built-in file zipper. - if (file.begins_with("__MACOSX")) { - ret = unzGoToNextFile(pkg); - continue; - } +void ExportTemplateManager::_version_selected() { + if (!is_downloading()) { + file_metadata.clear(); + _update_template_tree(); + } + _update_install_button(); +} - if (file.ends_with("version.txt")) { - Vector uncomp_data; - uncomp_data.resize(info.uncompressed_size); +void ExportTemplateManager::_tree_button_clicked(TreeItem *p_item, int p_column, int p_id, MouseButton p_button) { + switch ((ButtonID)p_id) { + case ButtonID::DOWNLOAD: { + _install_templates(p_item); + } break; - // Read. - unzOpenCurrentFile(pkg); - ret = unzReadCurrentFile(pkg, uncomp_data.ptrw(), uncomp_data.size()); - ERR_BREAK_MSG(ret < 0, vformat("An error occurred while attempting to read from file: %s. This file will not be used.", file)); - unzCloseCurrentFile(pkg); + case ButtonID::REPAIR: { + p_item->set_collapsed(false); + _install_templates(p_item); + } break; - String data_str = String::utf8((const char *)uncomp_data.ptr(), uncomp_data.size()); - data_str = data_str.strip_edges(); + case ButtonID::REMOVE: { + item_to_delete = p_item; + confirm_delete->popup_centered(); + } break; - // Version number should be of the form major.minor[.patch].status[.module_config] - // so it can in theory have 3 or more slices. - if (data_str.get_slice_count(".") < 3) { - EditorNode::get_singleton()->show_warning(vformat(TTR("Invalid version.txt format inside the export templates file: %s."), data_str)); - unzClose(pkg); - return false; + case ButtonID::CANCEL: { + if (_item_is_file(p_item)) { + _cancel_item_download(p_item); + if (_is_template_download_finished(p_item->get_parent())) { + queued_templates.erase(p_item->get_parent()->get_text(0)); + } + } else { + queued_templates.erase(p_item->get_text(0)); + for (TreeItem *child = p_item->get_first_child(); child; child = child->get_next()) { + if (_get_file_metadata(child)->download_status != DownloadStatus::NONE) { + _cancel_item_download(child); + } + } } + _process_download_queue(); + _update_template_tree(); + } break; - version = data_str; - contents_dir = file.get_base_dir().trim_suffix("/").trim_suffix("\\"); - } - - if (file.get_file().size() != 0) { - fc++; - } + case ButtonID::FAIL: { + FileMetadata *meta = _get_file_metadata(p_item); + EditorNode::get_singleton()->show_warning(meta->fail_reason + ".", TTR("Download Failed")); + } break; - ret = unzGoToNextFile(pkg); + case ButtonID::NONE: { + } break; } +} - if (version.is_empty()) { - EditorNode::get_singleton()->show_warning(TTR("No version.txt found inside the export templates file.")); - unzClose(pkg); - return false; - } +void ExportTemplateManager::_tree_item_edited() { + TreeItem *edited = available_templates_tree->get_edited(); + ERR_FAIL_NULL(edited); - Ref d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); - String template_path = EditorPaths::get_singleton()->get_export_templates_dir().path_join(version); - Error err = d->make_dir_recursive(template_path); - if (err != OK) { - EditorNode::get_singleton()->show_warning(TTR("Error creating path for extracting templates:") + "\n" + template_path); - unzClose(pkg); - return false; - } + edited->propagate_check(0, false); + _update_install_button(); +} - EditorProgress *p = nullptr; - if (!p_skip_progress) { - p = memnew(EditorProgress("ltask", TTR("Extracting Export Templates"), fc)); - } +void ExportTemplateManager::_install_templates(TreeItem *p_files) { + _queue_download_tree_item(p_files ? p_files : available_templates_tree->get_root()); + download_count = queued_files.size(); - fc = 0; - ret = unzGoToFirstFile(pkg); - while (ret == UNZ_OK) { - // Get filename. - unz_file_info info; - char fname[16384]; - ret = unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0); - if (ret != UNZ_OK) { - break; - } + file_metadata.clear(); + _update_template_tree(); + _process_download_queue(); + _update_install_button(); + _update_template_tree_theme(installed_templates_tree); + _update_template_tree_theme(available_templates_tree); - if (String::utf8(fname).ends_with("/")) { - // File is a directory, ignore it. - // Directories will be created when extracting each file. - ret = unzGoToNextFile(pkg); - continue; - } + ProgressIndicator *indicator = EditorNode::get_bottom_panel()->get_progress_indicator(); + indicator->set_tooltip_text(TTRC("Downloading export templates...")); + indicator->set_value(0); + indicator->show(); +} - String file_path(String::utf8(fname).simplify_path()); +void ExportTemplateManager::_open_template_directory() { + const String selected_version = version_list->get_item_text(version_list->get_current()); + OS::get_singleton()->shell_show_in_file_manager(_get_template_folder_path(selected_version), true); +} - String file = file_path.get_file(); +void ExportTemplateManager::_queue_download_tree_item(TreeItem *p_item) { + if (_item_is_file(p_item)) { + bool valid; + bool is_installed_tree = p_item->get_tree() == installed_templates_tree; + if (is_installed_tree) { + FileMetadata *meta = _get_file_metadata(p_item); + valid = meta->is_missing; + } else { + valid = download_all_enabled || p_item->is_checked(0); + } - // Skip the __MACOSX directory created by macOS's built-in file zipper. - if (file.is_empty() || file.begins_with("__MACOSX")) { - ret = unzGoToNextFile(pkg); - continue; + if (valid) { + queued_files.insert(p_item->get_text(0)); + if (!is_installed_tree) { + queued_templates.insert(p_item->get_parent()->get_text(0)); + } } + } else { + for (TreeItem *child = p_item->get_first_child(); child; child = child->get_next()) { + _queue_download_tree_item(child); + } + } +} - Vector uncomp_data; - uncomp_data.resize(info.uncompressed_size); +void ExportTemplateManager::_process_download_queue() { + queue_update_pending = false; - // Read - unzOpenCurrentFile(pkg); - ret = unzReadCurrentFile(pkg, uncomp_data.ptrw(), uncomp_data.size()); - ERR_BREAK_MSG(ret < 0, vformat("An error occurred while attempting to read from file: %s. This file will not be used.", file)); - unzCloseCurrentFile(pkg); + int downloader_index = 0; + bool is_finished = true; + for (TreeItem *item : downloading_items) { + FileMetadata *meta = _get_file_metadata(item); - String base_dir = file_path.get_base_dir().trim_suffix("/"); + is_finished = is_finished && _status_is_finished(meta->download_status); + if (meta->download_status != DownloadStatus::PENDING) { + continue; + } - if (base_dir != contents_dir && base_dir.begins_with(contents_dir)) { - base_dir = base_dir.substr(contents_dir.length(), file_path.length()).trim_prefix("/"); - file = base_dir.path_join(file); + TemplateDownloader *downloader = _get_available_downloader(&downloader_index); + if (!downloader) { + break; + } + downloader_index++; + + Error err = downloader->download_template(item->get_text(0), _get_current_mirror_url()); + if (err == OK) { + meta->download_status = DownloadStatus::IN_PROGRESS; + meta->downloader = downloader; + } else { + _item_download_failed(item, vformat(TTR("Download request failed: %s."), TTR(error_names[err]))); + } + } - Ref da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); - ERR_CONTINUE(da.is_null()); + if (is_finished) { + // Exit "downloading mode". + queued_templates.clear(); + downloading_items.clear(); + set_process_internal(false); + _update_template_tree_theme(installed_templates_tree); + _update_template_tree_theme(available_templates_tree); + _update_install_button(); + EditorNode::get_bottom_panel()->get_progress_indicator()->hide(); + } else { + set_process_internal(true); + } +} - String output_dir = template_path.path_join(base_dir); +void ExportTemplateManager::_queue_process_download_queue() { + if (queue_update_pending) { + return; + } + callable_mp(this, &ExportTemplateManager::_process_download_queue).call_deferred(); + queue_update_pending = true; +} - if (!DirAccess::exists(output_dir)) { - Error mkdir_err = da->make_dir_recursive(output_dir); - ERR_CONTINUE(mkdir_err != OK); - } +TemplateDownloader *ExportTemplateManager::_get_available_downloader(int *r_from_index) { + int counter = -1; + for (TemplateDownloader *downloader : downloaders) { + counter++; + if (counter < *r_from_index) { + continue; } - - if (p) { - p->step(TTR("Importing:") + " " + file, fc); + if (!downloader->is_downloading()) { + *r_from_index = counter; + return downloader; } + } + return nullptr; +} - String to_write = template_path.path_join(file); - Ref f = FileAccess::open(to_write, FileAccess::WRITE); +void ExportTemplateManager::_download_request_completed(const String &p_filename) { + bool found = false; + bool template_finished = false; - if (f.is_null()) { - ret = unzGoToNextFile(pkg); - fc++; - ERR_CONTINUE_MSG(true, "Can't open file from path '" + String(to_write) + "'."); + queued_files.erase(p_filename); + for (TreeItem *item : downloading_items) { + if (item->get_text(0) != p_filename) { + continue; } + item->clear_buttons(); - f->store_buffer(uncomp_data.ptr(), uncomp_data.size()); - f.unref(); // close file. -#ifndef WINDOWS_ENABLED - FileAccess::set_unix_permissions(to_write, (info.external_fa >> 16) & 0x01FF); -#endif + FileMetadata *meta = _get_file_metadata(p_filename); + meta->downloader = nullptr; + meta->download_status = DownloadStatus::COMPLETED; + meta->is_missing = false; - ret = unzGoToNextFile(pkg); - fc++; + found = true; + template_finished = _is_template_download_finished(item->get_parent()); + if (template_finished) { + queued_templates.erase(item->get_parent()->get_text(0)); + } + break; } - - if (p) { - memdelete(p); + if (!found) { + ERR_FAIL_COND(!found); } - unzClose(pkg); + _queue_process_download_queue(); - _update_template_status(); - EditorSettings::get_singleton()->set("_export_template_download_directory", p_file.get_base_dir()); - return true; + if (template_finished) { + _update_template_tree(); + } } -void ExportTemplateManager::_uninstall_template(const String &p_version) { - uninstall_confirm->set_text(vformat(TTR("Remove templates for the version '%s'?"), p_version)); - uninstall_confirm->popup_centered(); - uninstall_version = p_version; -} +void ExportTemplateManager::_download_request_failed(const String &p_filename, const String &p_reason) { + bool found = false; + bool template_finished = false; -void ExportTemplateManager::_uninstall_template_confirmed() { - Ref da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); - const String &templates_dir = EditorPaths::get_singleton()->get_export_templates_dir(); + queued_files.erase(p_filename); + for (TreeItem *item : downloading_items) { + if (item->get_text(0) != p_filename) { + continue; + } - Error err = da->change_dir(templates_dir); - ERR_FAIL_COND_MSG(err != OK, "Could not access templates directory at '" + templates_dir + "'."); - err = da->change_dir(uninstall_version); - ERR_FAIL_COND_MSG(err != OK, "Could not access templates directory at '" + templates_dir.path_join(uninstall_version) + "'."); + FileMetadata *meta = _get_file_metadata(p_filename); + meta->downloader = nullptr; - err = da->erase_contents_recursive(); - ERR_FAIL_COND_MSG(err != OK, "Could not remove all templates in '" + templates_dir.path_join(uninstall_version) + "'."); + _item_download_failed(item, p_reason); - da->change_dir(".."); - err = da->remove(uninstall_version); - ERR_FAIL_COND_MSG(err != OK, "Could not remove templates directory at '" + templates_dir.path_join(uninstall_version) + "'."); + found = true; + template_finished = _is_template_download_finished(item->get_parent()); + if (template_finished) { + queued_templates.erase(item->get_parent()->get_text(0)); + } + break; + } + if (!found) { + ERR_FAIL_COND(!found); + } + _queue_process_download_queue(); - _update_template_status(); + if (template_finished) { + _update_template_tree(); + } } -String ExportTemplateManager::_get_selected_mirror() const { - if (mirrors_list->get_item_count() == 1) { - return ""; +bool ExportTemplateManager::_is_template_download_finished(TreeItem *p_template) { + for (TreeItem *child = p_template->get_first_child(); child; child = child->get_next()) { + if (!downloading_items.has(child)) { + continue; + } + FileMetadata *meta = _get_file_metadata(child); + if (!_status_is_finished(meta->download_status)) { + return false; + } } + return true; +} - int selected = mirrors_list->get_selected_id(); - if (selected == 0) { - // This is a special "best available" value; so pick the first available mirror from the rest of the list. - selected = 1; +void ExportTemplateManager::_apply_item_folding(TreeItem *p_item, bool p_default) { + if (folding_cache.is_empty()) { + if (p_default) { + p_item->set_collapsed(true); + } + } else { + bool *cached = folding_cache.getptr(_get_item_path(p_item)); + if (cached) { + p_item->set_collapsed(*cached); + } else if (p_default) { + p_item->set_collapsed(true); + } } - - return mirrors_list->get_item_metadata(selected); } -void ExportTemplateManager::_mirror_options_button_cbk(int p_id) { - switch (p_id) { - case VISIT_WEB_MIRROR: { - String mirror_url = _get_selected_mirror(); - if (mirror_url.is_empty()) { - EditorNode::get_singleton()->show_warning(TTR("There are no mirrors available.")); - return; - } +void ExportTemplateManager::_cancel_item_download(TreeItem *p_item) { + _item_download_failed(p_item, TTR("Canceled by the user")); + queued_files.erase(p_item->get_text(0)); - OS::get_singleton()->shell_open(mirror_url); - } break; + FileMetadata *meta = _get_file_metadata(p_item); + if (meta->downloader) { + meta->downloader->cancel_download(); + meta->downloader = nullptr; + } +} - case COPY_MIRROR_URL: { - String mirror_url = _get_selected_mirror(); - if (mirror_url.is_empty()) { - EditorNode::get_singleton()->show_warning(TTR("There are no mirrors available.")); - return; - } +void ExportTemplateManager::_item_download_failed(TreeItem *p_item, const String &p_reason) { + FileMetadata *meta = _get_file_metadata(p_item); + meta->fail_reason = p_reason; + meta->download_status = DownloadStatus::FAILED; - DisplayServer::get_singleton()->clipboard_set(mirror_url); - } break; - } + p_item->clear_buttons(); + _add_fail_reason_button(p_item); } -void ExportTemplateManager::_installed_table_button_cbk(Object *p_item, int p_column, int p_id, MouseButton p_button) { - if (p_button != MouseButton::LEFT) { - return; - } - TreeItem *ti = Object::cast_to(p_item); - if (!ti) { - return; - } +void ExportTemplateManager::_add_fail_reason_button(TreeItem *p_item, const String &p_filename) { + FileMetadata *meta = _get_file_metadata(p_filename.is_empty() ? p_item->get_text(0) : p_filename); + p_item->add_button(0, theme_cache.failure_icon, (int)ButtonID::FAIL); + p_item->set_button_tooltip_text(0, -1, vformat(TTR("Download failed.\nReason: %s."), meta->fail_reason)); +} - switch (p_id) { - case OPEN_TEMPLATE_FOLDER: { - String version_string = ti->get_text(0); - _open_template_folder(version_string); +void ExportTemplateManager::_set_item_type(TreeItem *p_item, int p_type) { + switch (p_type) { + case TreeItem::CELL_MODE_CHECK: { + p_item->set_cell_mode(0, TreeItem::CELL_MODE_CHECK); + p_item->set_editable(0, true); } break; - case UNINSTALL_TEMPLATE: { - String version_string = ti->get_text(0); - _uninstall_template(version_string); + case TreeItem::CELL_MODE_CUSTOM: { + p_item->set_cell_mode(0, TreeItem::CELL_MODE_CUSTOM); + p_item->set_custom_draw_callback(0, callable_mp(this, &ExportTemplateManager::_draw_item_progress)); } break; } } -void ExportTemplateManager::_open_template_folder(const String &p_version) { - const String &templates_dir = EditorPaths::get_singleton()->get_export_templates_dir(); - OS::get_singleton()->shell_show_in_file_manager(templates_dir.path_join(p_version), true); +void ExportTemplateManager::_setup_item_text(TreeItem *p_item, const String &p_text) { + if (p_item == p_item->get_tree()->get_root()) { + if (p_item->get_tree() == installed_templates_tree) { + p_item->set_meta(PATH_META, "installed/"); + } else { + p_item->set_meta(PATH_META, "available/"); + } + } else { + p_item->set_text(0, p_text); + const String path = p_item->get_parent()->get_meta(PATH_META).operator String() + p_text; + p_item->set_meta(PATH_META, path); + + if (p_item->get_cell_mode(0) == TreeItem::CELL_MODE_CHECK) { + int *checked = checked_cache.getptr(path); + if (checked) { + if (*checked == 1) { + p_item->set_indeterminate(0, true); + } else { + p_item->set_checked(0, *checked == 2); + } + } + } + } } -void ExportTemplateManager::popup_manager() { - _update_template_status(); +ExportTemplateManager::FileMetadata *ExportTemplateManager::_get_file_metadata(const String &p_text) const { + FileMetadata *meta = file_metadata.getptr(p_text); + if (likely(meta)) { + return meta; + } + HashMap::Iterator it = file_metadata.insert(p_text, FileMetadata()); + return &it->value; +} - switch (_get_downloads_availability()) { - case DOWNLOADS_AVAILABLE: { - current_missing_label->set_text(TTR("Export templates are missing. Download them or install from a file.")); +ExportTemplateManager::FileMetadata *ExportTemplateManager::_get_file_metadata(const TreeItem *p_item) const { + return _get_file_metadata(p_item->get_text(0)); +} - mirrors_list->clear(); - mirrors_list->add_item(TTR("Best available mirror"), 0); - mirrors_list->set_disabled(false); - mirrors_list->set_tooltip_text(""); +String ExportTemplateManager::_get_item_path(TreeItem *p_item) const { + return p_item->get_meta(PATH_META, String()); +} - mirror_options_button->set_disabled(false); +bool ExportTemplateManager::_item_is_file(TreeItem *p_item) const { + return p_item->get_meta(FILE_META, false).operator bool(); +} - download_current_button->set_disabled(false); - download_current_button->set_tooltip_text(""); +float ExportTemplateManager::_get_download_progress(const TreeItem *p_item) const { + FileMetadata *meta = _get_file_metadata(p_item); + switch (meta->download_status) { + case DownloadStatus::NONE: + case DownloadStatus::PENDING: { + return 0.0; + } - if (!is_downloading_templates) { - _refresh_mirrors(); + case DownloadStatus::IN_PROGRESS: { + if (!meta->downloader) { + return 0.0; } + return meta->downloader->get_download_progress(); + } - enable_online_hb->hide(); - } break; + case DownloadStatus::COMPLETED: { + return 1.0; + } + + case DownloadStatus::FAILED: { + return meta->progress_cache; + } + } + return 0.0; +} - case DOWNLOADS_NOT_AVAILABLE_IN_OFFLINE_MODE: { - current_missing_label->set_text(TTR("Export templates are missing. Install them from a file.")); +void ExportTemplateManager::_draw_item_progress(TreeItem *p_item, const Rect2 &p_rect) { + Tree *owning_tree = p_item->get_tree(); + owning_tree->draw_rect(p_rect, Color(0, 0, 0, 0.5)); - mirrors_list->clear(); - mirrors_list->add_item(TTR("Not available in offline mode"), 0); - mirrors_list->set_disabled(true); - mirrors_list->set_tooltip_text(TTR("Template downloading is disabled in offline mode.")); + if (!_item_is_file(p_item)) { + float progress = 0.0; + int item_count = 0; - mirror_options_button->set_disabled(true); + bool has_fail = false; + for (TreeItem *child = p_item->get_first_child(); child; child = child->get_next()) { + if (!downloading_items.has(child)) { + continue; + } + item_count++; + progress += _get_download_progress(child); - download_current_button->set_disabled(true); - download_current_button->set_tooltip_text(TTR("Template downloading is disabled in offline mode.")); + FileMetadata *meta = _get_file_metadata(child); + has_fail = has_fail || meta->download_status == DownloadStatus::FAILED; + } + progress /= item_count; + owning_tree->draw_rect(Rect2(p_rect.position, Vector2(p_rect.size.x * progress, p_rect.size.y)), has_fail ? theme_cache.download_failed_color : theme_cache.download_progress_color); + return; + } - enable_online_hb->show(); + FileMetadata *meta = _get_file_metadata(p_item); + switch (meta->download_status) { + case DownloadStatus::NONE: { } break; - case DOWNLOADS_NOT_AVAILABLE_FOR_DEV_BUILDS: { - current_missing_label->set_text(TTR("Export templates are missing. Install them from a file.")); - - mirrors_list->clear(); - mirrors_list->add_item(TTR("No templates for development builds"), 0); - mirrors_list->set_disabled(true); - mirrors_list->set_tooltip_text(TTR("Official export templates aren't available for development builds.")); + case DownloadStatus::PENDING: { + uint64_t frame = Engine::get_singleton()->get_frames_drawn(); + const Ref progress_texture = theme_cache.progress_icons[frame / 4 % 8]; + owning_tree->draw_texture(progress_texture, Vector2(p_rect.get_end().x - progress_texture->get_width(), p_rect.position.y + p_rect.size.y * 0.5 - progress_texture->get_height() * 0.5)); + } break; - mirror_options_button->set_disabled(true); + case DownloadStatus::IN_PROGRESS: { + float progress = _get_download_progress(p_item); + meta->progress_cache = progress; + owning_tree->draw_rect(Rect2(p_rect.position, Vector2(p_rect.size.x * progress, p_rect.size.y)), theme_cache.download_progress_color); + } break; - download_current_button->set_disabled(true); - download_current_button->set_tooltip_text(TTR("Official export templates aren't available for development builds.")); + case DownloadStatus::COMPLETED: { + owning_tree->draw_rect(p_rect, theme_cache.download_progress_color); + } break; - enable_online_hb->hide(); + case DownloadStatus::FAILED: { + owning_tree->draw_rect(Rect2(p_rect.position, Vector2(p_rect.size.x * _get_download_progress(p_item), p_rect.size.y)), theme_cache.download_failed_color); } break; + } +} - case DOWNLOADS_NOT_AVAILABLE_FOR_DOUBLE_BUILDS: { - current_missing_label->set_text(TTR("Export templates are missing. Install them from a file.")); +void ExportTemplateManager::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_READY: { + EditorNode::get_bottom_panel()->get_progress_indicator()->connect("clicked", callable_mp(this, &ExportTemplateManager::popup_manager)); + } break; - mirrors_list->clear(); - mirrors_list->add_item(TTR("No templates for double-precision builds"), 0); - mirrors_list->set_disabled(true); - mirrors_list->set_tooltip_text(TTR("Official export templates aren't available for double-precision builds.")); + case NOTIFICATION_TRANSLATION_CHANGED: { + if (template_data.is_empty()) { + break; + } + platform_map[PlatformID::WINDOWS].group = TTR("Desktop", "Platform Group"); + platform_map[PlatformID::LINUX].group = TTR("Desktop", "Platform Group"); + platform_map[PlatformID::MACOS].group = TTR("Desktop", "Platform Group"); + platform_map[PlatformID::WEB].group = TTR("Web", "Platform Group"); + platform_map[PlatformID::ANDROID].group = TTR("Mobile", "Platform Group"); + platform_map[PlatformID::IOS].group = TTR("Mobile", "Platform Group"); + platform_map[PlatformID::COMMON].name = TTR("Common"); + template_data[TemplateID::WEB_EXTENSIONS].name = TTR("Web with Extensions"); + template_data[TemplateID::WEB_NOTHREADS].name = TTR("Web Single-Threaded"); + template_data[TemplateID::WEB_EXTENSIONS_NOTHREADS].name = TTR("Web with Extensions Single-Threaded"); + template_data[TemplateID::ANDROID_SOURCE].name = TTR("Android Source"); + template_data[TemplateID::ANDROID_SOURCE].name = TTR("ICU Data"); + } break; - mirror_options_button->set_disabled(true); + case NOTIFICATION_THEME_CHANGED: { + open_folder_button->set_button_icon(get_editor_theme_icon("Folder")); + install_button->set_button_icon(get_editor_theme_icon("AssetStore")); + open_mirror->set_button_icon(get_editor_theme_icon("ExternalLink")); + + theme_cache.install_icon = get_editor_theme_icon("AssetStore"); + theme_cache.remove_icon = get_editor_theme_icon("Remove"); + theme_cache.repair_icon = get_editor_theme_icon("Tools"); + theme_cache.failure_icon = get_editor_theme_icon("NodeWarning"); + theme_cache.cancel_icon = get_editor_theme_icon("Close"); + for (int i = 0; i < 8; i++) { + theme_cache.progress_icons[i] = get_editor_theme_icon("Progress" + itos(i + 1)); + } - download_current_button->set_disabled(true); - download_current_button->set_tooltip_text(TTR("Official export templates aren't available for double-precision builds.")); - } - } + theme_cache.current_version_color = get_theme_color("accent_color", EditorStringName(Editor)); + theme_cache.incomplete_template_color = get_theme_color("warning_color", EditorStringName(Editor)); + theme_cache.missing_file_color = get_theme_color("error_color", EditorStringName(Editor)); + theme_cache.download_progress_color = Color(get_theme_color("success_color", EditorStringName(Editor)), 0.5); + theme_cache.download_failed_color = Color(theme_cache.missing_file_color, 0.5); - popup_centered(Size2(720, 280) * EDSCALE); -} + theme_cache.icon_width = get_theme_constant("class_icon_size", EditorStringName(Editor)); + } break; -void ExportTemplateManager::stop_download() { - download_templates->cancel_request(); - is_downloading_templates = false; -} + case NOTIFICATION_INTERNAL_PROCESS: { + available_templates_tree->queue_redraw(); + installed_templates_tree->queue_redraw(); -void ExportTemplateManager::ok_pressed() { - if (!is_downloading_templates) { - hide(); - return; + float progress = 0.0; + int indeterminate_count = download_count; + for (const TreeItem *item : downloading_items) { + progress += _get_download_progress(item); + indeterminate_count--; + } + progress += indeterminate_count; + EditorNode::get_bottom_panel()->get_progress_indicator()->set_value(progress / download_count); + } } - - hide_dialog_accept->popup_centered(); -} - -void ExportTemplateManager::_hide_dialog() { - hide(); } String ExportTemplateManager::get_android_build_directory(const Ref &p_preset) { @@ -942,256 +1325,464 @@ Error ExportTemplateManager::install_android_template_from_file(const String &p_ return OK; } -void ExportTemplateManager::_notification(int p_what) { - switch (p_what) { - case NOTIFICATION_READY: { - EditorNode::get_bottom_panel()->get_progress_indicator()->connect("clicked", callable_mp(this, &ExportTemplateManager::popup_manager)); - } break; +void ExportTemplateManager::popup_manager() { + if (template_data.is_empty()) { + _initialize_template_data(); + } + _update_online_mode(); - case NOTIFICATION_THEME_CHANGED: { - current_value->add_theme_font_override(SceneStringName(font), get_theme_font(SNAME("main"), EditorStringName(EditorFonts))); - current_missing_label->add_theme_color_override(SceneStringName(font_color), get_theme_color(SNAME("error_color"), EditorStringName(Editor))); - current_installed_label->add_theme_color_override(SceneStringName(font_color), get_theme_color(SNAME("font_disabled_color"), EditorStringName(Editor))); + if (!is_downloading()) { + _update_template_tree(); + _request_mirrors(); + } + popup_centered_clamped(Vector2i(640, 700) * EDSCALE); +} + +bool ExportTemplateManager::is_downloading() const { + return !queued_files.is_empty(); +} + +void ExportTemplateManager::stop_download() { + for (TreeItem *item : downloading_items) { + FileMetadata *meta = _get_file_metadata(item); + if (meta && !_status_is_finished(meta->download_status)) { + _cancel_item_download(item); + } + } +} + +ExportTemplateManager::ExportTemplateManager() { + set_title(TTRC("Export Template Manager")); + set_ok_button_text(TTRC("Close")); + + VBoxContainer *main_vb = memnew(VBoxContainer); + add_child(main_vb); + + HBoxContainer *download_header = memnew(HBoxContainer); + download_header->set_alignment(BoxContainer::ALIGNMENT_BEGIN); + main_vb->add_child(download_header); - mirror_options_button->set_button_icon(get_editor_theme_icon(SNAME("GuiTabMenuHl"))); + download_header->add_child(memnew(Label(TTRC("Download from:")))); + + mirrors_list = memnew(OptionButton); + mirrors_list->set_accessibility_name(TTRC("Mirror")); + download_header->add_child(mirrors_list); + + open_mirror = memnew(Button); + open_mirror->set_tooltip_text(TTRC("Open in Web Browser")); + download_header->add_child(open_mirror); + open_mirror->connect(SceneStringName(pressed), callable_mp(this, &ExportTemplateManager::_open_mirror)); + + install_button = memnew(Button); + install_button->set_h_size_flags(Control::SIZE_SHRINK_END | Control::SIZE_EXPAND); + download_header->add_child(install_button); + install_button->connect(SceneStringName(pressed), callable_mp(this, &ExportTemplateManager::_install_templates).bind((TreeItem *)nullptr)); + + HSplitContainer *main_split = memnew(HSplitContainer); + main_split->set_v_size_flags(Control::SIZE_EXPAND_FILL); + main_vb->add_child(main_split); + + VBoxContainer *side_vb = memnew(VBoxContainer); + main_split->add_child(side_vb); + + Label *version_header = memnew(Label(TTRC("Godot Version"))); + version_header->set_theme_type_variation("HeaderSmall"); + side_vb->add_child(version_header); + + version_list = memnew(ItemList); + version_list->set_accessibility_name(TTRC("Godot Version List")); + version_list->set_theme_type_variation("ItemListSecondary"); + version_list->set_v_size_flags(Control::SIZE_EXPAND_FILL); + side_vb->add_child(version_list); + version_list->connect(SceneStringName(item_selected), callable_mp(this, &ExportTemplateManager::_version_selected).unbind(1)); + + open_folder_button = memnew(Button); + open_folder_button->set_tooltip_text(TTRC("Open templates directory.")); + open_folder_button->set_h_size_flags(Control::SIZE_SHRINK_BEGIN); + side_vb->add_child(open_folder_button); + open_folder_button->connect(SceneStringName(pressed), callable_mp(this, &ExportTemplateManager::_open_template_directory)); + + VSplitContainer *center_split = memnew(VSplitContainer); + center_split->set_h_size_flags(Control::SIZE_EXPAND_FILL); + main_split->add_child(center_split); + + VBoxContainer *available_templates_container = memnew(VBoxContainer); + available_templates_container->set_v_size_flags(Control::SIZE_EXPAND_FILL); + center_split->add_child(available_templates_container); + + Label *template_header2 = memnew(Label(TTRC("Available Templates"))); + template_header2->set_theme_type_variation("HeaderSmall"); + template_header2->set_h_size_flags(Control::SIZE_EXPAND_FILL); + available_templates_container->add_child(template_header2); + + available_templates_tree = memnew(Tree); + available_templates_tree->set_accessibility_name(TTRC("Available Templates")); + available_templates_tree->set_hide_root(true); + available_templates_tree->set_theme_type_variation("TreeSecondary"); + available_templates_tree->set_v_size_flags(Control::SIZE_EXPAND_FILL); + available_templates_tree->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED); + available_templates_container->add_child(available_templates_tree); + available_templates_tree->connect("button_clicked", callable_mp(this, &ExportTemplateManager::_tree_button_clicked)); + available_templates_tree->connect("item_edited", callable_mp(this, &ExportTemplateManager::_tree_item_edited)); + + VBoxContainer *installed_templates_container = memnew(VBoxContainer); + installed_templates_container->set_v_size_flags(Control::SIZE_EXPAND_FILL); + center_split->add_child(installed_templates_container); + + Label *template_header = memnew(Label(TTRC("Installed Templates"))); + template_header->set_theme_type_variation("HeaderSmall"); + installed_templates_container->add_child(template_header); + + installed_templates_tree = memnew(Tree); + installed_templates_tree->set_accessibility_name(TTRC("Installed Templates")); + installed_templates_tree->set_hide_root(true); + installed_templates_tree->set_theme_type_variation("TreeSecondary"); + installed_templates_tree->set_v_size_flags(Control::SIZE_EXPAND_FILL); + installed_templates_tree->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED); + installed_templates_container->add_child(installed_templates_tree); + installed_templates_tree->connect("button_clicked", callable_mp(this, &ExportTemplateManager::_tree_button_clicked)); + + offline_container = memnew(HBoxContainer); + offline_container->set_alignment(BoxContainer::ALIGNMENT_CENTER); + offline_container->hide(); + main_vb->add_child(offline_container); + + Label *offline_mode_label = memnew(Label(TTRC("Offline mode, some functionality is not available."))); + offline_container->add_child(offline_mode_label); + + LinkButton *enable_online_button = memnew(LinkButton); + enable_online_button->set_text(TTRC("Go Online")); + offline_container->add_child(enable_online_button); + enable_online_button->connect(SceneStringName(pressed), callable_mp(this, &ExportTemplateManager::_force_online_mode)); + + confirm_delete = memnew(ConfirmationDialog); + confirm_delete->set_text(TTRC("Remove the selected template files? (Cannot be undone.)\nDepending on your filesystem configuration, the files will either be moved to the system trash or deleted permanently.")); + add_child(confirm_delete); + confirm_delete->connect(SceneStringName(confirmed), callable_mp(this, &ExportTemplateManager::_delete_confirmed)); + + mirrors_requester = memnew(HTTPRequest); + mirrors_requester->connect("request_completed", callable_mp(this, &ExportTemplateManager::_mirrors_request_completed)); + add_child(mirrors_requester); + + const String template_directory = _get_template_folder_path(GODOT_VERSION_FULL_CONFIG); + for (int i = 0; i < 5; i++) { + TemplateDownloader *downloader = memnew(TemplateDownloader(template_directory)); + downloader->set_use_threads(true); + add_child(downloader); + downloaders.push_back(downloader); + downloader->connect("download_completed", callable_mp(this, &ExportTemplateManager::_download_request_completed)); + downloader->connect("download_failed", callable_mp(this, &ExportTemplateManager::_download_request_failed)); + } +} + +int TemplateDownloader::_find_sequence_backwards(const PackedByteArray &p_source, const PackedByteArray &p_target) const { + const int64_t source_size = p_source.size(); + const int64_t target_size = p_target.size(); + + if (target_size == 0) { + return -1; + } + if (target_size > source_size) { + return -1; + } + const uint8_t *src_ptr = p_source.ptr(); + const uint8_t *tgt_ptr = p_target.ptr(); + + for (int64_t i = source_size - target_size; i >= 0; i--) { + if (memcmp(&src_ptr[i], tgt_ptr, target_size) == 0) { + return (int)i; + } + } + return -1; +} + +String TemplateDownloader::_get_download_error(int p_result, int p_response_code) const { + switch (p_result) { + case HTTPRequest::RESULT_CANT_RESOLVE: + return TTR("Can't resolve the requested address"); + case HTTPRequest::RESULT_BODY_SIZE_LIMIT_EXCEEDED: + case HTTPRequest::RESULT_CONNECTION_ERROR: + case HTTPRequest::RESULT_CHUNKED_BODY_SIZE_MISMATCH: + case HTTPRequest::RESULT_TLS_HANDSHAKE_ERROR: + case HTTPRequest::RESULT_CANT_CONNECT: + return TTR("Can't connect to the mirror"); + case HTTPRequest::RESULT_NO_RESPONSE: + return TTR("No response from the mirror"); + case HTTPRequest::RESULT_REQUEST_FAILED: + return TTR("Request failed"); + case HTTPRequest::RESULT_REDIRECT_LIMIT_REACHED: + return TTR("Request ended up in a redirect loop"); + } + + switch (p_response_code) { + case HTTPClient::RESPONSE_FORBIDDEN: + return TTR("Forbidden"); + case HTTPClient::RESPONSE_NOT_FOUND: + return TTR("Not found"); + default: // Handle only common errors. + return vformat(TTR("Response code: %d"), p_response_code); + } +} + +void TemplateDownloader::_request_completed(int p_result, int p_response_code, const PackedStringArray &p_headers, const PackedByteArray &p_body) { + switch (current_step) { + case Step::WAITING: { + _download_failed(String()); // Not really possible to happen, so just fail with empty message. + ERR_FAIL_MSG("Request completed on wrong step."); } break; - case NOTIFICATION_PROCESS: { - update_countdown -= get_process_delta_time(); - if (update_countdown > 0) { + case Step::QUERYING: { + if (p_result != HTTPRequest::RESULT_SUCCESS || p_response_code != HTTPClient::RESPONSE_OK) { + _download_failed(_get_download_error(p_result, p_response_code)); return; } - update_countdown = 0.5; - - String status; - int downloaded_bytes; - int total_bytes; - bool success = _humanize_http_status(download_templates, &status, &downloaded_bytes, &total_bytes); - - if (downloaded_bytes >= 0) { - if (total_bytes > 0) { - float progress = float(downloaded_bytes) / total_bytes; - EditorNode::get_bottom_panel()->get_progress_indicator()->set_value(progress); - _set_current_progress_value(progress, status); - } else { - _set_current_progress_value(0, status); + for (const String &header : p_headers) { + if (header.to_lower().begins_with("content-length:")) { + file_size = header.split(":")[1].to_int(); } - } else { - _set_current_progress_status(status); } - if (!success) { - EditorNode::get_bottom_panel()->get_progress_indicator()->hide(); - set_process(false); + current_step = Step::SCANNING; + // Request the last 64 KB of the file to read the Central Directory. + const String tail_range = vformat("Range: bytes=%d-%d", MAX(0, file_size - 0x10000), file_size - 1); + + Error err = request(url, PackedStringArray{ tail_range }, HTTPClient::METHOD_GET); + if (err != OK) { + _download_failed(vformat(TTR("Download request failed: %s."), TTR(error_names[err]))); } } break; - case NOTIFICATION_WM_CLOSE_REQUEST: { - // This won't stop the window from closing, but will show the alert if the download is active. - ok_pressed(); + case Step::SCANNING: { + if (p_result != HTTPRequest::RESULT_SUCCESS || p_response_code != HTTPClient::RESPONSE_PARTIAL_CONTENT) { + _download_failed(_get_download_error(p_result, p_response_code)); + return; + } + PackedByteArray eocd_sig = { 0x50, 0x4b, 0x05, 0x06 }; + int eocd_pos = _find_sequence_backwards(p_body, eocd_sig); + if (eocd_pos == -1) { + _download_failed(TTR("Invalid template archive header.")); + return; + } + const uint8_t *tail_data = p_body.ptr(); + + int total_entries = decode_uint16(tail_data + eocd_pos + 10); + int cd_start_offset = decode_uint32(tail_data + eocd_pos + 16); + int buffer_start_abs = file_size - p_body.size(); + int current_pos = cd_start_offset - buffer_start_abs; + const String target_path = "templates/" + filename; + + for (int i = 0; i < total_entries; i++) { + if (decode_uint32(tail_data + current_pos) != 0x02014b50) { + break; + } + + int comp_method = decode_uint16(tail_data + current_pos + 10); // 0 = Stored, 8 = Deflated + int comp_size = decode_uint32(tail_data + current_pos + 20); + int uncomp_size = decode_uint32(tail_data + current_pos + 24); + int name_len = decode_uint16(tail_data + current_pos + 28); + int extra_len = decode_uint16(tail_data + current_pos + 30); + int comm_len = decode_uint16(tail_data + current_pos + 32); + int local_offset = decode_uint32(tail_data + current_pos + 42); + + int full_record_len = 46 + name_len + extra_len + comm_len; + const PackedByteArray raw_record = p_body.slice(current_pos, current_pos + full_record_len); + const String file_name = String::utf8((const char *)p_body.slice(current_pos + 46, current_pos + 46 + name_len).ptr(), name_len); + + if (file_name == target_path) { + file_info.offset = local_offset; + file_info.compressed_size = comp_size; + file_info.uncompressed_size = uncomp_size; + file_info.raw_record = raw_record; + file_info.method = comp_method; + file_info.name = file_name; + break; + } + current_pos += full_record_len; + } + + if (file_info.name.is_empty()) { + _download_failed(TTR("Requested template not found in the archive.")); + return; + } + + int start_byte = file_info.offset; + int end_byte = file_info.offset + file_info.compressed_size + file_info.raw_record.size(); + + current_step = Step::DOWNLOADING; + const String data_range = vformat("Range: bytes=%d-%d", start_byte, end_byte); + + Error err = request(url, PackedStringArray{ data_range }, HTTPClient::METHOD_GET); + if (err != OK) { + _download_failed(vformat(TTR("Download request failed: %s."), TTR(error_names[err]))); + } } break; - } -} -ExportTemplateManager::ExportTemplateManager() { - set_title(TTR("Export Template Manager")); - set_hide_on_ok(false); - set_ok_button_text(TTR("Close")); + case Step::DOWNLOADING: { + if (p_result != HTTPRequest::RESULT_SUCCESS || p_response_code != HTTPClient::RESPONSE_PARTIAL_CONTENT) { + _download_failed(_get_download_error(p_result, p_response_code)); + return; + } + const String mini_zip_path = EditorPaths::get_singleton()->get_temp_dir().path_join(filename + ".zip"); + const uint8_t *fragment = p_body.ptr(); - VBoxContainer *main_vb = memnew(VBoxContainer); - add_child(main_vb); + int local_name_len = decode_uint16(fragment + 26); + int local_extra_len = decode_uint16(fragment + 28); + int full_file_chunk_size = 30 + local_name_len + local_extra_len + file_info.compressed_size; - // Current version controls. - HBoxContainer *current_hb = memnew(HBoxContainer); - main_vb->add_child(current_hb); - - Label *current_label = memnew(Label); - current_label->set_theme_type_variation("HeaderSmall"); - current_label->set_text(TTR("Current Version:")); - current_hb->add_child(current_label); - - current_value = memnew(Label); - current_value->set_focus_mode(Control::FOCUS_ACCESSIBILITY); - current_hb->add_child(current_value); - - // Current version statuses. - // Status: Current version is missing. - current_missing_label = memnew(Label); - current_missing_label->set_theme_type_variation("HeaderSmall"); - - current_missing_label->set_h_size_flags(Control::SIZE_EXPAND_FILL); - current_missing_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_RIGHT); - current_hb->add_child(current_missing_label); - - // Status: Current version is installed. - current_installed_label = memnew(Label); - current_installed_label->set_theme_type_variation("HeaderSmall"); - current_installed_label->set_h_size_flags(Control::SIZE_EXPAND_FILL); - current_installed_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_RIGHT); - current_installed_label->set_text(TTR("Export templates are installed and ready to be used.")); - current_hb->add_child(current_installed_label); - current_installed_label->hide(); - - // Currently installed template. - current_installed_hb = memnew(HBoxContainer); - main_vb->add_child(current_installed_hb); - - current_installed_path = memnew(LineEdit); - current_installed_path->set_editable(false); - current_installed_path->set_h_size_flags(Control::SIZE_EXPAND_FILL); - current_installed_path->set_accessibility_name(TTRC("Installed Path")); - current_installed_hb->add_child(current_installed_path); - -#ifndef ANDROID_ENABLED - Button *current_open_button = memnew(Button); - current_open_button->set_text(TTR("Open Folder")); - current_open_button->set_tooltip_text(TTR("Open the folder containing installed templates for the current version.")); - current_installed_hb->add_child(current_open_button); - current_open_button->connect(SceneStringName(pressed), callable_mp(this, &ExportTemplateManager::_open_template_folder).bind(GODOT_VERSION_FULL_CONFIG)); -#endif + if (p_body.size() < full_file_chunk_size) { + _download_failed(vformat(TTR("Archive fragment too small. Loaded: %d, required: %d."), p_body.size(), full_file_chunk_size)); + return; + } - current_uninstall_button = memnew(Button); - current_uninstall_button->set_text(TTR("Uninstall")); - current_uninstall_button->set_tooltip_text(TTR("Uninstall templates for the current version.")); - current_installed_hb->add_child(current_uninstall_button); - current_uninstall_button->connect(SceneStringName(pressed), callable_mp(this, &ExportTemplateManager::_uninstall_template).bind(GODOT_VERSION_FULL_CONFIG)); + const PackedByteArray clean_fragment = p_body.slice(0, full_file_chunk_size); - main_vb->add_child(memnew(HSeparator)); + PackedByteArray cd_record = file_info.raw_record.duplicate(); + uint8_t *record_write = cd_record.ptrw(); + encode_uint32(0, record_write + 42); // IMPORTANT: Set the offset to 0, as the file is at the very beginning of the mini-ZIP. - // Download and install section. - HBoxContainer *install_templates_hb = memnew(HBoxContainer); - main_vb->add_child(install_templates_hb); + // EOCD (End of Central Directory) + PackedByteArray eocd; + eocd.resize_initialized(22); - // Download and install buttons are available. - install_options_vb = memnew(VBoxContainer); - install_options_vb->set_h_size_flags(Control::SIZE_EXPAND_FILL); - install_templates_hb->add_child(install_options_vb); + uint8_t *eocd_write = eocd.ptrw(); + encode_uint32(0x06054b50, eocd_write); // Signature (4 bytes). + // Offsets 4-7 remain 0 (disk numbers). + encode_uint16(1, eocd_write + 8); // Number of entries on this disk (2 bytes). + encode_uint16(1, eocd_write + 10); // Total number of entries (2 bytes). + encode_uint32(cd_record.size(), eocd_write + 12); // Central Directory size (4 bytes). + encode_uint32(clean_fragment.size(), eocd_write + 16); // CD start offset (after file data) (4 bytes). - HBoxContainer *download_install_hb = memnew(HBoxContainer); - install_options_vb->add_child(download_install_hb); + // Write Mini-Zip to a file. + Ref f = FileAccess::open(mini_zip_path, FileAccess::WRITE); + if (f.is_null()) { + _download_failed(TTR("Failed to open mini-ZIP for writing.")); + return; + } + f->store_buffer(clean_fragment); + f->store_buffer(cd_record); + f->store_buffer(eocd); + f.unref(); + + PackedByteArray extracted_data; + { + // Read back the mini-ZIP. + Ref zip_access; + zlib_filefunc_def io = zipio_create_io(&zip_access); + unzFile uzf = unzOpen2(mini_zip_path.utf8().get_data(), &io); + if (!uzf) { + _download_failed(TTR("ZIP reader could not open mini-ZIP.")); + return; + } - Label *mirrors_label = memnew(Label); - mirrors_label->set_text(TTR("Download from:")); - download_install_hb->add_child(mirrors_label); + // IMPORTANT: The path in the ZIP reader must exactly match the one in the CD. - mirrors_list = memnew(OptionButton); - mirrors_list->set_accessibility_name(TTRC("Mirror")); - mirrors_list->set_custom_minimum_size(Size2(280, 0) * EDSCALE); - download_install_hb->add_child(mirrors_list); + int err = UNZ_OK; - request_mirrors = memnew(HTTPRequest); - mirrors_list->add_child(request_mirrors); - request_mirrors->connect("request_completed", callable_mp(this, &ExportTemplateManager::_refresh_mirrors_completed)); + // Locate and open the file. + err = godot_unzip_locate_file(uzf, file_info.name, true); + if (err != UNZ_OK) { + _download_failed(TTR("File does not exist in zip archive.")); + return; + } - mirror_options_button = memnew(MenuButton); - mirror_options_button->set_accessibility_name(TTRC("Mirror Options")); - mirror_options_button->get_popup()->add_item(TTR("Open in Web Browser"), VISIT_WEB_MIRROR); - mirror_options_button->get_popup()->add_item(TTR("Copy Mirror URL"), COPY_MIRROR_URL); - download_install_hb->add_child(mirror_options_button); - mirror_options_button->get_popup()->connect(SceneStringName(id_pressed), callable_mp(this, &ExportTemplateManager::_mirror_options_button_cbk)); + err = unzOpenCurrentFile(uzf); + if (err != UNZ_OK) { + _download_failed(TTR("Could not open file within zip archive.")); + return; + } - download_install_hb->add_spacer(); + // Read the file info. + unz_file_info info; + err = unzGetCurrentFileInfo(uzf, &info, nullptr, 0, nullptr, 0, nullptr, 0); + if (err != UNZ_OK) { + _download_failed(TTR("Unable to read file information from zip archive.")); + return; + } - download_current_button = memnew(Button); - download_current_button->set_text(TTR("Download and Install")); - download_current_button->set_tooltip_text(TTR("Download and install templates for the current version from the best possible mirror.")); - download_install_hb->add_child(download_current_button); - download_current_button->connect(SceneStringName(pressed), callable_mp(this, &ExportTemplateManager::_download_current)); + // Read the file data. + extracted_data.resize(info.uncompressed_size); + uint8_t *buffer = extracted_data.ptrw(); + int to_read = extracted_data.size(); + while (to_read > 0) { + int bytes_read = unzReadCurrentFile(uzf, buffer, to_read); + if (bytes_read < 0 || (bytes_read == UNZ_EOF && to_read != 0)) { + _download_failed(TTR("IO/zlib error reading file from zip archive.")); + return; + } + buffer += bytes_read; + to_read -= bytes_read; + } - HBoxContainer *install_file_hb = memnew(HBoxContainer); - install_file_hb->set_alignment(BoxContainer::ALIGNMENT_END); - install_options_vb->add_child(install_file_hb); + // Verify the data and return. + err = unzCloseCurrentFile(uzf); + if (err != UNZ_OK) { + _download_failed(TTR("CRC error reading file from zip archive.")); + return; + } + } - install_file_button = memnew(Button); - install_file_button->set_text(TTR("Install from File")); - install_file_button->set_tooltip_text(TTR("Install templates from a local file.")); - install_file_hb->add_child(install_file_button); - install_file_button->connect(SceneStringName(pressed), callable_mp(this, &ExportTemplateManager::_install_file)); + if (extracted_data.is_empty()) { + _download_failed(TTR("Mini-ZIP data was empty.")); + // The mini-ZIP is not deleted for inspection. + return; + } else { + DirAccess::remove_absolute(mini_zip_path); - enable_online_hb = memnew(HBoxContainer); - install_options_vb->add_child(enable_online_hb); + f = FileAccess::open(target_directory.path_join(filename), FileAccess::WRITE); + if (f.is_null()) { + _download_failed(TTR("Failed to template file for writing.")); + return; + } + f->store_buffer(extracted_data); + f.unref(); - Label *enable_online_label = memnew(Label); - enable_online_label->set_text(TTR("Online mode is needed to download the templates.")); - enable_online_hb->add_child(enable_online_label); + current_step = Step::WAITING; + emit_signal(SNAME("download_completed"), filename); + } + } break; + } +} - LinkButton *enable_online_button = memnew(LinkButton); - enable_online_button->set_v_size_flags(Control::SIZE_SHRINK_CENTER); - enable_online_button->set_text(TTR("Go Online")); - enable_online_hb->add_child(enable_online_button); - enable_online_button->connect(SceneStringName(pressed), callable_mp(this, &ExportTemplateManager::_force_online_mode)); +void TemplateDownloader::_download_failed(const String &p_reason) { + const String failed_file = filename; + cancel_download(); + emit_signal(SNAME("download_failed"), failed_file, p_reason); +} + +void TemplateDownloader::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_POSTINITIALIZE: { + connect(SNAME("request_completed"), callable_mp(this, &TemplateDownloader::_request_completed), CONNECT_DEFERRED); + } break; + } +} + +void TemplateDownloader::_bind_methods() { + ADD_SIGNAL(MethodInfo("download_completed")); + ADD_SIGNAL(MethodInfo("download_failed")); +} - // Templates are being downloaded; buttons unavailable. - download_progress_hb = memnew(HBoxContainer); - download_progress_hb->set_h_size_flags(Control::SIZE_EXPAND_FILL); - install_templates_hb->add_child(download_progress_hb); - download_progress_hb->hide(); - - download_progress_bar = memnew(ProgressBar); - download_progress_bar->set_h_size_flags(Control::SIZE_EXPAND_FILL); - download_progress_bar->set_v_size_flags(Control::SIZE_SHRINK_CENTER); - download_progress_bar->set_min(0); - download_progress_bar->set_max(1); - download_progress_bar->set_value(0); - download_progress_bar->set_step(0.01); - download_progress_bar->set_editor_preview_indeterminate(true); - download_progress_hb->add_child(download_progress_bar); - - download_progress_label = memnew(Label); - download_progress_label->set_h_size_flags(Control::SIZE_EXPAND_FILL); - download_progress_hb->add_child(download_progress_label); - - Button *download_cancel_button = memnew(Button); - download_cancel_button->set_text(TTR("Cancel")); - download_cancel_button->set_tooltip_text(TTR("Cancel the download of the templates.")); - download_progress_hb->add_child(download_cancel_button); - download_cancel_button->connect(SceneStringName(pressed), callable_mp(this, &ExportTemplateManager::_cancel_template_download)); - - download_templates = memnew(HTTPRequest); - install_templates_hb->add_child(download_templates); - download_templates->connect("request_completed", callable_mp(this, &ExportTemplateManager::_download_template_completed)); - - main_vb->add_child(memnew(HSeparator)); - - // Other installed templates table. - HBoxContainer *installed_versions_hb = memnew(HBoxContainer); - main_vb->add_child(installed_versions_hb); - Label *installed_label = memnew(Label); - installed_label->set_theme_type_variation("HeaderSmall"); - installed_label->set_text(TTR("Other Installed Versions:")); - installed_versions_hb->add_child(installed_label); - - MarginContainer *mc = memnew(MarginContainer); - mc->set_theme_type_variation("NoBorderHorizontalWindow"); - mc->set_v_size_flags(Control::SIZE_EXPAND_FILL); - main_vb->add_child(mc); - - installed_table = memnew(Tree); - installed_table->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED); - installed_table->set_hide_root(true); - installed_table->set_scroll_hint_mode(Tree::SCROLL_HINT_MODE_BOTH); - installed_table->set_custom_minimum_size(Size2(0, 100) * EDSCALE); - installed_table->set_v_size_flags(Control::SIZE_EXPAND_FILL); - mc->add_child(installed_table); - installed_table->connect("button_clicked", callable_mp(this, &ExportTemplateManager::_installed_table_button_cbk)); - - // Dialogs. - uninstall_confirm = memnew(ConfirmationDialog); - uninstall_confirm->set_title(TTR("Uninstall Template")); - add_child(uninstall_confirm); - uninstall_confirm->connect(SceneStringName(confirmed), callable_mp(this, &ExportTemplateManager::_uninstall_template_confirmed)); - - install_file_dialog = memnew(EditorFileDialog); - install_file_dialog->set_title(TTR("Select Template File")); - install_file_dialog->set_access(FileDialog::ACCESS_FILESYSTEM); - install_file_dialog->set_file_mode(FileDialog::FILE_MODE_OPEN_FILE); - install_file_dialog->set_current_dir(EDITOR_DEF("_export_template_download_directory", "")); - install_file_dialog->add_filter("*.tpz", TTR("Godot Export Templates")); - install_file_dialog->connect("file_selected", callable_mp(this, &ExportTemplateManager::_install_file_selected).bind(false)); - add_child(install_file_dialog); - - hide_dialog_accept = memnew(AcceptDialog); - hide_dialog_accept->set_text(TTR("The templates will continue to download.\nYou may experience a short editor freeze when they finish.")); - add_child(hide_dialog_accept); - hide_dialog_accept->connect(SceneStringName(confirmed), callable_mp(this, &ExportTemplateManager::_hide_dialog)); +Error TemplateDownloader::download_template(const String &p_file_name, const String &p_source) { + url = p_source; + filename = p_file_name; + + current_step = Step::QUERYING; + return request(p_source, PackedStringArray(), HTTPClient::METHOD_HEAD); +} + +void TemplateDownloader::cancel_download() { + cancel_request(); + + current_step = Step::WAITING; + filename = String(); + url = String(); + file_size = 0; + file_info = FileInfo(); +} + +float TemplateDownloader::get_download_progress() const { + if (current_step == Step::DOWNLOADING) { + return (float)get_downloaded_bytes() / get_body_size(); + } + return 0.0f; } diff --git a/editor/export/export_template_manager.h b/editor/export/export_template_manager.h index 31668fc332c2..a9fe0a8ca64d 100644 --- a/editor/export/export_template_manager.h +++ b/editor/export/export_template_manager.h @@ -31,91 +31,242 @@ #pragma once #include "scene/gui/dialogs.h" +#include "scene/main/http_request.h" +class Button; class EditorExportPreset; -class ExportTemplateVersion; -class FileDialog; -class HTTPRequest; -class MenuButton; +class ItemList; +class HBoxContainer; class OptionButton; -class ProgressBar; +class Texture2D; class Tree; +class TreeItem; + +class TemplateDownloader : public HTTPRequest { + GDCLASS(TemplateDownloader, HTTPRequest); + + struct FileInfo { + int offset = 0; + int compressed_size = 0; + int uncompressed_size = 0; + int method = 0; + PackedByteArray raw_record; + String name; + }; + + enum class Step { + WAITING, + QUERYING, + SCANNING, + DOWNLOADING, + }; + + String url; + String filename; + String target_directory; + + Step current_step = Step::WAITING; + int file_size = 0; + FileInfo file_info; + + int _find_sequence_backwards(const PackedByteArray &p_source, const PackedByteArray &p_target) const; + String _get_download_error(int p_result, int p_response_code) const; + + void _request_completed(int p_result, int p_response_code, const PackedStringArray &p_headers, const PackedByteArray &p_body); + void _download_failed(const String &p_reason); + +protected: + void _notification(int p_what); + static void _bind_methods(); + +public: + Error download_template(const String &p_file_name, const String &p_source); + void cancel_download(); + + bool is_downloading() const { return current_step != Step::WAITING; } + float get_download_progress() const; + + TemplateDownloader(const String &p_template_directory) : + target_directory(p_template_directory) {} +}; class ExportTemplateManager : public AcceptDialog { GDCLASS(ExportTemplateManager, AcceptDialog); - bool current_version_exists = false; - bool mirrors_available = false; - bool is_refreshing_mirrors = false; - bool is_downloading_templates = false; - float update_countdown = 0; + const StringName PATH_META = "path"; + const StringName FILE_META = "file"; - Label *current_value = nullptr; - Label *current_missing_label = nullptr; - Label *current_installed_label = nullptr; + enum class TemplateID { + WINDOWS_X86_32, + WINDOWS_X86_64, + WINDOWS_ARM64, - HBoxContainer *current_installed_hb = nullptr; - LineEdit *current_installed_path = nullptr; - Button *current_uninstall_button = nullptr; + LINUX_X86_32, + LINUX_X86_64, + LINUX_ARM32, + LINUX_ARM64, - VBoxContainer *install_options_vb = nullptr; - OptionButton *mirrors_list = nullptr; + MACOS, + + WEB, + WEB_EXTENSIONS, + WEB_NOTHREADS, + WEB_EXTENSIONS_NOTHREADS, + + ANDROID, + ANDROID_SOURCE, - enum MirrorAction { - VISIT_WEB_MIRROR, - COPY_MIRROR_URL, + IOS, + + ICU_DATA, + }; + + enum class PlatformID { + WINDOWS, + LINUX, + MACOS, + WEB, + ANDROID, + IOS, + COMMON, + }; + + enum class DownloadStatus { + NONE, + PENDING, + IN_PROGRESS, + COMPLETED, + FAILED, + }; + + enum class ButtonID { + DOWNLOAD, + REPAIR, + REMOVE, + CANCEL, + FAIL, + NONE, + }; + + struct PlatformInfo { + String name; + Ref icon; + HashSet templates; + String group; + }; + + struct TemplateInfo { + String name; + String description; + PackedStringArray file_list; }; - MenuButton *mirror_options_button = nullptr; - HBoxContainer *enable_online_hb = nullptr; - HBoxContainer *download_progress_hb = nullptr; - ProgressBar *download_progress_bar = nullptr; - Label *download_progress_label = nullptr; - HTTPRequest *download_templates = nullptr; - Button *install_file_button = nullptr; - Button *download_current_button = nullptr; - HTTPRequest *request_mirrors = nullptr; - - enum TemplatesAction { - OPEN_TEMPLATE_FOLDER, - UNINSTALL_TEMPLATE, + struct FileMetadata { + DownloadStatus download_status = DownloadStatus::NONE; + TemplateDownloader *downloader = nullptr; + String fail_reason; + float progress_cache = 0.0; + bool is_missing = false; }; - Tree *installed_table = nullptr; + bool mirrors_empty = true; + + HashMap platform_map; + HashMap template_data; + + HTTPRequest *mirrors_requester = nullptr; + LocalVector downloaders; - ConfirmationDialog *uninstall_confirm = nullptr; - String uninstall_version; - FileDialog *install_file_dialog = nullptr; - AcceptDialog *hide_dialog_accept = nullptr; + bool download_all_enabled = true; + HashSet queued_templates; + HashSet queued_files; + int download_count = 0; + mutable HashMap file_metadata; + LocalVector downloading_items; + bool queue_update_pending = false; + TreeItem *item_to_delete = nullptr; - void _update_template_status(); + HashMap checked_cache; + HashMap folding_cache; + + OptionButton *mirrors_list = nullptr; + Button *open_mirror = nullptr; + ItemList *version_list = nullptr; + Tree *installed_templates_tree = nullptr; + Tree *available_templates_tree = nullptr; + Button *open_folder_button = nullptr; + Button *install_button = nullptr; + HBoxContainer *offline_container = nullptr; + ConfirmationDialog *confirm_delete = nullptr; - void _download_current(); - void _download_template(const String &p_url, bool p_skip_check = false); - void _download_template_completed(int p_status, int p_code, const PackedStringArray &headers, const PackedByteArray &p_data); - void _cancel_template_download(); - void _refresh_mirrors(); - void _refresh_mirrors_completed(int p_status, int p_code, const PackedStringArray &headers, const PackedByteArray &p_data); + void _request_mirrors(); + void _mirrors_request_completed(int p_result, int p_response_code, const PackedStringArray &p_headers, const PackedByteArray &p_body); + void _set_empty_mirror_list(); + String _get_current_mirror_url() const; + void _update_online_mode(); + bool _is_online() const; void _force_online_mode(); + void _open_mirror(); + void _delete_confirmed(); - bool _humanize_http_status(HTTPRequest *p_request, String *r_status, int *r_downloaded_bytes, int *r_total_bytes); - void _set_current_progress_status(const String &p_status, bool p_error = false); - void _set_current_progress_value(float p_value, const String &p_status); + void _initialize_template_data(); + void _update_template_tree(); + void _update_template_tree_theme(Tree *p_tree); + void _fill_template_tree(Tree *p_tree, const HashMap> &p_installed_template_files, bool p_is_current_version); + void _update_template_tree_with_folding(); + void _update_install_button(); + bool _can_download_templates(); - void _install_file(); - bool _install_file_selected(const String &p_file, bool p_skip_progress = false); + void _update_folding_cache(TreeItem *p_item); - void _uninstall_template(const String &p_version); - void _uninstall_template_confirmed(); + String _get_template_folder_path(const String &p_version) const; + Ref _get_platform_icon(const String &p_platform_name); - String _get_selected_mirror() const; - void _mirror_options_button_cbk(int p_id); - void _installed_table_button_cbk(Object *p_item, int p_column, int p_id, MouseButton p_button); + void _version_selected(); + void _tree_button_clicked(TreeItem *p_item, int p_column, int p_id, MouseButton p_button); + void _tree_item_edited(); + void _install_templates(TreeItem *p_files = nullptr); + void _open_template_directory(); - void _open_template_folder(const String &p_version); + void _queue_download_tree_item(TreeItem *p_item); + void _process_download_queue(); + void _queue_process_download_queue(); + TemplateDownloader *_get_available_downloader(int *r_from_index); + void _download_request_completed(const String &p_filename); + void _download_request_failed(const String &p_filename, const String &p_reason); + bool _is_template_download_finished(TreeItem *p_template); - virtual void ok_pressed() override; - void _hide_dialog(); + void _set_item_type(TreeItem *p_item, int p_type); + void _setup_item_text(TreeItem *p_item, const String &p_text); + FileMetadata *_get_file_metadata(const String &p_text) const; + FileMetadata *_get_file_metadata(const TreeItem *p_item) const; + void _apply_item_folding(TreeItem *p_item, bool p_default = false); + void _cancel_item_download(TreeItem *p_item); + void _item_download_failed(TreeItem *p_item, const String &p_reason); + void _add_fail_reason_button(TreeItem *p_item, const String &p_filename = String()); + + String _get_item_path(TreeItem *p_item) const; + bool _item_is_file(TreeItem *p_item) const; + bool _status_is_finished(DownloadStatus p_status) { return p_status == DownloadStatus::COMPLETED || p_status == DownloadStatus::FAILED; } + float _get_download_progress(const TreeItem *p_item) const; + void _draw_item_progress(TreeItem *p_item, const Rect2 &p_rect); + + struct ThemeCache { + Ref install_icon; + Ref remove_icon; + Ref repair_icon; + Ref failure_icon; + Ref cancel_icon; + Ref progress_icons[8]; + + Color current_version_color; + Color incomplete_template_color; + Color missing_file_color; + Color download_progress_color; + Color download_failed_color; + + int icon_width = 0; + } theme_cache; protected: void _notification(int p_what); @@ -128,11 +279,10 @@ class ExportTemplateManager : public AcceptDialog { bool is_android_template_installed(const Ref &p_preset); bool can_install_android_template(const Ref &p_preset); Error install_android_template(const Ref &p_preset); - Error install_android_template_from_file(const String &p_file, const Ref &p_preset); void popup_manager(); - bool is_downloading() const { return is_downloading_templates; } + bool is_downloading() const; void stop_download(); ExportTemplateManager(); diff --git a/scene/gui/tree.cpp b/scene/gui/tree.cpp index 3609a4cc98f0..3b524c1db700 100644 --- a/scene/gui/tree.cpp +++ b/scene/gui/tree.cpp @@ -1461,6 +1461,10 @@ Color TreeItem::get_button_color(int p_column, int p_index) const { void TreeItem::set_button_tooltip_text(int p_column, int p_index, const String &p_tooltip) { ERR_FAIL_INDEX(p_column, cells.size()); + if (p_index < 0) { + p_index += cells[p_column].buttons.size(); + } + ERR_FAIL_INDEX(p_index, cells[p_column].buttons.size()); cells.write[p_column].buttons.write[p_index].tooltip = p_tooltip;