From 64c89529752bb4756d7e7273f52a4bd72c87602e Mon Sep 17 00:00:00 2001 From: kobewi Date: Tue, 3 Mar 2026 22:35:49 +0100 Subject: [PATCH] Rework export template dialog to allow individual templates --- editor/export/export_template_manager.cpp | 2005 ++++++++++++--------- editor/export/export_template_manager.h | 231 ++- scene/gui/tree.cpp | 4 + 3 files changed, 1308 insertions(+), 932 deletions(-) diff --git a/editor/export/export_template_manager.cpp b/editor/export/export_template_manager.cpp index e523a2ffd1..17feb924e8 100644 --- a/editor/export/export_template_manager.cpp +++ b/editor/export/export_template_manager.cpp @@ -30,768 +30,1152 @@ #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/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 - return DOWNLOADS_NOT_AVAILABLE_FOR_DOUBLE_BUILDS; -#else - - const int network_mode = EDITOR_GET("network/connection/network_mode"); - - if (network_mode == EditorSettings::NETWORK_OFFLINE) { - return DOWNLOADS_NOT_AVAILABLE_IN_OFFLINE_MODE; - } - - return DOWNLOADS_AVAILABLE; + } else if (true) { + _set_empty_mirror_list(); + mirrors_list->set_tooltip_text(TTRC("Official export templates aren't available for double-precision builds.")); #endif -} - -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(); - } - } - da->list_dir_end(); - - // Update the state of the current version. - String current_version = GODOT_VERSION_FULL_CONFIG; - current_value->set_text(current_version); - - if (templates.has(current_version)) { - current_missing_label->hide(); - current_installed_label->show(); - - current_installed_hb->show(); - current_version_exists = true; + } else if (!_is_online()) { + mirrors_list->set_tooltip_text(TTRC("Template downloading is disabled in offline mode.")); } else { - current_installed_label->hide(); - current_missing_label->show(); - - current_installed_hb->hide(); - current_version_exists = false; + mirrors_list->set_tooltip_text(String()); } + const String mirrors_metadata_url = vformat("https://godotengine.org/mirrorlist/%s.json", "4.6.stable" /*GODOT_VERSION_FULL_CONFIG*/); // TODO: debug-adjusted, uncomment before merging xd + mirrors_requester->request(mirrors_metadata_url); +} - if (is_downloading_templates) { - install_options_vb->hide(); - download_progress_hb->show(); - } else { - download_progress_hb->hide(); - install_options_vb->show(); +void ExportTemplateManager::_mirrors_request_completed(int p_result, int p_response_code, const PackedStringArray &p_headers, const PackedByteArray &p_body) { + mirrors_list->clear(); - if (templates.has(current_version)) { - current_installed_path->set_text(templates_dir.path_join(current_version)); + 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); } - } - - // Update the list of other installed versions. - installed_table->clear(); - TreeItem *installed_root = installed_table->create_item(); - - for (RBSet::Element *E = templates.back(); E; E = E->prev()) { - String version_string = E->get(); - if (version_string == current_version) { - continue; - } - - TreeItem *ti = installed_table->create_item(installed_root); - ti->set_text(0, version_string); - -#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.")); - } -} - -void ExportTemplateManager::_download_current() { - if (is_downloading_templates) { - return; - } - is_downloading_templates = true; - - install_options_vb->hide(); - download_progress_hb->show(); - - 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; - } - - _download_template(mirror_url, true); - } else if (!is_refreshing_mirrors) { - _set_current_progress_status(TTR("Retrieving the mirror list...")); - _refresh_mirrors(); - } -} - -void ExportTemplateManager::_download_template(const String &p_url, bool p_skip_check) { - if (!p_skip_check && is_downloading_templates) { - return; - } - is_downloading_templates = true; - - install_options_vb->hide(); - download_progress_hb->show(); - download_progress_bar->show(); - download_progress_bar->set_indeterminate(true); - - _set_current_progress_status(TTR("Starting the download...")); - - download_templates->set_download_file(EditorPaths::get_singleton()->get_cache_dir().path_join("tmp_templates.tpz")); - download_templates->set_use_threads(true); - - 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); - - 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(); + EditorNode::get_singleton()->show_warning(error); + _set_empty_mirror_list(); return; } - set_process(true); - _set_current_progress_status(TTR("Connecting to the mirror...")); - - ProgressIndicator *indicator = EditorNode::get_bottom_panel()->get_progress_indicator(); - indicator->set_tooltip_text(TTRC("Downloading export templates...")); - indicator->set_value(0); - indicator->show(); -} - -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)); - } - } - } break; - } - - EditorNode::get_bottom_panel()->get_progress_indicator()->hide(); - set_process(false); -} - -void ExportTemplateManager::_cancel_template_download() { - if (!is_downloading_templates) { - return; - } - - 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::_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; - } - - String response_json = String::utf8((const char *)p_data.ptr(), p_data.size()); + String response_json = String::utf8((const char *)p_body.ptr(), p_body.size()); 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(); - } + _set_empty_mirror_list(); return; } - mirrors_list->clear(); - mirrors_list->add_item(TTR("Best available mirror"), 0); - - mirrors_available = false; + bool mirrors_available = false; Dictionary mirror_data = json.get_data(); if (mirror_data.has("mirrors")) { Array mirrors = mirror_data["mirrors"]; - - for (int i = 0; i < mirrors.size(); i++) { - Dictionary m = mirrors[i]; + mirrors.push_front(Dictionary({ { "name", "localhost8k" }, { "url", "http://127.0.0.1:8000" } })); // TODO: debug-only, remove before merging xd + for (const Variant &mirror : mirrors) { + Dictionary m = mirror; ERR_CONTINUE(!m.has("url") || !m.has("name")); mirrors_list->add_item(m["name"]); - mirrors_list->set_item_metadata(i + 1, m["url"]); + mirrors_list->set_item_metadata(-1, m["url"]); mirrors_available = true; } + // Hard-coded for translation. Should match the up-to-date list of mirrors. + // TTR("Official Releases mirror") } 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(); + _set_empty_mirror_list(); + } else { + 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(); } } +} - is_refreshing_mirrors = false; +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(); +} - 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; - } +String ExportTemplateManager::_get_current_mirror_url() const { + return mirrors_list->get_item_metadata(mirrors_list->get_selected()); +} - _download_template(mirror_url, true); +void ExportTemplateManager::_update_online_mode() { + offline_container->set_visible((int)EDITOR_GET("network/connection/network_mode") == EditorSettings::NETWORK_OFFLINE); + + if (_is_online()) { + _update_install_button(); + } else { + mirrors_list->clear(); + _set_empty_mirror_list(); } } +bool ExportTemplateManager::_is_online() const { + return !offline_container->is_visible(); +} + 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(); - popup_manager(); + _update_online_mode(); + _request_mirrors(); } -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; +void ExportTemplateManager::_open_mirror() { + OS::get_singleton()->shell_open(_get_current_mirror_url()); +} - 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(); +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 ARM-64"; + info.description = TTRC("32-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; + } - 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()); + { + 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 ARM-32"; + 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 ARM-64"; + 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; + } + + { + TemplateInfo info; + info.name = "macOS"; + info.description = TTRC("Universal build for macOS."); + info.file_list = { "macos.zip" }; + template_data[TemplateID::MACOS] = info; + } + + { + 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; + } + + { + 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; + } + + { + TemplateInfo info; + info.name = "iOS"; + info.description = TTRC("Build for Apple's iOS."); + info.file_list = { "ios.zip" }; + template_data[TemplateID::IOS] = info; + } + + { + 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; + } + + // 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; + } + + // 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::_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; - 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; + } } - return success; + _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::_set_current_progress_status(const String &p_status, bool p_error) { - download_progress_label->set_text(p_status); +void ExportTemplateManager::_update_template_tree_theme(Tree *p_tree) { + if (is_downloading()) { + // Prevents hiding progress bar. + Ref empty_style; + empty_style.instantiate(); - 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))); + 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 { - download_progress_label->add_theme_color_override(SceneStringName(font_color), get_theme_color(SceneStringName(font_color), SNAME("Label"))); + 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")); } } -void ExportTemplateManager::_set_current_progress_value(float p_value, const String &p_status) { - if (!is_visible()) { +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; + + if (p_tree->get_root()) { + _update_folding_cache(p_tree->get_root()); + p_tree->clear(); + } + + TreeItem *platform_parent = p_tree->create_item(); + _setup_item_text(platform_parent, String()); + + 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; } - 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(); -} + String current_group; + for (const KeyValue &KV : platform_map) { + const PlatformInfo &template_platform = KV.value; -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; - } - int ret = unzGoToFirstFile(pkg); - - // Count them and find version. - int fc = 0; - String version; - String contents_dir; - - 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; - } - - 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; - } - - if (file.ends_with("version.txt")) { - Vector uncomp_data; - uncomp_data.resize(info.uncompressed_size); - - // 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); - - String data_str = String::utf8((const char *)uncomp_data.ptr(), uncomp_data.size()); - data_str = data_str.strip_edges(); - - // 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; + 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; } - version = data_str; - contents_dir = file.get_base_dir().trim_suffix("/").trim_suffix("\\"); - } - - if (file.get_file().size() != 0) { - fc++; - } - - ret = unzGoToNextFile(pkg); - } - - if (version.is_empty()) { - EditorNode::get_singleton()->show_warning(TTR("No version.txt found inside the export templates file.")); - unzClose(pkg); - return false; - } - - 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; - } - - EditorProgress *p = nullptr; - if (!p_skip_progress) { - p = memnew(EditorProgress("ltask", TTR("Extracting Export Templates"), fc)); - } - - 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; - } - - if (String::utf8(fname).ends_with("/")) { - // File is a directory, ignore it. - // Directories will be created when extracting each file. - ret = unzGoToNextFile(pkg); - continue; - } - - String file_path(String::utf8(fname).simplify_path()); - - String file = file_path.get_file(); - - // Skip the __MACOSX directory created by macOS's built-in file zipper. - if (file.is_empty() || file.begins_with("__MACOSX")) { - ret = unzGoToNextFile(pkg); - continue; - } - - Vector uncomp_data; - uncomp_data.resize(info.uncompressed_size); - - // 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); - - String base_dir = file_path.get_base_dir().trim_suffix("/"); - - 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); - - Ref da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); - ERR_CONTINUE(da.is_null()); - - String output_dir = template_path.path_join(base_dir); - - if (!DirAccess::exists(output_dir)) { - Error mkdir_err = da->make_dir_recursive(output_dir); - ERR_CONTINUE(mkdir_err != OK); + if (any_installed && !all_installed) { + // Not going to change anymore. + break; } } - if (p) { - p->step(TTR("Importing:") + " " + file, fc); + if ((is_available_tree && all_installed) || (is_installed_tree && !any_installed)) { + continue; } - String to_write = template_path.path_join(file); - Ref f = FileAccess::open(to_write, FileAccess::WRITE); + 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; - if (f.is_null()) { - ret = unzGoToNextFile(pkg); - fc++; - ERR_CONTINUE_MSG(true, "Can't open file from path '" + String(to_write) + "'."); + 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); + } } - 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 + 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); - ret = unzGoToNextFile(pkg); - fc++; + for (TemplateID id : template_platform.templates) { + TemplateInfo &template_info = template_data[id]; + + 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; + } + + const LocalVector &installed_files = is_template_installed ? p_installed_template_files[id] : empty_vector; + + 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(); + } + + 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 any_missing = false; + bool any_failed = false; + for (const String &file : template_info.file_list) { + FileMetadata *meta = _get_file_metadata(file); + + TreeItem *file_item = template_item->create_child(); + file_item->set_meta(FILE_META, true); + + 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; + } + } + } + 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); } - if (p) { - memdelete(p); + 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))); } - unzClose(pkg); +} - _update_template_status(); - EditorSettings::get_singleton()->set("_export_template_download_directory", p_file.get_base_dir()); +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; + } + + 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 { + install_button->set_text(TTRC("Install Selected Templates")); + } + + 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()); + } +} + +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; +} + +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; + } + } + for (TreeItem *child = p_item->get_first_child(); child; child = child->get_next()) { + _update_folding_cache(child); + } +} + +String ExportTemplateManager::_get_template_folder_path(const String &p_version) const { + return EditorPaths::get_singleton()->get_export_templates_dir().path_join(p_version); +} + +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(); +} + +void ExportTemplateManager::_version_selected() { + if (!is_downloading()) { + file_metadata.clear(); + _update_template_tree(); + } + _update_install_button(); +} + +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; + + case ButtonID::REPAIR: { + p_item->set_collapsed(false); + _install_templates(p_item); + } break; + + case ButtonID::REMOVE: { + 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(p_item)) { + OS::get_singleton()->move_to_trash(template_directory.path_join(p_item->get_text(0))); + file_metadata.erase(p_item->get_text(0)); + } else { + for (TreeItem *child = p_item->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(); + } break; + + 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; + + case ButtonID::FAIL: { + FileMetadata *meta = _get_file_metadata(p_item); + EditorNode::get_singleton()->show_warning(meta->fail_reason + ".", TTR("Download Failed")); + } break; + + case ButtonID::NONE: { + } break; + } +} + +void ExportTemplateManager::_tree_item_edited() { + TreeItem *edited = available_templates_tree->get_edited(); + ERR_FAIL_NULL(edited); + + edited->propagate_check(0, false); + _update_install_button(); +} + +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(); + + 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); + + ProgressIndicator *indicator = EditorNode::get_bottom_panel()->get_progress_indicator(); + indicator->set_tooltip_text(TTRC("Downloading export templates...")); + indicator->set_value(0); + indicator->show(); +} + +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); +} + +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); + } + + 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); + } + } +} + +void ExportTemplateManager::_process_download_queue() { + queue_update_pending = false; + + int downloader_index = 0; + bool is_finished = true; + for (TreeItem *item : downloading_items) { + FileMetadata *meta = _get_file_metadata(item); + + is_finished = is_finished && _status_is_finished(meta->download_status); + if (meta->download_status != DownloadStatus::PENDING) { + continue; + } + + HTTPRequest *downloader = _get_available_downloader(&downloader_index); + if (!downloader) { + break; + } + downloader_index++; + + const String filename = item->get_text(0); + downloader->set_download_file(EditorPaths::get_singleton()->get_cache_dir().path_join(filename)); + + Error err = downloader->request(_get_current_mirror_url() + "/" + filename); + if (err == OK) { + meta->download_status = DownloadStatus::IN_PROGRESS; + meta->downloader = downloader; + } else { + _item_download_failed(item, TTR(error_names[err])); + } + } + + 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); + } +} + +void ExportTemplateManager::_queue_process_download_queue() { + if (queue_update_pending) { + return; + } + callable_mp(this, &ExportTemplateManager::_process_download_queue).call_deferred(); + queue_update_pending = true; +} + +HTTPRequest *ExportTemplateManager::_get_available_downloader(int *r_from_index) { + int counter = -1; + for (HTTPRequest *downloader : downloaders) { + counter++; + if (counter < *r_from_index) { + continue; + } + if (downloader->get_http_client_status() == HTTPClient::STATUS_DISCONNECTED) { + *r_from_index = counter; + return downloader; + } + } + return nullptr; +} + +void ExportTemplateManager::_download_request_completed(int p_result, int p_response_code, const PackedStringArray &p_headers, const PackedByteArray &p_body, HTTPRequest *p_downloader) { + const String filename = p_downloader->get_download_file().get_file(); + bool found = false; + bool template_finished = false; + + queued_files.erase(filename); + for (TreeItem *item : downloading_items) { + if (item->get_text(0) != filename) { + continue; + } + + FileMetadata *meta = _get_file_metadata(filename); + meta->downloader = nullptr; + + if (p_result == HTTPRequest::RESULT_SUCCESS && p_response_code == HTTPClient::RESPONSE_OK) { + DirAccess::rename_absolute(p_downloader->get_download_file(), _get_template_folder_path(VERSION_FULL_CONFIG).path_join(filename)); + + item->clear_buttons(); + meta->download_status = DownloadStatus::COMPLETED; + meta->is_missing = false; + } else { + _item_download_failed(item, _get_download_error(p_result, p_response_code)); + } + + 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(); + + if (template_finished) { + _update_template_tree(); + } +} + +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; } -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::_uninstall_template_confirmed() { - 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 + "'."); - err = da->change_dir(uninstall_version); - ERR_FAIL_COND_MSG(err != OK, "Could not access templates directory at '" + templates_dir.path_join(uninstall_version) + "'."); - - err = da->erase_contents_recursive(); - ERR_FAIL_COND_MSG(err != OK, "Could not remove all templates in '" + templates_dir.path_join(uninstall_version) + "'."); - - 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) + "'."); - - _update_template_status(); -} - -String ExportTemplateManager::_get_selected_mirror() const { - if (mirrors_list->get_item_count() == 1) { - return ""; +String ExportTemplateManager::_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"); } - 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; - } - - 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; - } - - OS::get_singleton()->shell_open(mirror_url); - } break; - - 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; - } - - DisplayServer::get_singleton()->clipboard_set(mirror_url); - } break; + 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 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; - } - - switch (p_id) { - case OPEN_TEMPLATE_FOLDER: { - String version_string = ti->get_text(0); - _open_template_folder(version_string); - } break; - - case UNINSTALL_TEMPLATE: { - String version_string = ti->get_text(0); - _uninstall_template(version_string); - } 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::popup_manager() { - _update_template_status(); - - switch (_get_downloads_availability()) { - case DOWNLOADS_AVAILABLE: { - current_missing_label->set_text(TTR("Export templates are missing. Download them or install from a file.")); - - mirrors_list->clear(); - mirrors_list->add_item(TTR("Best available mirror"), 0); - mirrors_list->set_disabled(false); - mirrors_list->set_tooltip_text(""); - - mirror_options_button->set_disabled(false); - - download_current_button->set_disabled(false); - download_current_button->set_tooltip_text(""); - - if (!is_downloading_templates) { - _refresh_mirrors(); - } - - enable_online_hb->hide(); - } break; - - case DOWNLOADS_NOT_AVAILABLE_IN_OFFLINE_MODE: { - current_missing_label->set_text(TTR("Export templates are missing. Install them from a file.")); - - 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.")); - - mirror_options_button->set_disabled(true); - - download_current_button->set_disabled(true); - download_current_button->set_tooltip_text(TTR("Template downloading is disabled in offline mode.")); - - enable_online_hb->show(); - } 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.")); - - mirror_options_button->set_disabled(true); - - download_current_button->set_disabled(true); - download_current_button->set_tooltip_text(TTR("Official export templates aren't available for development builds.")); - - enable_online_hb->hide(); - } break; - - case DOWNLOADS_NOT_AVAILABLE_FOR_DOUBLE_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 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.")); - - mirror_options_button->set_disabled(true); - - download_current_button->set_disabled(true); - download_current_button->set_tooltip_text(TTR("Official export templates aren't available for double-precision builds.")); +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); } } - - popup_centered(Size2(720, 280) * EDSCALE); } -void ExportTemplateManager::stop_download() { - download_templates->cancel_request(); - is_downloading_templates = false; +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)); + + FileMetadata *meta = _get_file_metadata(p_item); + if (meta->downloader) { + meta->downloader->cancel_request(); + meta->downloader = nullptr; + } } -void ExportTemplateManager::ok_pressed() { - if (!is_downloading_templates) { - hide(); +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; + + p_item->clear_buttons(); + _add_fail_reason_button(p_item); +} + +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)); +} + +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 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::_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); + } + } + } + } +} + +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; +} + +ExportTemplateManager::FileMetadata *ExportTemplateManager::_get_file_metadata(const TreeItem *p_item) const { + return _get_file_metadata(p_item->get_text(0)); +} + +String ExportTemplateManager::_get_item_path(TreeItem *p_item) const { + return p_item->get_meta(PATH_META, String()); +} + +bool ExportTemplateManager::_item_is_file(TreeItem *p_item) const { + return p_item->get_meta(FILE_META, false).operator bool(); +} + +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; + } + + case DownloadStatus::IN_PROGRESS: { + if (!meta->downloader) { + return 0.0; + } + return (float)meta->downloader->get_downloaded_bytes() / (float)meta->downloader->get_body_size(); + } + + case DownloadStatus::COMPLETED: { + return 1.0; + } + + case DownloadStatus::FAILED: { + return meta->progress_cache; + } + } + return 0.0; +} + +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)); + + if (!_item_is_file(p_item)) { + float progress = 0.0; + int item_count = 0; + + 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); + + 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; } - hide_dialog_accept->popup_centered(); + FileMetadata *meta = _get_file_metadata(p_item); + switch (meta->download_status) { + case DownloadStatus::NONE: { + } break; + + 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; + + 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; + + case DownloadStatus::COMPLETED: { + owning_tree->draw_rect(p_rect, theme_cache.download_progress_color); + } break; + + 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; + } } -void ExportTemplateManager::_hide_dialog() { - hide(); +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; + + 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; + + case NOTIFICATION_THEME_CHANGED: { + open_folder_button->set_button_icon(get_editor_theme_icon("Folder")); + install_button->set_button_icon(get_editor_theme_icon("AssetLib")); + open_mirror->set_button_icon(get_editor_theme_icon("ExternalLink")); + + theme_cache.install_icon = get_editor_theme_icon("AssetLib"); + 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)); + } + + 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); + + theme_cache.icon_width = get_theme_constant("class_icon_size", EditorStringName(Editor)); + } break; + + case NOTIFICATION_INTERNAL_PROCESS: { + available_templates_tree->queue_redraw(); + installed_templates_tree->queue_redraw(); + + 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); + } + } } String ExportTemplateManager::get_android_build_directory(const Ref &p_preset) { @@ -942,256 +1326,145 @@ 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); +} - mirror_options_button->set_button_icon(get_editor_theme_icon(SNAME("GuiTabMenuHl"))); - } break; +bool ExportTemplateManager::is_downloading() const { + return !queued_files.is_empty(); +} - case NOTIFICATION_PROCESS: { - update_countdown -= get_process_delta_time(); - if (update_countdown > 0) { - 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); - } - } else { - _set_current_progress_status(status); - } - - if (!success) { - EditorNode::get_bottom_panel()->get_progress_indicator()->hide(); - set_process(false); - } - } 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(); - } break; +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(TTR("Export Template Manager")); - set_hide_on_ok(false); - set_ok_button_text(TTR("Close")); + set_title(TTRC("Export Template Manager")); + set_ok_button_text(TTRC("Close")); VBoxContainer *main_vb = memnew(VBoxContainer); add_child(main_vb); - // Current version controls. - HBoxContainer *current_hb = memnew(HBoxContainer); - main_vb->add_child(current_hb); + HBoxContainer *download_header = memnew(HBoxContainer); + download_header->set_alignment(BoxContainer::ALIGNMENT_BEGIN); + main_vb->add_child(download_header); - 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 - - 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)); - - main_vb->add_child(memnew(HSeparator)); - - // Download and install section. - HBoxContainer *install_templates_hb = memnew(HBoxContainer); - main_vb->add_child(install_templates_hb); - - // 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); - - HBoxContainer *download_install_hb = memnew(HBoxContainer); - install_options_vb->add_child(download_install_hb); - - Label *mirrors_label = memnew(Label); - mirrors_label->set_text(TTR("Download from:")); - download_install_hb->add_child(mirrors_label); + download_header->add_child(memnew(Label(TTRC("Download from:")))); 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); + download_header->add_child(mirrors_list); - request_mirrors = memnew(HTTPRequest); - mirrors_list->add_child(request_mirrors); - request_mirrors->connect("request_completed", callable_mp(this, &ExportTemplateManager::_refresh_mirrors_completed)); + 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)); - 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)); + 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)); - download_install_hb->add_spacer(); + HSplitContainer *main_split = memnew(HSplitContainer); + main_split->set_v_size_flags(Control::SIZE_EXPAND_FILL); + main_vb->add_child(main_split); - 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)); + VBoxContainer *side_vb = memnew(VBoxContainer); + main_split->add_child(side_vb); - HBoxContainer *install_file_hb = memnew(HBoxContainer); - install_file_hb->set_alignment(BoxContainer::ALIGNMENT_END); - install_options_vb->add_child(install_file_hb); + Label *version_header = memnew(Label(TTRC("Godot Version"))); + version_header->set_theme_type_variation("HeaderSmall"); + side_vb->add_child(version_header); - 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)); + 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)); - enable_online_hb = memnew(HBoxContainer); - install_options_vb->add_child(enable_online_hb); + 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)); - 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); + 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_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->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)); - // 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(); + mirrors_requester = memnew(HTTPRequest); + mirrors_requester->connect("request_completed", callable_mp(this, &ExportTemplateManager::_mirrors_request_completed)); + add_child(mirrors_requester); - 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)); + for (int i = 0; i < 5; i++) { + HTTPRequest *downloader = memnew(HTTPRequest); + downloader->set_use_threads(true); + add_child(downloader); + downloaders.push_back(downloader); + downloader->connect("request_completed", callable_mp(this, &ExportTemplateManager::_download_request_completed).bind(downloader), CONNECT_DEFERRED); + } } diff --git a/editor/export/export_template_manager.h b/editor/export/export_template_manager.h index 31668fc332..e42fcfda20 100644 --- a/editor/export/export_template_manager.h +++ b/editor/export/export_template_manager.h @@ -32,90 +32,190 @@ #include "scene/gui/dialogs.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 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, + + MACOS, + + WEB, + WEB_EXTENSIONS, + WEB_NOTHREADS, + WEB_EXTENSIONS_NOTHREADS, + + ANDROID, + ANDROID_SOURCE, + + 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; + }; + + struct FileMetadata { + DownloadStatus download_status = DownloadStatus::NONE; + HTTPRequest *downloader = nullptr; + String fail_reason; + float progress_cache = 0.0; + bool is_missing = false; + }; + + bool mirrors_empty = true; + + HashMap platform_map; + HashMap template_data; + + HTTPRequest *mirrors_requester = nullptr; + LocalVector downloaders; + + 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; + + HashMap checked_cache; + HashMap folding_cache; - VBoxContainer *install_options_vb = nullptr; 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; - enum MirrorAction { - VISIT_WEB_MIRROR, - COPY_MIRROR_URL, - }; - - 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, - }; - - Tree *installed_table = nullptr; - - ConfirmationDialog *uninstall_confirm = nullptr; - String uninstall_version; - FileDialog *install_file_dialog = nullptr; - AcceptDialog *hide_dialog_accept = nullptr; - - void _update_template_status(); - - 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(); - 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(); + HTTPRequest *_get_available_downloader(int *r_from_index); + void _download_request_completed(int p_result, int p_response_code, const PackedStringArray &p_headers, const PackedByteArray &p_body, HTTPRequest *p_downloader); + bool _is_template_download_finished(TreeItem *p_template); + String _get_download_error(int p_result, int p_response_code) const; - 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 +228,10 @@ public: 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 3609a4cc98..3b524c1db7 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;