Files
godot/editor/export/export_template_manager.cpp
T
Kotob M 09c2419d2d Improve explanation for offline mode in export template manager
Makes info text yellow instead of white, fixes tooltip on button not showing the correct explanation, and also centers the Go Online button.
2026-05-10 23:56:54 +03:00

2002 lines
70 KiB
C++

/**************************************************************************/
/* export_template_manager.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#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/version.h"
#include "editor/editor_node.h"
#include "editor/editor_string_names.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/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/option_button.h"
#include "scene/gui/split_container.h"
#include "scene/gui/tree.h"
#include "scene/resources/texture.h"
#include "servers/display/display_server.h"
#include "servers/rendering/rendering_server.h"
void ExportTemplateManager::_request_mirrors() {
mirrors_list->clear();
mirrors_empty = true;
_update_install_button();
// 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 (!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());
}
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);
}
}
void ExportTemplateManager::_mirrors_request_completed(int p_result, int p_response_code, const PackedStringArray &p_headers, const PackedByteArray &p_body) {
mirrors_list->clear();
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;
}
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!"));
_set_empty_mirror_list();
return;
}
bool mirrors_available = false;
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"));
mirrors_list->add_item(m["name"]);
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) {
_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();
}
}
}
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();
}
String ExportTemplateManager::_get_current_mirror_url() const {
return mirrors_list->get_item_metadata(mirrors_list->get_selected());
}
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();
_update_online_mode();
_request_mirrors();
}
void ExportTemplateManager::_open_mirror() {
OS::get_singleton()->shell_open(_get_current_mirror_url());
}
void ExportTemplateManager::_delete_all() {
item_to_delete = installed_templates_tree->get_root();
confirm_delete->set_text(TTRC("Remove all installed template files? (Cannot be undone.)\nDepending on your filesystem configuration, the files will either be moved to the system trash or deleted permanently."));
confirm_delete->popup_centered();
}
void ExportTemplateManager::_delete_confirmed() {
_delete_file(item_to_delete);
const String selected_version = version_list->get_item_text(version_list->get_current());
if (selected_version != GODOT_VERSION_FULL_CONFIG) {
// Deleting all installed templates removes the version from list.
_update_version_list();
}
_update_template_tree();
item_to_delete = nullptr;
}
void ExportTemplateManager::_delete_file(const TreeItem *p_item) {
if (_item_is_file(p_item)) {
const String selected_version = version_list->get_item_text(version_list->get_current());
const String full_path = _get_template_folder_path(selected_version).path_join(p_item->get_text(0));
if (FileAccess::exists(full_path)) {
OS::get_singleton()->move_to_trash(full_path);
}
file_metadata.erase(p_item->get_text(0));
} else {
for (TreeItem *child = p_item->get_first_child(); child; child = child->get_next()) {
_delete_file(child);
}
}
}
void ExportTemplateManager::_tpz_file_selected(const String &p_file) {
Ref<FileAccess> 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;
}
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<uint8_t> 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;
}
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;
}
Ref<DirAccess> 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;
}
EditorProgress ep("export_tpz", 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<uint8_t> 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<DirAccess> 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);
}
}
ep.step(TTR("Importing:") + " " + file, fc);
String to_write = template_path.path_join(file);
Ref<FileAccess> f = FileAccess::open(to_write, FileAccess::WRITE);
if (f.is_null()) {
ret = unzGoToNextFile(pkg);
fc++;
ERR_CONTINUE_MSG(true, "Can't open file from path '" + String(to_write) + "'.");
}
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
ret = unzGoToNextFile(pkg);
fc++;
}
unzClose(pkg);
EditorSettings::get_singleton()->set("_export_template_download_directory", p_file.get_base_dir());
const String selected_version = version_list->get_item_text(version_list->get_current());
if (selected_version == version) {
_update_template_tree();
}
}
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;
}
{
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("Android APK template and source for Gradle builds.");
info.file_list = { "android_debug.apk", "android_release.apk", "android_source.zip" };
template_data[TemplateID::ANDROID] = 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 };
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(GODOT_VERSION_FULL_CONFIG));
_update_version_list();
}
void ExportTemplateManager::_update_version_list() {
String current_selected;
if (version_list->get_current() > -1) {
current_selected = version_list->get_item_text(version_list->get_current());
}
version_list->clear();
Ref<DirAccess> templates_dir = DirAccess::open(EditorPaths::get_singleton()->get_export_templates_dir());
ERR_FAIL_COND(templates_dir.is_null());
int current_version_id = -1;
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);
current_version_id = version_list->get_item_count() - 1;
} else {
bool recognized = false;
for (const String &file : DirAccess::get_files_at(templates_dir->get_current_dir().path_join(dir))) {
for (const KeyValue<TemplateID, TemplateInfo> &KV : template_data) {
if (KV.value.file_list.has(file)) {
recognized = true;
break;
}
}
if (recognized) {
break;
}
}
if (!recognized) {
continue;
}
version_list->add_item(dir);
if (dir == current_selected) {
version_list->select(version_list->get_item_count() - 1);
}
}
version_list->set_item_metadata(-1, dir);
}
if (version_list->get_current() == -1) {
version_list->select(current_version_id);
}
}
void ExportTemplateManager::_update_template_tree() {
downloading_items.clear();
const String selected_version = version_list->get_item_text(version_list->get_current());
Ref<DirAccess> 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<TemplateID, LocalVector<String>> installed_template_files;
for (const KeyValue<PlatformID, PlatformInfo> &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);
}
}
}
}
_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::_fill_template_tree(Tree *p_tree, const HashMap<TemplateID, LocalVector<String>> &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<String> 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;
}
String current_group;
for (const KeyValue<PlatformID, PlatformInfo> &KV : platform_map) {
const PlatformInfo &template_platform = KV.value;
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;
}
if (any_installed && !all_installed) {
// Not going to change anymore.
break;
}
}
if ((is_available_tree && all_installed) || (is_installed_tree && !any_installed)) {
continue;
}
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 (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);
}
}
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);
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<String> &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 (is_installed_tree) {
delete_all_button->set_disabled(p_tree->get_root()->get_child_count() == 0);
}
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::_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 (!_is_online()) {
install_button->set_tooltip_text(TTRC("Download not available in offline mode."));
} else if (mirrors_empty) {
install_button->set_tooltip_text(TTRC("No mirrors available for download."));
} 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<Texture2D> ExportTemplateManager::_get_platform_icon(const String &p_platform_name) {
for (int i = 0; i < EditorExport::get_singleton()->get_export_platform_count(); i++) {
Ref<EditorExportPlatform> platform = EditorExport::get_singleton()->get_export_platform(i);
if (platform->get_name() == p_platform_name) {
return platform->get_logo();
}
}
return Ref<Texture2D>();
}
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: {
item_to_delete = p_item;
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."));
confirm_delete->popup_centered();
} 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();
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;
}
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])));
}
}
if (is_finished) {
// Exit "downloading mode".
queued_templates.clear();
downloading_items.clear();
set_process_internal(false);
_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;
}
TemplateDownloader *ExportTemplateManager::_get_available_downloader(int *r_from_index) {
int counter = -1;
for (TemplateDownloader *downloader : downloaders) {
counter++;
if (counter < *r_from_index) {
continue;
}
if (!downloader->is_downloading()) {
*r_from_index = counter;
return downloader;
}
}
return nullptr;
}
void ExportTemplateManager::_download_request_completed(const String &p_filename) {
bool found = false;
bool template_finished = false;
queued_files.erase(p_filename);
for (TreeItem *item : downloading_items) {
if (item->get_text(0) != p_filename) {
continue;
}
item->clear_buttons();
FileMetadata *meta = _get_file_metadata(p_filename);
meta->downloader = nullptr;
meta->download_status = DownloadStatus::COMPLETED;
meta->is_missing = false;
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();
}
}
void ExportTemplateManager::_download_request_failed(const String &p_filename, const String &p_reason) {
bool found = false;
bool template_finished = false;
queued_files.erase(p_filename);
for (TreeItem *item : downloading_items) {
if (item->get_text(0) != p_filename) {
continue;
}
FileMetadata *meta = _get_file_metadata(p_filename);
meta->downloader = nullptr;
_item_download_failed(item, p_reason);
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::_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);
}
}
}
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_download();
meta->downloader = nullptr;
}
}
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<String, FileMetadata>::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(const 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 meta->downloader->get_download_progress();
}
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();
RID ci = owning_tree->get_custom_drawing_canvas_item();
RS::get_singleton()->canvas_item_add_rect(ci, 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;
RS::get_singleton()->canvas_item_add_rect(ci, 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;
}
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<Texture2D> progress_texture = theme_cache.progress_icons[frame / 4 % 8];
const Rect2 rect = Rect2(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), progress_texture->get_size());
RS::get_singleton()->canvas_item_add_texture_rect(ci, rect, progress_texture->get_rid());
} break;
case DownloadStatus::IN_PROGRESS: {
float progress = _get_download_progress(p_item);
meta->progress_cache = progress;
RS::get_singleton()->canvas_item_add_rect(ci, Rect2(p_rect.position, Vector2(p_rect.size.x * progress, p_rect.size.y)), theme_cache.download_progress_color);
} break;
case DownloadStatus::COMPLETED: {
RS::get_singleton()->canvas_item_add_rect(ci, p_rect, theme_cache.download_progress_color);
} break;
case DownloadStatus::FAILED: {
RS::get_singleton()->canvas_item_add_rect(ci, 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::_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::ICU_DATA].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("AssetStore"));
open_mirror->set_button_icon(get_editor_theme_icon("ExternalLink"));
delete_all_button->set_button_icon(get_editor_theme_icon("Remove"));
tpz_button->set_button_icon(get_editor_theme_icon("FileBrowse"));
offline_mode_label->add_theme_color_override(SceneStringName(font_color), get_theme_color(SNAME("warning_color"), EditorStringName(Editor)));
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));
}
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<EditorExportPreset> &p_preset) {
if (p_preset.is_valid()) {
String gradle_build_dir = p_preset->get("gradle_build/gradle_build_directory");
if (!gradle_build_dir.is_empty()) {
return gradle_build_dir.path_join("build");
}
}
return "res://android/build";
}
String ExportTemplateManager::get_android_source_zip(const Ref<EditorExportPreset> &p_preset) {
if (p_preset.is_valid()) {
String android_source_zip = p_preset->get("gradle_build/android_source_template");
if (!android_source_zip.is_empty()) {
return android_source_zip;
}
}
const String templates_dir = EditorPaths::get_singleton()->get_export_templates_dir().path_join(GODOT_VERSION_FULL_CONFIG);
return templates_dir.path_join("android_source.zip");
}
String ExportTemplateManager::get_android_template_identifier(const Ref<EditorExportPreset> &p_preset) {
// The template identifier is the Godot version for the default template, and the full path plus md5 hash for custom templates.
if (p_preset.is_valid()) {
String android_source_zip = p_preset->get("gradle_build/android_source_template");
if (!android_source_zip.is_empty()) {
return android_source_zip + String(" [") + FileAccess::get_md5(android_source_zip) + String("]");
}
}
return GODOT_VERSION_FULL_CONFIG;
}
bool ExportTemplateManager::is_android_template_installed(const Ref<EditorExportPreset> &p_preset) {
return DirAccess::exists(get_android_build_directory(p_preset));
}
bool ExportTemplateManager::can_install_android_template(const Ref<EditorExportPreset> &p_preset) {
return FileAccess::exists(get_android_source_zip(p_preset));
}
Error ExportTemplateManager::install_android_template(const Ref<EditorExportPreset> &p_preset) {
const String source_zip = get_android_source_zip(p_preset);
ERR_FAIL_COND_V(!FileAccess::exists(source_zip), ERR_CANT_OPEN);
return install_android_template_from_file(source_zip, p_preset);
}
Error ExportTemplateManager::install_android_template_from_file(const String &p_file, const Ref<EditorExportPreset> &p_preset) {
// To support custom Android builds, we install the Java source code and buildsystem
// from android_source.zip to the project's res://android folder.
Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES);
ERR_FAIL_COND_V(da.is_null(), ERR_CANT_CREATE);
String build_dir = get_android_build_directory(p_preset);
String parent_dir = build_dir.get_base_dir();
// Make parent of the build dir (if it does not exist).
da->make_dir_recursive(parent_dir);
{
// Add identifier, to ensure building won't work if the current template doesn't match.
Ref<FileAccess> f = FileAccess::open(parent_dir.path_join(".build_version"), FileAccess::WRITE);
ERR_FAIL_COND_V(f.is_null(), ERR_CANT_CREATE);
f->store_line(get_android_template_identifier(p_preset));
}
// Create the android build directory.
Error err = da->make_dir_recursive(build_dir);
ERR_FAIL_COND_V(err != OK, err);
{
// Add an empty .gdignore file to avoid scan.
Ref<FileAccess> f = FileAccess::open(build_dir.path_join(".gdignore"), FileAccess::WRITE);
ERR_FAIL_COND_V(f.is_null(), ERR_CANT_CREATE);
f->store_line("");
}
// Uncompress source template.
Ref<FileAccess> io_fa;
zlib_filefunc_def io = zipio_create_io(&io_fa);
unzFile pkg = unzOpen2(p_file.utf8().get_data(), &io);
ERR_FAIL_NULL_V_MSG(pkg, ERR_CANT_OPEN, "Android sources not in ZIP format.");
int ret = unzGoToFirstFile(pkg);
int total_files = 0;
// Count files to unzip.
while (ret == UNZ_OK) {
total_files++;
ret = unzGoToNextFile(pkg);
}
ret = unzGoToFirstFile(pkg);
ProgressDialog::get_singleton()->add_task("uncompress_src", TTR("Uncompressing Android Build Sources"), total_files);
HashSet<String> dirs_tested;
int idx = 0;
while (ret == UNZ_OK) {
// Get file path.
unz_file_info info;
char fpath[16384];
ret = unzGetCurrentFileInfo(pkg, &info, fpath, 16384, nullptr, 0, nullptr, 0);
if (ret != UNZ_OK) {
break;
}
String path = String::utf8(fpath);
String base_dir = path.get_base_dir();
if (!path.ends_with("/")) {
Vector<uint8_t> uncomp_data;
uncomp_data.resize(info.uncompressed_size);
// Read.
unzOpenCurrentFile(pkg);
unzReadCurrentFile(pkg, uncomp_data.ptrw(), uncomp_data.size());
unzCloseCurrentFile(pkg);
if (!dirs_tested.has(base_dir)) {
da->make_dir_recursive(build_dir.path_join(base_dir));
dirs_tested.insert(base_dir);
}
String to_write = build_dir.path_join(path);
Ref<FileAccess> f = FileAccess::open(to_write, FileAccess::WRITE);
if (f.is_valid()) {
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
} else {
ERR_PRINT("Can't uncompress file: " + to_write);
}
}
ProgressDialog::get_singleton()->task_step("uncompress_src", path, idx);
idx++;
ret = unzGoToNextFile(pkg);
}
ProgressDialog::get_singleton()->end_task("uncompress_src");
unzClose(pkg);
EditorFileSystem::get_singleton()->scan_changes();
return OK;
}
void ExportTemplateManager::popup_manager() {
if (template_data.is_empty()) {
_initialize_template_data();
}
_update_online_mode();
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);
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));
tpz_button = memnew(Button);
tpz_button->set_tooltip_text(TTRC("Install from a TPZ file."));
download_header->add_child(tpz_button);
tpz_selection_dialog = memnew(EditorFileDialog);
tpz_selection_dialog->set_title(TTRC("Select Template File"));
tpz_selection_dialog->set_access(FileDialog::ACCESS_FILESYSTEM);
tpz_selection_dialog->set_file_mode(FileDialog::FILE_MODE_OPEN_FILE);
tpz_selection_dialog->set_current_dir(EDITOR_DEF("_export_template_download_directory", ""));
tpz_selection_dialog->add_filter("*.tpz", TTRC("Godot Export Templates"));
tpz_selection_dialog->connect("file_selected", callable_mp(this, &ExportTemplateManager::_tpz_file_selected));
add_child(tpz_selection_dialog);
tpz_button->connect(SceneStringName(pressed), callable_mp((FileDialog *)tpz_selection_dialog, &FileDialog::popup_file_dialog));
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));
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);
HBoxContainer *installed_header = memnew(HBoxContainer);
installed_templates_container->add_child(installed_header);
Label *template_header = memnew(Label(TTRC("Installed Templates")));
template_header->set_theme_type_variation("HeaderSmall");
installed_header->add_child(template_header);
open_folder_button = memnew(Button);
open_folder_button->set_tooltip_text(TTRC("Open templates directory."));
open_folder_button->set_h_size_flags(Control::SIZE_EXPAND | Control::SIZE_SHRINK_END);
installed_header->add_child(open_folder_button);
open_folder_button->connect(SceneStringName(pressed), callable_mp(this, &ExportTemplateManager::_open_template_directory));
delete_all_button = memnew(Button);
delete_all_button->set_tooltip_text(TTRC("Remove all installed templates."));
installed_header->add_child(delete_all_button);
delete_all_button->connect(SceneStringName(pressed), callable_mp(this, &ExportTemplateManager::_delete_all));
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);
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"));
enable_online_button->set_v_size_flags(Control::SIZE_SHRINK_CENTER);
offline_container->add_child(enable_online_button);
enable_online_button->connect(SceneStringName(pressed), callable_mp(this, &ExportTemplateManager::_force_online_mode));
confirm_delete = memnew(ConfirmationDialog);
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 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;
}
for (const String &header : p_headers) {
if (header.to_lower().begins_with("content-length:")) {
file_size = header.split(":")[1].to_int();
}
}
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 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;
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();
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;
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;
}
const PackedByteArray clean_fragment = p_body.slice(0, full_file_chunk_size);
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.
// EOCD (End of Central Directory)
PackedByteArray eocd;
eocd.resize_initialized(22);
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).
// Write Mini-Zip to a file.
Ref<FileAccess> 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<FileAccess> 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;
}
// IMPORTANT: The path in the ZIP reader must exactly match the one in the CD.
int err = UNZ_OK;
// 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;
}
err = unzOpenCurrentFile(uzf);
if (err != UNZ_OK) {
_download_failed(TTR("Could not open file within zip archive."));
return;
}
// 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;
}
// 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;
}
// Verify the data and return.
err = unzCloseCurrentFile(uzf);
if (err != UNZ_OK) {
_download_failed(TTR("CRC error reading file from zip archive."));
return;
}
}
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);
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();
current_step = Step::WAITING;
emit_signal(SNAME("download_completed"), filename);
}
} break;
}
}
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"));
}
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;
}