Path: blob/master/editor/asset_library/asset_library_editor_plugin.cpp
20785 views
/**************************************************************************/1/* asset_library_editor_plugin.cpp */2/**************************************************************************/3/* This file is part of: */4/* GODOT ENGINE */5/* https://godotengine.org */6/**************************************************************************/7/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */8/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */9/* */10/* Permission is hereby granted, free of charge, to any person obtaining */11/* a copy of this software and associated documentation files (the */12/* "Software"), to deal in the Software without restriction, including */13/* without limitation the rights to use, copy, modify, merge, publish, */14/* distribute, sublicense, and/or sell copies of the Software, and to */15/* permit persons to whom the Software is furnished to do so, subject to */16/* the following conditions: */17/* */18/* The above copyright notice and this permission notice shall be */19/* included in all copies or substantial portions of the Software. */20/* */21/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */22/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */23/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */24/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */25/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */26/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */27/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */28/**************************************************************************/2930#include "asset_library_editor_plugin.h"3132#include "core/io/dir_access.h"33#include "core/io/json.h"34#include "core/io/stream_peer_tls.h"35#include "core/os/keyboard.h"36#include "core/version.h"37#include "editor/editor_main_screen.h"38#include "editor/editor_node.h"39#include "editor/editor_string_names.h"40#include "editor/file_system/editor_paths.h"41#include "editor/gui/editor_file_dialog.h"42#include "editor/settings/editor_settings.h"43#include "editor/settings/project_settings_editor.h"44#include "editor/themes/editor_scale.h"45#include "scene/gui/menu_button.h"46#include "scene/gui/separator.h"47#include "scene/resources/image_texture.h"4849static inline void setup_http_request(HTTPRequest *request) {50request->set_use_threads(EDITOR_GET("asset_library/use_threads"));5152const String proxy_host = EDITOR_GET("network/http_proxy/host");53const int proxy_port = EDITOR_GET("network/http_proxy/port");54request->set_http_proxy(proxy_host, proxy_port);55request->set_https_proxy(proxy_host, proxy_port);56}5758void EditorAssetLibraryItem::configure(const String &p_title, int p_asset_id, const String &p_category, int p_category_id, const String &p_author, int p_author_id, const String &p_cost) {59title_text = p_title;60title->set_text(title_text);61title->set_tooltip_text(title_text);62asset_id = p_asset_id;63category->set_text(p_category);64category_id = p_category_id;65author->set_text(p_author);66author_id = p_author_id;67price->set_text(p_cost);6869_calculate_misc_links_size();70}7172void EditorAssetLibraryItem::set_image(int p_type, int p_index, const Ref<Texture2D> &p_image) {73ERR_FAIL_COND(p_type != EditorAssetLibrary::IMAGE_QUEUE_ICON);74ERR_FAIL_COND(p_index != 0);7576icon->set_texture_normal(p_image);77}7879void EditorAssetLibraryItem::_notification(int p_what) {80switch (p_what) {81case NOTIFICATION_ENTER_TREE: {82icon->set_texture_normal(get_editor_theme_icon(SNAME("ProjectIconLoading")));83category->add_theme_color_override(SceneStringName(font_color), Color(0.5, 0.5, 0.5));84author->add_theme_color_override(SceneStringName(font_color), Color(0.5, 0.5, 0.5));85price->add_theme_color_override(SceneStringName(font_color), Color(0.5, 0.5, 0.5));8687if (author->get_default_cursor_shape() == CURSOR_ARROW) {88// Disable visible feedback if author link isn't clickable.89author->add_theme_color_override("font_pressed_color", Color(0.5, 0.5, 0.5));90author->add_theme_color_override("font_hover_color", Color(0.5, 0.5, 0.5));91}9293_calculate_misc_links_size();94} break;9596case NOTIFICATION_TRANSLATION_CHANGED: {97_calculate_misc_links_size();98} break;99100case NOTIFICATION_RESIZED: {101calculate_misc_links_ratio();102} break;103}104}105106void EditorAssetLibraryItem::_calculate_misc_links_size() {107Ref<TextLine> text_buf;108text_buf.instantiate();109text_buf->add_string(author->get_text(), author->get_button_font(), author->get_button_font_size());110author_width = text_buf->get_line_width();111112text_buf->clear();113const Ref<Font> font = get_theme_font(SceneStringName(font), SNAME("Label"));114const int font_size = get_theme_font_size(SceneStringName(font_size), SNAME("Label"));115text_buf->add_string(price->get_text(), font, font_size);116price_width = text_buf->get_line_width();117118calculate_misc_links_ratio();119}120121void EditorAssetLibraryItem::calculate_misc_links_ratio() {122const int separators_width = 15 * EDSCALE;123const float total_width = author_price_hbox->get_size().width - (separator->get_size().width + separators_width);124if (total_width <= 0) {125return;126}127128float ratio_left = 1;129// Make the ratios a fraction bigger, to avoid unnecessary trimming.130const float extra_ratio = 4.0 / total_width;131132const float author_ratio = MIN(1, author_width / total_width);133author->set_stretch_ratio(author_ratio + extra_ratio);134ratio_left -= author_ratio;135136const float price_ratio = MIN(1, price_width / total_width);137price->set_stretch_ratio(price_ratio + extra_ratio);138ratio_left -= price_ratio;139140spacer->set_stretch_ratio(ratio_left);141}142143void EditorAssetLibraryItem::_asset_clicked() {144emit_signal(SNAME("asset_selected"), asset_id);145}146147void EditorAssetLibraryItem::_category_clicked() {148emit_signal(SNAME("category_selected"), category_id);149}150151void EditorAssetLibraryItem::_author_clicked() {152emit_signal(SNAME("author_selected"), author->get_text());153}154155void EditorAssetLibraryItem::_bind_methods() {156ClassDB::bind_method("set_image", &EditorAssetLibraryItem::set_image);157ADD_SIGNAL(MethodInfo("asset_selected"));158ADD_SIGNAL(MethodInfo("category_selected"));159ADD_SIGNAL(MethodInfo("author_selected"));160}161162EditorAssetLibraryItem::EditorAssetLibraryItem(bool p_clickable) {163Ref<StyleBoxEmpty> border;164border.instantiate();165border->set_content_margin_all(5 * EDSCALE);166add_theme_style_override(SceneStringName(panel), border);167168HBoxContainer *hb = memnew(HBoxContainer);169// Add some spacing to visually separate the icon from the asset details.170hb->add_theme_constant_override("separation", 15 * EDSCALE);171add_child(hb);172173icon = memnew(TextureButton);174icon->set_accessibility_name(TTRC("Open asset details"));175icon->set_custom_minimum_size(Size2(64, 64) * EDSCALE);176hb->add_child(icon);177178VBoxContainer *vb = memnew(VBoxContainer);179180hb->add_child(vb);181vb->set_h_size_flags(Control::SIZE_EXPAND_FILL);182183title = memnew(LinkButton);184title->set_accessibility_name(TTRC("Title"));185title->set_auto_translate_mode(AutoTranslateMode::AUTO_TRANSLATE_MODE_DISABLED);186title->set_text_overrun_behavior(TextServer::OVERRUN_TRIM_ELLIPSIS);187title->set_underline_mode(LinkButton::UNDERLINE_MODE_ON_HOVER);188vb->add_child(title);189190category = memnew(LinkButton);191category->set_accessibility_name(TTRC("Category"));192category->set_text_overrun_behavior(TextServer::OVERRUN_TRIM_ELLIPSIS);193category->set_underline_mode(LinkButton::UNDERLINE_MODE_ON_HOVER);194vb->add_child(category);195196author_price_hbox = memnew(HBoxContainer);197author_price_hbox->add_theme_constant_override("separation", 5 * EDSCALE);198vb->add_child(author_price_hbox);199200author = memnew(LinkButton);201author->set_tooltip_text(TTRC("Author"));202author->set_text_overrun_behavior(TextServer::OVERRUN_TRIM_ELLIPSIS);203author->set_accessibility_name(TTRC("Author"));204author->set_h_size_flags(Control::SIZE_EXPAND_FILL);205author_price_hbox->add_child(author);206207separator = memnew(HSeparator);208author_price_hbox->add_child(separator);209210if (p_clickable) {211author->set_underline_mode(LinkButton::UNDERLINE_MODE_ON_HOVER);212icon->set_default_cursor_shape(CURSOR_POINTING_HAND);213icon->connect(SceneStringName(pressed), callable_mp(this, &EditorAssetLibraryItem::_asset_clicked));214title->connect(SceneStringName(pressed), callable_mp(this, &EditorAssetLibraryItem::_asset_clicked));215category->connect(SceneStringName(pressed), callable_mp(this, &EditorAssetLibraryItem::_category_clicked));216author->connect(SceneStringName(pressed), callable_mp(this, &EditorAssetLibraryItem::_author_clicked));217} else {218title->set_mouse_filter(MOUSE_FILTER_IGNORE);219category->set_mouse_filter(MOUSE_FILTER_IGNORE);220author->set_underline_mode(LinkButton::UNDERLINE_MODE_NEVER);221author->set_default_cursor_shape(CURSOR_ARROW);222}223224Ref<StyleBoxEmpty> label_margin;225label_margin.instantiate();226label_margin->set_content_margin_all(0);227228price = memnew(Label);229price->set_focus_mode(FOCUS_ACCESSIBILITY);230price->add_theme_style_override(CoreStringName(normal), label_margin);231price->set_tooltip_text(TTRC("License"));232price->set_text_overrun_behavior(TextServer::OVERRUN_TRIM_ELLIPSIS);233price->set_accessibility_name(TTRC("License"));234price->set_h_size_flags(Control::SIZE_EXPAND_FILL);235price->set_mouse_filter(MOUSE_FILTER_PASS);236author_price_hbox->add_child(price);237238spacer = memnew(Control);239spacer->set_h_size_flags(Control::SIZE_EXPAND_FILL);240author_price_hbox->add_child(spacer);241242set_custom_minimum_size(Size2(250, 80) * EDSCALE);243set_h_size_flags(Control::SIZE_EXPAND_FILL);244}245246//////////////////////////////////////////////////////////////////////////////247248void EditorAssetLibraryItemDescription::set_image(int p_type, int p_index, const Ref<Texture2D> &p_image) {249switch (p_type) {250case EditorAssetLibrary::IMAGE_QUEUE_ICON: {251item->call("set_image", p_type, p_index, p_image);252icon = p_image;253} break;254case EditorAssetLibrary::IMAGE_QUEUE_THUMBNAIL: {255for (int i = 0; i < preview_images.size(); i++) {256if (preview_images[i].id == p_index) {257if (preview_images[i].is_video) {258Ref<Image> overlay = previews->get_editor_theme_icon(SNAME("PlayOverlay"))->get_image();259Ref<Image> thumbnail = p_image->get_image();260thumbnail = thumbnail->duplicate();261Point2i overlay_pos = Point2i((thumbnail->get_width() - overlay->get_width()) / 2, (thumbnail->get_height() - overlay->get_height()) / 2);262263// Overlay and thumbnail need the same format for `blend_rect` to work.264thumbnail->convert(Image::FORMAT_RGBA8);265thumbnail->blend_rect(overlay, overlay->get_used_rect(), overlay_pos);266preview_images[i].button->set_button_icon(ImageTexture::create_from_image(thumbnail));267268// Make it clearer that clicking it will open an external link269preview_images[i].button->set_default_cursor_shape(Control::CURSOR_POINTING_HAND);270} else {271preview_images[i].button->set_button_icon(p_image);272}273break;274}275}276} break;277case EditorAssetLibrary::IMAGE_QUEUE_SCREENSHOT: {278for (int i = 0; i < preview_images.size(); i++) {279if (preview_images[i].id == p_index) {280preview_images.write[i].image = p_image;281if (preview_images[i].button->is_pressed()) {282_preview_click(p_index);283}284break;285}286}287} break;288}289}290291void EditorAssetLibraryItemDescription::_notification(int p_what) {292switch (p_what) {293case NOTIFICATION_THEME_CHANGED: {294previews_bg->add_theme_style_override(SceneStringName(panel), previews->get_theme_stylebox(CoreStringName(normal), SNAME("TextEdit")));295} break;296297case NOTIFICATION_POST_POPUP: {298callable_mp(item, &EditorAssetLibraryItem::calculate_misc_links_ratio).call_deferred();299} break;300}301}302303void EditorAssetLibraryItemDescription::_bind_methods() {304ClassDB::bind_method(D_METHOD("set_image"), &EditorAssetLibraryItemDescription::set_image);305}306307void EditorAssetLibraryItemDescription::_link_click(const String &p_url) {308ERR_FAIL_COND(!p_url.begins_with("http"));309OS::get_singleton()->shell_open(p_url);310}311312void EditorAssetLibraryItemDescription::_preview_click(int p_id) {313for (int i = 0; i < preview_images.size(); i++) {314if (preview_images[i].id == p_id) {315preview_images[i].button->set_pressed(true);316if (!preview_images[i].is_video) {317if (preview_images[i].image.is_valid()) {318preview->set_texture(preview_images[i].image);319child_controls_changed();320}321} else {322_link_click(preview_images[i].video_link);323}324} else {325preview_images[i].button->set_pressed(false);326}327}328}329330void EditorAssetLibraryItemDescription::configure(const String &p_title, int p_asset_id, const String &p_category, int p_category_id, const String &p_author, int p_author_id, const String &p_cost, int p_version, const String &p_version_string, const String &p_description, const String &p_download_url, const String &p_browse_url, const String &p_sha256_hash) {331asset_id = p_asset_id;332title = p_title;333download_url = p_download_url;334sha256 = p_sha256_hash;335item->configure(p_title, p_asset_id, p_category, p_category_id, p_author, p_author_id, p_cost);336description->clear();337description->add_text(TTR("Version:") + " " + p_version_string + "\n");338description->add_text(TTR("Contents:") + " ");339description->push_meta(p_browse_url);340description->add_text(TTR("View Files"));341description->pop();342description->add_text("\n" + TTR("Description:") + "\n\n");343description->append_text(p_description);344description->set_selection_enabled(true);345description->set_context_menu_enabled(true);346set_title(p_title);347}348349void EditorAssetLibraryItemDescription::add_preview(int p_id, bool p_video, const String &p_url) {350if (preview_images.is_empty()) {351previews_vbox->show();352}353354Preview new_preview;355new_preview.id = p_id;356new_preview.video_link = p_url;357new_preview.is_video = p_video;358new_preview.button = memnew(Button);359new_preview.button->set_button_icon(previews->get_editor_theme_icon(SNAME("ThumbnailWait")));360new_preview.button->set_toggle_mode(true);361new_preview.button->connect(SceneStringName(pressed), callable_mp(this, &EditorAssetLibraryItemDescription::_preview_click).bind(p_id));362preview_hb->add_child(new_preview.button);363if (!p_video) {364new_preview.image = previews->get_editor_theme_icon(SNAME("ThumbnailWait"));365}366preview_images.push_back(new_preview);367if (preview_images.size() == 1 && !p_video) {368_preview_click(p_id);369}370}371372EditorAssetLibraryItemDescription::EditorAssetLibraryItemDescription() {373HBoxContainer *hbox = memnew(HBoxContainer);374add_child(hbox);375VBoxContainer *desc_vbox = memnew(VBoxContainer);376hbox->add_child(desc_vbox);377hbox->add_theme_constant_override("separation", 15 * EDSCALE);378379item = memnew(EditorAssetLibraryItem);380381desc_vbox->add_child(item);382desc_vbox->set_custom_minimum_size(Size2(440 * EDSCALE, 440 * EDSCALE));383384description = memnew(RichTextLabel);385desc_vbox->add_child(description);386description->set_v_size_flags(Control::SIZE_EXPAND_FILL);387description->connect("meta_clicked", callable_mp(this, &EditorAssetLibraryItemDescription::_link_click));388description->add_theme_constant_override(SceneStringName(line_separation), Math::round(5 * EDSCALE));389390previews_vbox = memnew(VBoxContainer);391previews_vbox->hide(); // Will be shown if we add any previews later.392393hbox->add_child(previews_vbox);394previews_vbox->add_theme_constant_override("separation", 15 * EDSCALE);395previews_vbox->set_v_size_flags(Control::SIZE_EXPAND_FILL);396previews_vbox->set_h_size_flags(Control::SIZE_EXPAND_FILL);397398preview = memnew(TextureRect);399previews_vbox->add_child(preview);400preview->set_expand_mode(TextureRect::EXPAND_IGNORE_SIZE);401preview->set_stretch_mode(TextureRect::STRETCH_KEEP_ASPECT_CENTERED);402preview->set_custom_minimum_size(Size2(640 * EDSCALE, 345 * EDSCALE));403preview->set_v_size_flags(Control::SIZE_EXPAND_FILL);404preview->set_h_size_flags(Control::SIZE_EXPAND_FILL);405406previews_bg = memnew(PanelContainer);407previews_vbox->add_child(previews_bg);408previews_bg->set_custom_minimum_size(Size2(640 * EDSCALE, 101 * EDSCALE));409410previews = memnew(ScrollContainer);411previews_bg->add_child(previews);412previews->set_vertical_scroll_mode(ScrollContainer::SCROLL_MODE_DISABLED);413preview_hb = memnew(HBoxContainer);414preview_hb->set_v_size_flags(Control::SIZE_EXPAND_FILL);415416previews->add_child(preview_hb);417set_ok_button_text(TTRC("Download"));418set_cancel_button_text(TTRC("Close"));419}420421///////////////////////////////////////////////////////////////////////////////////422423void EditorAssetLibraryItemDownload::_http_download_completed(int p_status, int p_code, const PackedStringArray &headers, const PackedByteArray &p_data) {424String error_text;425426switch (p_status) {427case HTTPRequest::RESULT_CHUNKED_BODY_SIZE_MISMATCH:428case HTTPRequest::RESULT_CONNECTION_ERROR:429case HTTPRequest::RESULT_BODY_SIZE_LIMIT_EXCEEDED: {430error_text = TTR("Connection error, please try again.");431status->set_text(TTRC("Can't connect."));432} break;433case HTTPRequest::RESULT_CANT_CONNECT:434case HTTPRequest::RESULT_TLS_HANDSHAKE_ERROR: {435error_text = TTR("Can't connect to host:") + " " + host;436status->set_text(TTRC("Can't connect."));437} break;438case HTTPRequest::RESULT_NO_RESPONSE: {439error_text = TTR("No response from host:") + " " + host;440status->set_text(TTRC("No response."));441} break;442case HTTPRequest::RESULT_CANT_RESOLVE: {443error_text = TTR("Can't resolve hostname:") + " " + host;444status->set_text(TTRC("Can't resolve."));445} break;446case HTTPRequest::RESULT_REQUEST_FAILED: {447error_text = TTR("Request failed, return code:") + " " + itos(p_code);448status->set_text(TTRC("Request failed."));449} break;450case HTTPRequest::RESULT_DOWNLOAD_FILE_CANT_OPEN:451case HTTPRequest::RESULT_DOWNLOAD_FILE_WRITE_ERROR: {452error_text = TTR("Cannot save response to:") + " " + download->get_download_file();453status->set_text(TTRC("Write error."));454} break;455case HTTPRequest::RESULT_REDIRECT_LIMIT_REACHED: {456error_text = TTR("Request failed, too many redirects");457status->set_text(TTRC("Redirect loop."));458} break;459case HTTPRequest::RESULT_TIMEOUT: {460error_text = TTR("Request failed, timeout");461status->set_text(TTRC("Timeout."));462} break;463default: {464if (p_code != 200) {465error_text = TTR("Request failed, return code:") + " " + itos(p_code);466status->set_text(TTR("Failed:") + " " + itos(p_code));467} else if (!sha256.is_empty()) {468String download_sha256 = FileAccess::get_sha256(download->get_download_file());469if (sha256 != download_sha256) {470error_text = TTR("Bad download hash, assuming file has been tampered with.") + "\n";471error_text += TTR("Expected:") + " " + sha256 + "\n" + TTR("Got:") + " " + download_sha256;472status->set_text(TTRC("Failed SHA-256 hash check"));473}474}475} break;476}477478// Make the progress bar invisible but don't reflow other Controls around it.479progress->set_modulate(Color(0, 0, 0, 0));480progress->set_indeterminate(false);481482if (!error_text.is_empty()) {483download_error->set_text(TTR("Asset Download Error:") + "\n" + error_text);484download_error->popup_centered();485// Let the user retry the download.486retry_button->show();487return;488}489490install_button->set_disabled(false);491status->set_text(TTRC("Ready to install!"));492493set_process(false);494495// Automatically prompt for installation once the download is completed.496install();497}498499void EditorAssetLibraryItemDownload::configure(const String &p_title, int p_asset_id, const Ref<Texture2D> &p_preview, const String &p_download_url, const String &p_sha256_hash) {500title->set_text(p_title);501icon->set_texture(p_preview);502asset_id = p_asset_id;503if (p_preview.is_null()) {504icon->set_texture(get_editor_theme_icon(SNAME("FileBrokenBigThumb")));505}506host = p_download_url;507sha256 = p_sha256_hash;508_make_request();509}510511void EditorAssetLibraryItemDownload::_notification(int p_what) {512switch (p_what) {513case NOTIFICATION_THEME_CHANGED: {514panel->add_theme_style_override(SceneStringName(panel), get_theme_stylebox(SceneStringName(panel), SNAME("AssetLib")));515status->add_theme_color_override(SceneStringName(font_color), get_theme_color(SNAME("status_color"), SNAME("AssetLib")));516dismiss_button->set_texture_normal(get_theme_icon(SNAME("dismiss"), SNAME("AssetLib")));517} break;518519case NOTIFICATION_PROCESS: {520// Make the progress bar visible again when retrying the download.521progress->set_modulate(Color(1, 1, 1, 1));522523if (download->get_downloaded_bytes() > 0) {524progress->set_max(download->get_body_size());525progress->set_value(download->get_downloaded_bytes());526}527528int cstatus = download->get_http_client_status();529530if (cstatus == HTTPClient::STATUS_BODY) {531if (download->get_body_size() > 0) {532progress->set_indeterminate(false);533status->set_text(vformat(534TTR("Downloading (%s / %s)..."),535String::humanize_size(download->get_downloaded_bytes()),536String::humanize_size(download->get_body_size())));537} else {538progress->set_indeterminate(true);539status->set_text(vformat(540TTR("Downloading...") + " (%s)",541String::humanize_size(download->get_downloaded_bytes())));542}543}544545if (cstatus != prev_status) {546switch (cstatus) {547case HTTPClient::STATUS_RESOLVING: {548status->set_text(TTRC("Resolving..."));549progress->set_max(1);550progress->set_value(0);551} break;552case HTTPClient::STATUS_CONNECTING: {553status->set_text(TTRC("Connecting..."));554progress->set_max(1);555progress->set_value(0);556} break;557case HTTPClient::STATUS_REQUESTING: {558status->set_text(TTRC("Requesting..."));559progress->set_max(1);560progress->set_value(0);561} break;562default: {563}564}565prev_status = cstatus;566}567} break;568}569}570571void EditorAssetLibraryItemDownload::_close() {572// Clean up downloaded file.573DirAccess::remove_file_or_error(download->get_download_file());574queue_free();575}576577bool EditorAssetLibraryItemDownload::can_install() const {578return !install_button->is_disabled();579}580581void EditorAssetLibraryItemDownload::install() {582String file = download->get_download_file();583584if (external_install) {585emit_signal(SNAME("install_asset"), file, title->get_text());586return;587}588589asset_installer->set_asset_name(title->get_text());590asset_installer->open_asset(file, true);591}592593void EditorAssetLibraryItemDownload::_make_request() {594// Hide the Retry button if we've just pressed it.595retry_button->hide();596597download->cancel_request();598download->set_download_file(EditorPaths::get_singleton()->get_cache_dir().path_join("tmp_asset_" + itos(asset_id)) + ".zip");599600Error err = download->request(host);601if (err != OK) {602status->set_text(TTRC("Error making request"));603} else {604progress->set_indeterminate(true);605set_process(true);606}607}608609void EditorAssetLibraryItemDownload::_bind_methods() {610ADD_SIGNAL(MethodInfo("install_asset", PropertyInfo(Variant::STRING, "zip_path"), PropertyInfo(Variant::STRING, "name")));611}612613EditorAssetLibraryItemDownload::EditorAssetLibraryItemDownload() {614panel = memnew(PanelContainer);615add_child(panel);616617HBoxContainer *hb = memnew(HBoxContainer);618panel->add_child(hb);619icon = memnew(TextureRect);620icon->set_stretch_mode(TextureRect::STRETCH_KEEP_ASPECT_CENTERED);621icon->set_v_size_flags(0);622hb->add_child(icon);623624VBoxContainer *vb = memnew(VBoxContainer);625hb->add_child(vb);626vb->set_h_size_flags(Control::SIZE_EXPAND_FILL);627628HBoxContainer *title_hb = memnew(HBoxContainer);629vb->add_child(title_hb);630title = memnew(Label);631title->set_focus_mode(FOCUS_ACCESSIBILITY);632title_hb->add_child(title);633title->set_h_size_flags(Control::SIZE_EXPAND_FILL);634635dismiss_button = memnew(TextureButton);636dismiss_button->connect(SceneStringName(pressed), callable_mp(this, &EditorAssetLibraryItemDownload::_close));637dismiss_button->set_accessibility_name(TTRC("Close"));638title_hb->add_child(dismiss_button);639640title->set_clip_text(true);641642vb->add_spacer();643644status = memnew(Label(TTRC("Idle")));645vb->add_child(status);646progress = memnew(ProgressBar);647progress->set_editor_preview_indeterminate(true);648vb->add_child(progress);649650HBoxContainer *hb2 = memnew(HBoxContainer);651vb->add_child(hb2);652hb2->add_spacer();653654install_button = memnew(Button);655install_button->set_text(TTRC("Install..."));656install_button->set_disabled(true);657install_button->connect(SceneStringName(pressed), callable_mp(this, &EditorAssetLibraryItemDownload::install));658659retry_button = memnew(Button);660retry_button->set_text(TTRC("Retry"));661retry_button->connect(SceneStringName(pressed), callable_mp(this, &EditorAssetLibraryItemDownload::_make_request));662// Only show the Retry button in case of a failure.663retry_button->hide();664665hb2->add_child(retry_button);666hb2->add_child(install_button);667set_custom_minimum_size(Size2(310, 0) * EDSCALE);668669download = memnew(HTTPRequest);670panel->add_child(download);671download->connect("request_completed", callable_mp(this, &EditorAssetLibraryItemDownload::_http_download_completed));672setup_http_request(download);673674download_error = memnew(AcceptDialog);675panel->add_child(download_error);676download_error->set_title(TTRC("Download Error"));677678asset_installer = memnew(EditorAssetInstaller);679panel->add_child(asset_installer);680asset_installer->connect(SceneStringName(confirmed), callable_mp(this, &EditorAssetLibraryItemDownload::_close));681682prev_status = -1;683684external_install = false;685}686687////////////////////////////////////////////////////////////////////////////////688void EditorAssetLibrary::_notification(int p_what) {689switch (p_what) {690case NOTIFICATION_READY: {691add_theme_style_override(SceneStringName(panel), get_theme_stylebox(SNAME("bg"), SNAME("AssetLib")));692error_label->move_to_front();693} break;694695case NOTIFICATION_TRANSLATION_CHANGED: {696if (!initial_loading) {697_rerun_search(-1);698}699} break;700701case NOTIFICATION_THEME_CHANGED: {702error_tr->set_texture(get_editor_theme_icon(SNAME("Error")));703filter->set_right_icon(get_editor_theme_icon(SNAME("Search")));704library_scroll->add_theme_style_override(SceneStringName(panel), get_theme_stylebox(SceneStringName(panel), SNAME("Tree")));705downloads_scroll->add_theme_style_override(SceneStringName(panel), get_theme_stylebox(SNAME("downloads"), SNAME("AssetLib")));706error_label->add_theme_color_override("color", get_theme_color(SNAME("error_color"), EditorStringName(Editor)));707} break;708709case NOTIFICATION_VISIBILITY_CHANGED: {710if (is_visible()) {711#ifndef ANDROID_ENABLED712// Focus the search box automatically when switching to the Templates tab (in the Project Manager)713// or switching to the AssetLib tab (in the editor).714// The Project Manager's project filter box is automatically focused in the project manager code.715filter->grab_focus();716#endif717718if (initial_loading) {719_repository_changed(0); // Update when shown for the first time.720}721}722} break;723724case NOTIFICATION_PROCESS: {725HTTPClient::Status s = request->get_http_client_status();726const bool loading = s != HTTPClient::STATUS_DISCONNECTED;727728if (loading) {729library_scroll->set_modulate(Color(1, 1, 1, 0.5));730} else {731library_scroll->set_modulate(Color(1, 1, 1, 1));732}733734const bool no_downloads = downloads_hb->get_child_count() == 0;735if (no_downloads == downloads_scroll->is_visible()) {736downloads_scroll->set_visible(!no_downloads);737738library_mc->set_theme_type_variation(no_downloads ? (Engine::get_singleton()->is_project_manager_hint() ? "NoBorderAssetLibProjectManager" : "NoBorderAssetLib") : "NoBorderHorizontal");739library_scroll->set_scroll_hint_mode(no_downloads ? ScrollContainer::SCROLL_HINT_MODE_TOP_AND_LEFT : ScrollContainer::SCROLL_HINT_MODE_ALL);740}741742} break;743744case NOTIFICATION_RESIZED: {745_update_asset_items_columns();746} break;747748case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: {749if (!EditorSettings::get_singleton()->check_changed_settings_in_group("asset_library") &&750!EditorSettings::get_singleton()->check_changed_settings_in_group("network")) {751break;752}753754_update_repository_options();755setup_http_request(request);756757const bool loading_blocked_new = ((int)EDITOR_GET("network/connection/network_mode") == EditorSettings::NETWORK_OFFLINE);758if (loading_blocked_new != loading_blocked) {759loading_blocked = loading_blocked_new;760761if (!loading_blocked && is_visible()) {762_request_current_config(); // Reload config now that the network is available.763}764}765} break;766}767}768769void EditorAssetLibrary::_update_repository_options() {770// TODO: Move to editor_settings.cpp771Dictionary default_urls;772default_urls["godotengine.org (Official)"] = "https://godotengine.org/asset-library/api";773Dictionary available_urls = _EDITOR_DEF("asset_library/available_urls", default_urls, true);774repository->clear();775int i = 0;776for (const KeyValue<Variant, Variant> &kv : available_urls) {777repository->add_item(kv.key);778repository->set_item_metadata(i, kv.value);779i++;780}781}782783void EditorAssetLibrary::shortcut_input(const Ref<InputEvent> &p_event) {784ERR_FAIL_COND(p_event.is_null());785786const Ref<InputEventKey> key = p_event;787788if (key.is_valid() && key->is_pressed()) {789if (key->is_match(InputEventKey::create_reference(KeyModifierMask::CMD_OR_CTRL | Key::F)) && is_visible_in_tree()) {790filter->grab_focus();791filter->select_all();792accept_event();793}794}795}796797void EditorAssetLibrary::_install_asset() {798ERR_FAIL_NULL(description);799800EditorAssetLibraryItemDownload *d = _get_asset_in_progress(description->get_asset_id());801if (d) {802d->install();803return;804}805806EditorAssetLibraryItemDownload *download = memnew(EditorAssetLibraryItemDownload);807downloads_hb->add_child(download);808download->configure(description->get_title(), description->get_asset_id(), description->get_preview_icon(), description->get_download_url(), description->get_sha256());809810if (templates_only) {811download->set_external_install(true);812download->connect("install_asset", callable_mp(this, &EditorAssetLibrary::_install_external_asset));813}814}815816const char *EditorAssetLibrary::sort_key[SORT_MAX] = {817"updated",818"updated",819"name",820"name",821"cost",822"cost",823};824825const char *EditorAssetLibrary::sort_text[SORT_MAX] = {826TTRC("Recently Updated"),827TTRC("Least Recently Updated"),828TTRC("Name (A-Z)"),829TTRC("Name (Z-A)"),830TTRC("License (A-Z)"), // "cost" stores the SPDX license name in the Godot Asset Library.831TTRC("License (Z-A)"), // "cost" stores the SPDX license name in the Godot Asset Library.832};833834const char *EditorAssetLibrary::support_key[SUPPORT_MAX] = {835"official", // Former name for the Featured support level (still used on the API backend).836"community",837"testing",838};839840const char *EditorAssetLibrary::support_text[SUPPORT_MAX] = {841TTRC("Featured"),842TTRC("Community"),843TTRC("Testing"),844};845846void EditorAssetLibrary::_select_author(const String &p_author) {847if (!host.contains("godotengine.org")) {848// Don't open the link for alternative repositories.849return;850}851OS::get_singleton()->shell_open("https://godotengine.org/asset-library/asset?user=" + p_author.uri_encode());852}853854void EditorAssetLibrary::_select_category(int p_id) {855for (int i = 0; i < categories->get_item_count(); i++) {856if (i == 0) {857continue;858}859int id = categories->get_item_metadata(i);860if (id == p_id) {861categories->select(i);862_search();863break;864}865}866}867868void EditorAssetLibrary::_select_asset(int p_id) {869_api_request("asset/" + itos(p_id), REQUESTING_ASSET);870}871872void EditorAssetLibrary::_image_update(bool p_use_cache, bool p_final, const PackedByteArray &p_data, int p_queue_id) {873Object *obj = ObjectDB::get_instance(image_queue[p_queue_id].target);874if (!obj) {875return;876}877878bool image_set = false;879PackedByteArray image_data = p_data;880881if (p_use_cache) {882String cache_filename_base = EditorPaths::get_singleton()->get_cache_dir().path_join("assetimage_" + image_queue[p_queue_id].image_url.md5_text());883884Ref<FileAccess> file = FileAccess::open(cache_filename_base + ".data", FileAccess::READ);885if (file.is_valid()) {886PackedByteArray cached_data;887int len = file->get_32();888cached_data.resize(len);889890uint8_t *w = cached_data.ptrw();891file->get_buffer(w, len);892893image_data = cached_data;894}895}896897int len = image_data.size();898const uint8_t *r = image_data.ptr();899Ref<Image> image = memnew(Image);900901uint8_t png_signature[8] = { 137, 80, 78, 71, 13, 10, 26, 10 };902uint8_t jpg_signature[3] = { 255, 216, 255 };903uint8_t webp_signature[4] = { 82, 73, 70, 70 };904uint8_t bmp_signature[2] = { 66, 77 };905906if (r) {907Ref<Image> parsed_image;908909if ((memcmp(&r[0], &png_signature[0], 8) == 0) && Image::_png_mem_loader_func) {910parsed_image = Image::_png_mem_loader_func(r, len);911} else if ((memcmp(&r[0], &jpg_signature[0], 3) == 0) && Image::_jpg_mem_loader_func) {912parsed_image = Image::_jpg_mem_loader_func(r, len);913} else if ((memcmp(&r[0], &webp_signature[0], 4) == 0) && Image::_webp_mem_loader_func) {914parsed_image = Image::_webp_mem_loader_func(r, len);915} else if ((memcmp(&r[0], &bmp_signature[0], 2) == 0) && Image::_bmp_mem_loader_func) {916parsed_image = Image::_bmp_mem_loader_func(r, len);917} else if (Image::_svg_scalable_mem_loader_func) {918parsed_image = Image::_svg_scalable_mem_loader_func(r, len, 1.0);919}920921if (parsed_image.is_null()) {922if (is_print_verbose_enabled()) {923ERR_PRINT(vformat("Asset Library: Invalid image downloaded from '%s' for asset # %d", image_queue[p_queue_id].image_url, image_queue[p_queue_id].asset_id));924}925} else {926image->copy_internals_from(parsed_image);927}928}929930if (!image->is_empty()) {931switch (image_queue[p_queue_id].image_type) {932case IMAGE_QUEUE_ICON:933image->resize(64 * EDSCALE, 64 * EDSCALE, Image::INTERPOLATE_LANCZOS);934break;935936case IMAGE_QUEUE_THUMBNAIL: {937float max_height = 85 * EDSCALE;938939float scale_ratio = max_height / (image->get_height() * EDSCALE);940if (scale_ratio < 1) {941image->resize(image->get_width() * EDSCALE * scale_ratio, image->get_height() * EDSCALE * scale_ratio, Image::INTERPOLATE_LANCZOS);942}943} break;944945case IMAGE_QUEUE_SCREENSHOT: {946float max_height = 397 * EDSCALE;947948float scale_ratio = max_height / (image->get_height() * EDSCALE);949if (scale_ratio < 1) {950image->resize(image->get_width() * EDSCALE * scale_ratio, image->get_height() * EDSCALE * scale_ratio, Image::INTERPOLATE_LANCZOS);951}952} break;953}954955Ref<ImageTexture> tex = ImageTexture::create_from_image(image);956957obj->call("set_image", image_queue[p_queue_id].image_type, image_queue[p_queue_id].image_index, tex);958image_set = true;959}960961if (!image_set && p_final) {962obj->call("set_image", image_queue[p_queue_id].image_type, image_queue[p_queue_id].image_index, get_editor_theme_icon(SNAME("FileBrokenBigThumb")));963}964}965966void EditorAssetLibrary::_image_request_completed(int p_status, int p_code, const PackedStringArray &headers, const PackedByteArray &p_data, int p_queue_id) {967ERR_FAIL_COND(!image_queue.has(p_queue_id));968969if (p_status == HTTPRequest::RESULT_SUCCESS && p_code < HTTPClient::RESPONSE_BAD_REQUEST) {970if (p_code != HTTPClient::RESPONSE_NOT_MODIFIED) {971for (int i = 0; i < headers.size(); i++) {972if (headers[i].findn("ETag:") == 0) { // Save etag973String cache_filename_base = EditorPaths::get_singleton()->get_cache_dir().path_join("assetimage_" + image_queue[p_queue_id].image_url.md5_text());974String new_etag = headers[i].substr(headers[i].find_char(':') + 1).strip_edges();975Ref<FileAccess> file = FileAccess::open(cache_filename_base + ".etag", FileAccess::WRITE);976if (file.is_valid()) {977file->store_line(new_etag);978}979980int len = p_data.size();981const uint8_t *r = p_data.ptr();982file = FileAccess::open(cache_filename_base + ".data", FileAccess::WRITE);983if (file.is_valid()) {984file->store_32(len);985file->store_buffer(r, len);986}987988break;989}990}991}992_image_update(p_code == HTTPClient::RESPONSE_NOT_MODIFIED, true, p_data, p_queue_id);993994} else {995if (is_print_verbose_enabled()) {996WARN_PRINT(vformat("Asset Library: Error getting image from '%s' for asset # %d.", image_queue[p_queue_id].image_url, image_queue[p_queue_id].asset_id));997}998999Object *obj = ObjectDB::get_instance(image_queue[p_queue_id].target);1000if (obj) {1001obj->call("set_image", image_queue[p_queue_id].image_type, image_queue[p_queue_id].image_index, get_editor_theme_icon(SNAME("FileBrokenBigThumb")));1002}1003}10041005image_queue[p_queue_id].request->queue_free();1006image_queue.erase(p_queue_id);10071008_update_image_queue();1009}10101011void EditorAssetLibrary::_update_image_queue() {1012const int max_images = 6;1013int current_images = 0;10141015List<int> to_delete;1016for (KeyValue<int, ImageQueue> &E : image_queue) {1017if (!E.value.active && current_images < max_images) {1018String cache_filename_base = EditorPaths::get_singleton()->get_cache_dir().path_join("assetimage_" + E.value.image_url.md5_text());1019Vector<String> headers;10201021if (FileAccess::exists(cache_filename_base + ".etag") && FileAccess::exists(cache_filename_base + ".data")) {1022Ref<FileAccess> file = FileAccess::open(cache_filename_base + ".etag", FileAccess::READ);1023if (file.is_valid()) {1024headers.push_back("If-None-Match: " + file->get_line());1025}1026}10271028Error err = E.value.request->request(E.value.image_url, headers);1029if (err != OK) {1030to_delete.push_back(E.key);1031} else {1032E.value.active = true;1033}1034current_images++;1035} else if (E.value.active) {1036current_images++;1037}1038}10391040while (to_delete.size()) {1041image_queue[to_delete.front()->get()].request->queue_free();1042image_queue.erase(to_delete.front()->get());1043to_delete.pop_front();1044}1045}10461047void EditorAssetLibrary::_request_image(ObjectID p_for, int p_asset_id, String p_image_url, ImageType p_type, int p_image_index) {1048// Remove extra spaces around the URL. This isn't strictly valid, but recoverable.1049String trimmed_url = p_image_url.strip_edges();1050if (trimmed_url != p_image_url && is_print_verbose_enabled()) {1051WARN_PRINT(vformat("Asset Library: Badly formatted image URL '%s' for asset # %d.", p_image_url, p_asset_id));1052}10531054// Validate the image URL first.1055{1056String url_scheme;1057String url_host;1058int url_port;1059String url_path;1060String url_fragment;1061Error err = trimmed_url.parse_url(url_scheme, url_host, url_port, url_path, url_fragment);1062if (err != OK) {1063if (is_print_verbose_enabled()) {1064ERR_PRINT(vformat("Asset Library: Invalid image URL '%s' for asset # %d.", trimmed_url, p_asset_id));1065}10661067Object *obj = ObjectDB::get_instance(p_for);1068if (obj) {1069obj->call("set_image", p_type, p_image_index, get_editor_theme_icon(SNAME("FileBrokenBigThumb")));1070}1071return;1072}1073}10741075ImageQueue iq;1076iq.image_url = trimmed_url;1077iq.image_index = p_image_index;1078iq.image_type = p_type;1079iq.request = memnew(HTTPRequest);1080setup_http_request(iq.request);10811082iq.target = p_for;1083iq.asset_id = p_asset_id;1084iq.queue_id = ++last_queue_id;1085iq.active = false;10861087iq.request->connect("request_completed", callable_mp(this, &EditorAssetLibrary::_image_request_completed).bind(iq.queue_id));10881089image_queue[iq.queue_id] = iq;1090add_child(iq.request);10911092_image_update(true, false, PackedByteArray(), iq.queue_id);1093_update_image_queue();1094}10951096void EditorAssetLibrary::_repository_changed(int p_repository_id) {1097_set_library_message(TTRC("Loading..."));10981099asset_top_page->hide();1100asset_bottom_page->hide();1101asset_items->hide();11021103filter->set_editable(false);1104sort->set_disabled(true);1105categories->set_disabled(true);1106support->set_disabled(true);11071108host = repository->get_item_metadata(p_repository_id);1109if (templates_only) {1110_api_request("configure", REQUESTING_CONFIG, "?type=project");1111} else {1112_api_request("configure", REQUESTING_CONFIG);1113}1114}11151116void EditorAssetLibrary::_support_toggled(int p_support) {1117support->get_popup()->set_item_checked(p_support, !support->get_popup()->is_item_checked(p_support));1118_search();1119}11201121void EditorAssetLibrary::_rerun_search(int p_ignore) {1122_search();1123}11241125void EditorAssetLibrary::_search(int p_page) {1126String args;11271128if (templates_only) {1129args += "?type=project&";1130} else {1131args += "?";1132}1133args += String() + "sort=" + sort_key[sort->get_selected()];11341135// We use the "branch" version, i.e. major.minor, as patch releases should be compatible1136args += "&godot_version=" + String(GODOT_VERSION_BRANCH);11371138String support_list;1139for (int i = 0; i < SUPPORT_MAX; i++) {1140if (support->get_popup()->is_item_checked(i)) {1141support_list += String(support_key[i]) + "+";1142}1143}1144if (!support_list.is_empty()) {1145args += "&support=" + support_list.substr(0, support_list.length() - 1);1146}11471148if (categories->get_selected() > 0) {1149args += "&category=" + itos(categories->get_item_metadata(categories->get_selected()));1150}11511152// Sorting options with an odd index are always the reverse of the previous one1153if (sort->get_selected() % 2 == 1) {1154args += "&reverse=true";1155}11561157if (!filter->get_text().is_empty()) {1158args += "&filter=" + filter->get_text().uri_encode();1159}11601161if (p_page > 0) {1162args += "&page=" + itos(p_page);1163}11641165_api_request("asset", REQUESTING_SEARCH, args);1166}11671168void EditorAssetLibrary::_search_text_changed(const String &p_text) {1169filter_debounce_timer->start();1170}11711172void EditorAssetLibrary::_filter_debounce_timer_timeout() {1173_search();1174}11751176void EditorAssetLibrary::_request_current_config() {1177_repository_changed(repository->get_selected());1178}11791180HBoxContainer *EditorAssetLibrary::_make_pages(int p_page, int p_page_count, int p_page_len, int p_total_items, int p_current_items) {1181HBoxContainer *hbc = memnew(HBoxContainer);11821183if (p_page_count < 2) {1184return hbc;1185}11861187//do the mario1188int from = p_page - (5 / EDSCALE);1189if (from < 0) {1190from = 0;1191}1192int to = from + (10 / EDSCALE);1193if (to > p_page_count) {1194to = p_page_count;1195}11961197hbc->add_spacer();1198hbc->add_theme_constant_override("separation", 5 * EDSCALE);11991200Button *first = memnew(Button);1201first->set_text(TTR("First", "Pagination"));1202first->set_theme_type_variation("PanelBackgroundButton");1203if (p_page != 0) {1204first->connect(SceneStringName(pressed), callable_mp(this, &EditorAssetLibrary::_search).bind(0));1205} else {1206first->set_disabled(true);1207first->set_focus_mode(Control::FOCUS_ACCESSIBILITY);1208}1209hbc->add_child(first);12101211Button *prev = memnew(Button);1212prev->set_text(TTR("Previous", "Pagination"));1213prev->set_theme_type_variation("PanelBackgroundButton");1214if (p_page > 0) {1215prev->connect(SceneStringName(pressed), callable_mp(this, &EditorAssetLibrary::_search).bind(p_page - 1));1216} else {1217prev->set_disabled(true);1218prev->set_focus_mode(Control::FOCUS_ACCESSIBILITY);1219}1220hbc->add_child(prev);1221hbc->add_child(memnew(VSeparator));12221223for (int i = from; i < to; i++) {1224Button *current = memnew(Button);1225// Add padding to make page number buttons easier to click.1226current->set_text(vformat(" %d ", i + 1));1227current->set_theme_type_variation("PanelBackgroundButton");1228if (i == p_page) {1229current->set_disabled(true);1230current->set_focus_mode(Control::FOCUS_ACCESSIBILITY);1231} else {1232current->connect(SceneStringName(pressed), callable_mp(this, &EditorAssetLibrary::_search).bind(i));1233}1234hbc->add_child(current);1235}12361237Button *next = memnew(Button);1238next->set_text(TTR("Next", "Pagination"));1239next->set_theme_type_variation("PanelBackgroundButton");1240if (p_page < p_page_count - 1) {1241next->connect(SceneStringName(pressed), callable_mp(this, &EditorAssetLibrary::_search).bind(p_page + 1));1242} else {1243next->set_disabled(true);1244next->set_focus_mode(Control::FOCUS_ACCESSIBILITY);1245}1246hbc->add_child(memnew(VSeparator));1247hbc->add_child(next);12481249Button *last = memnew(Button);1250last->set_text(TTR("Last", "Pagination"));1251last->set_theme_type_variation("PanelBackgroundButton");1252if (p_page != p_page_count - 1) {1253last->connect(SceneStringName(pressed), callable_mp(this, &EditorAssetLibrary::_search).bind(p_page_count - 1));1254} else {1255last->set_disabled(true);1256last->set_focus_mode(Control::FOCUS_ACCESSIBILITY);1257}1258hbc->add_child(last);12591260hbc->add_spacer();12611262return hbc;1263}12641265void EditorAssetLibrary::_api_request(const String &p_request, RequestType p_request_type, const String &p_arguments) {1266if (requesting != REQUESTING_NONE) {1267request->cancel_request();1268}1269error_hb->hide();12701271if (loading_blocked) {1272_set_library_message_with_action(TTRC("The Asset Library requires an online connection and involves sending data over the internet."), TTRC("Go Online"), callable_mp(this, &EditorAssetLibrary::_force_online_mode));1273return;1274}12751276requesting = p_request_type;1277request->request(host + "/" + p_request + p_arguments);1278}12791280void EditorAssetLibrary::_http_request_completed(int p_status, int p_code, const PackedStringArray &headers, const PackedByteArray &p_data) {1281String str = String::utf8((const char *)p_data.ptr(), (int)p_data.size());1282bool error_abort = true;12831284switch (p_status) {1285case HTTPRequest::RESULT_CANT_RESOLVE: {1286error_label->set_text(TTR("Can't resolve hostname:") + " " + host);1287} break;1288case HTTPRequest::RESULT_BODY_SIZE_LIMIT_EXCEEDED:1289case HTTPRequest::RESULT_CONNECTION_ERROR:1290case HTTPRequest::RESULT_CHUNKED_BODY_SIZE_MISMATCH: {1291error_label->set_text(TTR("Connection error, please try again."));1292} break;1293case HTTPRequest::RESULT_TLS_HANDSHAKE_ERROR:1294case HTTPRequest::RESULT_CANT_CONNECT: {1295error_label->set_text(TTR("Can't connect to host:") + " " + host);1296} break;1297case HTTPRequest::RESULT_NO_RESPONSE: {1298error_label->set_text(TTR("No response from host:") + " " + host);1299} break;1300case HTTPRequest::RESULT_REQUEST_FAILED: {1301error_label->set_text(TTR("Request failed, return code:") + " " + itos(p_code));1302} break;1303case HTTPRequest::RESULT_REDIRECT_LIMIT_REACHED: {1304error_label->set_text(TTRC("Request failed, too many redirects"));13051306} break;1307default: {1308if (p_code != 200) {1309error_label->set_text(TTR("Request failed, return code:") + " " + itos(p_code));1310} else {1311error_abort = false;1312}1313} break;1314}13151316if (error_abort) {1317if (requesting == REQUESTING_CONFIG) {1318_set_library_message_with_action(TTRC("Failed to get repository configuration."), TTRC("Retry"), callable_mp(this, &EditorAssetLibrary::_request_current_config));1319}1320error_hb->show();1321return;1322}13231324Dictionary d;1325{1326JSON json;1327json.parse(str);1328d = json.get_data();1329}13301331RequestType requested = requesting;1332requesting = REQUESTING_NONE;13331334switch (requested) {1335case REQUESTING_CONFIG: {1336categories->clear();1337categories->add_item(TTRC("All"));1338categories->set_item_metadata(0, 0);1339if (d.has("categories")) {1340Array clist = d["categories"];1341for (int i = 0; i < clist.size(); i++) {1342Dictionary cat = clist[i];1343if (!cat.has("name") || !cat.has("id")) {1344continue;1345}1346String name = cat["name"];1347int id = cat["id"];1348categories->add_item(name);1349categories->set_item_metadata(-1, id);1350category_map[cat["id"]] = name;1351}1352}13531354filter->set_editable(true);1355sort->set_disabled(false);1356categories->set_disabled(false);1357support->set_disabled(false);13581359_search();1360} break;1361case REQUESTING_SEARCH: {1362initial_loading = false;13631364if (asset_items) {1365memdelete(asset_items);1366}13671368if (asset_top_page) {1369memdelete(asset_top_page);1370}13711372if (asset_bottom_page) {1373memdelete(asset_bottom_page);1374}13751376int page = 0;1377int pages = 1;1378int page_len = 10;1379int total_items = 1;1380Array result;13811382if (d.has("page")) {1383page = d["page"];1384}1385if (d.has("pages")) {1386pages = d["pages"];1387}1388if (d.has("page_length")) {1389page_len = d["page_length"];1390}1391if (d.has("total")) {1392total_items = d["total"];1393}1394if (d.has("result")) {1395result = d["result"];1396}13971398asset_top_page = _make_pages(page, pages, page_len, total_items, result.size());1399library_vb->add_child(asset_top_page);14001401asset_items = memnew(GridContainer);1402_update_asset_items_columns();1403asset_items->add_theme_constant_override("h_separation", 10 * EDSCALE);1404asset_items->add_theme_constant_override("v_separation", 10 * EDSCALE);14051406library_vb->add_child(asset_items);14071408asset_bottom_page = _make_pages(page, pages, page_len, total_items, result.size());1409library_vb->add_child(asset_bottom_page);14101411if (result.is_empty()) {1412String support_list;1413for (int i = 0; i < SUPPORT_MAX; i++) {1414if (support->get_popup()->is_item_checked(i)) {1415if (!support_list.is_empty()) {1416support_list += ", ";1417}1418support_list += TTRGET(support_text[i]);1419}1420}1421if (support_list.is_empty()) {1422support_list = "-";1423}14241425if (!filter->get_text().is_empty()) {1426_set_library_message(1427vformat(TTR("No results for \"%s\" for support level(s): %s."), filter->get_text(), support_list));1428} else {1429// No results, even though the user didn't search for anything specific.1430// This is typically because the version number changed recently1431// and no assets compatible with the new version have been published yet.1432_set_library_message(1433vformat(TTR("No results compatible with %s %s for support level(s): %s.\nCheck the enabled support levels using the 'Support' button in the top-right corner."), String(GODOT_VERSION_SHORT_NAME).capitalize(), String(GODOT_VERSION_BRANCH), support_list));1434}1435} else {1436library_message_box->hide();1437}14381439for (int i = 0; i < result.size(); i++) {1440Dictionary r = result[i];14411442ERR_CONTINUE(!r.has("title"));1443ERR_CONTINUE(!r.has("asset_id"));1444ERR_CONTINUE(!r.has("author"));1445ERR_CONTINUE(!r.has("author_id"));1446ERR_CONTINUE(!r.has("category_id"));1447ERR_FAIL_COND(!category_map.has(r["category_id"]));1448ERR_CONTINUE(!r.has("cost"));14491450EditorAssetLibraryItem *item = memnew(EditorAssetLibraryItem(true));1451asset_items->add_child(item);1452asset_items->connect(SceneStringName(sort_children), callable_mp(item, &EditorAssetLibraryItem::calculate_misc_links_ratio));1453item->configure(r["title"], r["asset_id"], category_map[r["category_id"]], r["category_id"], r["author"], r["author_id"], r["cost"]);1454item->connect("asset_selected", callable_mp(this, &EditorAssetLibrary::_select_asset));1455item->connect("author_selected", callable_mp(this, &EditorAssetLibrary::_select_author));1456item->connect("category_selected", callable_mp(this, &EditorAssetLibrary::_select_category));14571458if (r.has("icon_url") && !r["icon_url"].operator String().is_empty()) {1459_request_image(item->get_instance_id(), r["asset_id"], r["icon_url"], IMAGE_QUEUE_ICON, 0);1460}1461}14621463if (!result.is_empty()) {1464library_scroll->set_v_scroll(0);1465}1466} break;1467case REQUESTING_ASSET: {1468Dictionary r = d;14691470ERR_FAIL_COND(!r.has("title"));1471ERR_FAIL_COND(!r.has("asset_id"));1472ERR_FAIL_COND(!r.has("author"));1473ERR_FAIL_COND(!r.has("author_id"));1474ERR_FAIL_COND(!r.has("version"));1475ERR_FAIL_COND(!r.has("version_string"));1476ERR_FAIL_COND(!r.has("category_id"));1477ERR_FAIL_COND(!category_map.has(r["category_id"]));1478ERR_FAIL_COND(!r.has("cost"));1479ERR_FAIL_COND(!r.has("description"));1480ERR_FAIL_COND(!r.has("download_url"));1481ERR_FAIL_COND(!r.has("download_hash"));1482ERR_FAIL_COND(!r.has("browse_url"));14831484if (description) {1485memdelete(description);1486}14871488description = memnew(EditorAssetLibraryItemDescription);1489add_child(description);1490description->connect(SceneStringName(confirmed), callable_mp(this, &EditorAssetLibrary::_install_asset));14911492description->configure(r["title"], r["asset_id"], category_map[r["category_id"]], r["category_id"], r["author"], r["author_id"], r["cost"], r["version"], r["version_string"], r["description"], r["download_url"], r["browse_url"], r["download_hash"]);14931494EditorAssetLibraryItemDownload *download_item = _get_asset_in_progress(description->get_asset_id());1495if (download_item) {1496if (download_item->can_install()) {1497description->set_ok_button_text(TTRC("Install"));1498description->get_ok_button()->set_disabled(false);1499} else {1500description->set_ok_button_text(TTRC("Downloading..."));1501description->get_ok_button()->set_disabled(true);1502}1503} else {1504description->set_ok_button_text(TTRC("Download"));1505description->get_ok_button()->set_disabled(false);1506}15071508if (r.has("icon_url") && !r["icon_url"].operator String().is_empty()) {1509_request_image(description->get_instance_id(), r["asset_id"], r["icon_url"], IMAGE_QUEUE_ICON, 0);1510}15111512if (d.has("previews")) {1513Array previews = d["previews"];15141515for (int i = 0; i < previews.size(); i++) {1516Dictionary p = previews[i];15171518ERR_CONTINUE(!p.has("type"));1519ERR_CONTINUE(!p.has("link"));15201521bool is_video = p.has("type") && String(p["type"]) == "video";1522String video_url;1523if (is_video && p.has("link")) {1524video_url = p["link"];1525}15261527description->add_preview(i, is_video, video_url);15281529if (p.has("thumbnail")) {1530_request_image(description->get_instance_id(), r["asset_id"], p["thumbnail"], IMAGE_QUEUE_THUMBNAIL, i);1531}15321533if (!is_video) {1534_request_image(description->get_instance_id(), r["asset_id"], p["link"], IMAGE_QUEUE_SCREENSHOT, i);1535}1536}1537}15381539description->popup_centered();1540} break;1541default:1542break;1543}1544}15451546void EditorAssetLibrary::_asset_file_selected(const String &p_file) {1547if (asset_installer) {1548memdelete(asset_installer);1549asset_installer = nullptr;1550}15511552asset_installer = memnew(EditorAssetInstaller);1553asset_installer->set_asset_name(p_file);1554add_child(asset_installer);1555asset_installer->open_asset(p_file);1556}15571558void EditorAssetLibrary::_asset_open() {1559asset_open->popup_file_dialog();1560}15611562void EditorAssetLibrary::_manage_plugins() {1563ProjectSettingsEditor::get_singleton()->popup_project_settings(true);1564ProjectSettingsEditor::get_singleton()->set_plugins_page();1565}15661567EditorAssetLibraryItemDownload *EditorAssetLibrary::_get_asset_in_progress(int p_asset_id) const {1568for (int i = 0; i < downloads_hb->get_child_count(); i++) {1569EditorAssetLibraryItemDownload *d = Object::cast_to<EditorAssetLibraryItemDownload>(downloads_hb->get_child(i));1570if (d && d->get_asset_id() == p_asset_id) {1571return d;1572}1573}15741575return nullptr;1576}15771578void EditorAssetLibrary::_install_external_asset(String p_zip_path, String p_title) {1579emit_signal(SNAME("install_asset"), p_zip_path, p_title);1580}15811582void EditorAssetLibrary::_update_asset_items_columns() {1583int new_columns = get_size().x / (450.0 * EDSCALE);1584new_columns = MAX(1, new_columns);15851586if (new_columns != asset_items->get_columns()) {1587asset_items->set_columns(new_columns);1588}1589}15901591void EditorAssetLibrary::_set_library_message(const String &p_message) {1592library_message->set_text(p_message);15931594if (library_message_action.is_valid()) {1595library_message_button->disconnect(SceneStringName(pressed), library_message_action);1596library_message_action = Callable();1597}1598library_message_button->hide();15991600library_message_box->show();1601}16021603void EditorAssetLibrary::_set_library_message_with_action(const String &p_message, const String &p_action_text, const Callable &p_action) {1604library_message->set_text(p_message);16051606library_message_button->set_text(p_action_text);1607if (library_message_action.is_valid()) {1608library_message_button->disconnect(SceneStringName(pressed), library_message_action);1609library_message_action = Callable();1610}1611library_message_action = p_action;1612library_message_button->connect(SceneStringName(pressed), library_message_action);1613library_message_button->show();16141615library_message_box->show();1616}16171618void EditorAssetLibrary::_force_online_mode() {1619EditorSettings::get_singleton()->set_setting("network/connection/network_mode", EditorSettings::NETWORK_ONLINE);1620EditorSettings::get_singleton()->notify_changes();1621EditorSettings::get_singleton()->save();1622}16231624void EditorAssetLibrary::disable_community_support() {1625support->get_popup()->set_item_checked(SUPPORT_COMMUNITY, false);1626}16271628void EditorAssetLibrary::_bind_methods() {1629ADD_SIGNAL(MethodInfo("install_asset", PropertyInfo(Variant::STRING, "zip_path"), PropertyInfo(Variant::STRING, "name")));1630}16311632EditorAssetLibrary::EditorAssetLibrary(bool p_templates_only) {1633requesting = REQUESTING_NONE;1634templates_only = p_templates_only;1635loading_blocked = ((int)EDITOR_GET("network/connection/network_mode") == EditorSettings::NETWORK_OFFLINE);16361637VBoxContainer *library_main = memnew(VBoxContainer);1638add_child(library_main);16391640HBoxContainer *search_hb = memnew(HBoxContainer);16411642library_main->add_child(search_hb);1643library_main->add_theme_constant_override("separation", 10 * EDSCALE);16441645filter = memnew(LineEdit);1646if (templates_only) {1647filter->set_placeholder(TTRC("Search Templates, Projects, and Demos"));1648} else {1649filter->set_placeholder(TTRC("Search Assets (Excluding Templates, Projects, and Demos)"));1650}1651filter->set_clear_button_enabled(true);1652search_hb->add_child(filter);1653filter->set_h_size_flags(Control::SIZE_EXPAND_FILL);1654filter->connect(SceneStringName(text_changed), callable_mp(this, &EditorAssetLibrary::_search_text_changed));16551656// Perform a search automatically if the user hasn't entered any text for a certain duration.1657// This way, the user doesn't need to press Enter to initiate their search.1658filter_debounce_timer = memnew(Timer);1659filter_debounce_timer->set_one_shot(true);1660filter_debounce_timer->set_wait_time(0.25);1661filter_debounce_timer->connect("timeout", callable_mp(this, &EditorAssetLibrary::_filter_debounce_timer_timeout));1662search_hb->add_child(filter_debounce_timer);16631664if (!p_templates_only) {1665search_hb->add_child(memnew(VSeparator));1666}16671668Button *open_asset = memnew(Button);1669open_asset->set_text(TTRC("Import..."));1670search_hb->add_child(open_asset);1671open_asset->connect(SceneStringName(pressed), callable_mp(this, &EditorAssetLibrary::_asset_open));16721673Button *plugins = memnew(Button);1674plugins->set_text(TTRC("Plugins..."));1675search_hb->add_child(plugins);1676plugins->connect(SceneStringName(pressed), callable_mp(this, &EditorAssetLibrary::_manage_plugins));16771678if (p_templates_only) {1679open_asset->hide();1680plugins->hide();1681}16821683HBoxContainer *search_hb2 = memnew(HBoxContainer);1684library_main->add_child(search_hb2);16851686search_hb2->add_child(memnew(Label(TTRC("Sort:"))));1687sort = memnew(OptionButton);1688for (int i = 0; i < SORT_MAX; i++) {1689sort->add_item(sort_text[i]);1690}16911692search_hb2->add_child(sort);16931694sort->set_h_size_flags(Control::SIZE_EXPAND_FILL);1695sort->set_clip_text(true);1696sort->connect(SceneStringName(item_selected), callable_mp(this, &EditorAssetLibrary::_rerun_search));16971698search_hb2->add_child(memnew(VSeparator));16991700search_hb2->add_child(memnew(Label(TTRC("Category:"))));1701categories = memnew(OptionButton);1702categories->add_item(TTRC("All"));1703search_hb2->add_child(categories);1704categories->set_h_size_flags(Control::SIZE_EXPAND_FILL);1705categories->set_clip_text(true);1706categories->connect(SceneStringName(item_selected), callable_mp(this, &EditorAssetLibrary::_rerun_search));17071708search_hb2->add_child(memnew(VSeparator));17091710search_hb2->add_child(memnew(Label(TTRC("Site:"))));1711repository = memnew(OptionButton);17121713_update_repository_options();17141715repository->connect(SceneStringName(item_selected), callable_mp(this, &EditorAssetLibrary::_repository_changed));17161717search_hb2->add_child(repository);1718repository->set_h_size_flags(Control::SIZE_EXPAND_FILL);1719repository->set_clip_text(true);17201721search_hb2->add_child(memnew(VSeparator));17221723support = memnew(MenuButton);1724search_hb2->add_child(support);1725support->set_text(TTRC("Support"));1726support->get_popup()->set_hide_on_checkable_item_selection(false);1727support->get_popup()->add_check_item(support_text[SUPPORT_FEATURED], SUPPORT_FEATURED);1728support->get_popup()->add_check_item(support_text[SUPPORT_COMMUNITY], SUPPORT_COMMUNITY);1729support->get_popup()->add_check_item(support_text[SUPPORT_TESTING], SUPPORT_TESTING);1730support->get_popup()->set_item_checked(SUPPORT_FEATURED, true);1731support->get_popup()->set_item_checked(SUPPORT_COMMUNITY, true);1732support->get_popup()->connect(SceneStringName(id_pressed), callable_mp(this, &EditorAssetLibrary::_support_toggled));17331734/////////17351736library_mc = memnew(MarginContainer);1737library_mc->set_theme_type_variation(Engine::get_singleton()->is_project_manager_hint() ? "NoBorderAssetLibProjectManager" : "NoBorderAssetLib");1738library_mc->set_v_size_flags(Control::SIZE_EXPAND_FILL);1739library_main->add_child(library_mc);17401741library_scroll = memnew(ScrollContainer);1742library_scroll->set_scroll_hint_mode(ScrollContainer::SCROLL_HINT_MODE_TOP_AND_LEFT);1743library_scroll->set_horizontal_scroll_mode(ScrollContainer::SCROLL_MODE_DISABLED);1744library_mc->add_child(library_scroll);17451746Ref<StyleBoxEmpty> border2;1747border2.instantiate();1748border2->set_content_margin_individual(15 * EDSCALE, 15 * EDSCALE, 35 * EDSCALE, 15 * EDSCALE);17491750PanelContainer *library_vb_border = memnew(PanelContainer);1751library_scroll->add_child(library_vb_border);1752library_vb_border->add_theme_style_override(SceneStringName(panel), border2);1753library_vb_border->set_h_size_flags(Control::SIZE_EXPAND_FILL);17541755library_vb = memnew(VBoxContainer);1756library_vb->set_h_size_flags(Control::SIZE_EXPAND_FILL);17571758library_vb_border->add_child(library_vb);17591760library_message_box = memnew(VBoxContainer);1761library_message_box->hide();1762library_vb->add_child(library_message_box);17631764library_message = memnew(Label);1765library_message->set_focus_mode(FOCUS_ACCESSIBILITY);1766library_message->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);1767library_message_box->add_child(library_message);17681769library_message_button = memnew(Button);1770library_message_button->set_h_size_flags(SIZE_SHRINK_CENTER);1771library_message_button->set_theme_type_variation("PanelBackgroundButton");1772library_message_box->add_child(library_message_button);17731774asset_top_page = memnew(HBoxContainer);1775library_vb->add_child(asset_top_page);17761777asset_items = memnew(GridContainer);1778_update_asset_items_columns();1779asset_items->add_theme_constant_override("h_separation", 10 * EDSCALE);1780asset_items->add_theme_constant_override("v_separation", 10 * EDSCALE);17811782library_vb->add_child(asset_items);17831784asset_bottom_page = memnew(HBoxContainer);1785library_vb->add_child(asset_bottom_page);17861787request = memnew(HTTPRequest);1788add_child(request);1789setup_http_request(request);1790request->connect("request_completed", callable_mp(this, &EditorAssetLibrary::_http_request_completed));17911792last_queue_id = 0;17931794library_vb->add_theme_constant_override("separation", 20 * EDSCALE);17951796error_hb = memnew(HBoxContainer);1797library_main->add_child(error_hb);1798error_label = memnew(Label);1799error_label->set_focus_mode(FOCUS_ACCESSIBILITY);1800error_hb->add_child(error_label);1801error_tr = memnew(TextureRect);1802error_tr->set_v_size_flags(Control::SIZE_SHRINK_CENTER);1803error_hb->add_child(error_tr);18041805description = nullptr;18061807set_process(true);1808set_process_shortcut_input(true); // Global shortcuts since there is no main element to be focused.18091810downloads_scroll = memnew(ScrollContainer);1811downloads_scroll->set_vertical_scroll_mode(ScrollContainer::SCROLL_MODE_DISABLED);1812downloads_scroll->set_theme_type_variation("ScrollContainerSecondary");1813library_main->add_child(downloads_scroll);1814downloads_hb = memnew(HBoxContainer);1815downloads_scroll->add_child(downloads_hb);18161817asset_open = memnew(EditorFileDialog);18181819asset_open->set_access(EditorFileDialog::ACCESS_FILESYSTEM);1820asset_open->add_filter("*.zip", TTRC("Assets ZIP File"));1821asset_open->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_FILE);1822add_child(asset_open);1823asset_open->connect("file_selected", callable_mp(this, &EditorAssetLibrary::_asset_file_selected));18241825asset_installer = nullptr;1826}18271828///////18291830bool AssetLibraryEditorPlugin::is_available() {1831#ifdef WEB_ENABLED1832// Asset Library can't work on Web editor for now as most assets are sourced1833// directly from GitHub which does not set CORS.1834return false;1835#else1836return StreamPeerTLS::is_available() && !Engine::get_singleton()->is_recovery_mode_hint();1837#endif1838}18391840void AssetLibraryEditorPlugin::make_visible(bool p_visible) {1841if (p_visible) {1842addon_library->show();1843} else {1844addon_library->hide();1845}1846}18471848AssetLibraryEditorPlugin::AssetLibraryEditorPlugin() {1849addon_library = memnew(EditorAssetLibrary);1850addon_library->set_v_size_flags(Control::SIZE_EXPAND_FILL);1851EditorNode::get_singleton()->get_editor_main_screen()->get_control()->add_child(addon_library);1852addon_library->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT);1853addon_library->hide();1854}185518561857