Path: blob/master/editor/inspector/editor_resource_picker.cpp
20852 views
/**************************************************************************/1/* editor_resource_picker.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 "editor_resource_picker.h"3132#include "core/input/input.h"33#include "editor/audio/audio_stream_preview.h"34#include "editor/doc/editor_help.h"35#include "editor/docks/filesystem_dock.h"36#include "editor/docks/scene_tree_dock.h"37#include "editor/editor_node.h"38#include "editor/editor_string_names.h"39#include "editor/gui/editor_file_dialog.h"40#include "editor/gui/editor_quick_open_dialog.h"41#include "editor/inspector/editor_inspector.h"42#include "editor/inspector/editor_resource_preview.h"43#include "editor/plugins/editor_resource_conversion_plugin.h"44#include "editor/script/script_editor_plugin.h"45#include "editor/settings/editor_settings.h"46#include "editor/themes/editor_scale.h"47#include "scene/gui/button.h"48#include "scene/gui/texture_rect.h"49#include "scene/property_utils.h"50#include "scene/resources/gradient_texture.h"51#include "scene/resources/image_texture.h"5253static bool _has_sub_resources(const Ref<Resource> &p_res) {54List<PropertyInfo> property_list;55p_res->get_property_list(&property_list);56for (const PropertyInfo &p : property_list) {57Variant value = p_res->get(p.name);58if (p.type == Variant::OBJECT && p.hint == PROPERTY_HINT_RESOURCE_TYPE && !(p.usage & PROPERTY_USAGE_NEVER_DUPLICATE) && p_res->get(p.name).get_validated_object()) {59return true;60} else if (p.type == Variant::ARRAY) {61Array arr = value;62for (Variant &var : arr) {63Ref<Resource> res = var;64if (res.is_valid()) {65return true;66}67}68} else if (p.type == Variant::DICTIONARY) {69Dictionary dict = value;70for (const KeyValue<Variant, Variant> &kv : dict) {71Ref<Resource> resk = kv.key;72Ref<Resource> resv = kv.value;73if (resk.is_valid() || resv.is_valid()) {74return true;75}76}77}78}79return false;80}8182void EditorResourcePicker::_update_resource() {83String resource_path;84if (edited_resource.is_valid() && edited_resource->get_path().is_resource_file()) {85resource_path = edited_resource->get_path() + "\n";86}87String class_name = _get_resource_type(edited_resource);8889if (preview_rect) {90preview_rect->set_texture(Ref<Texture2D>());9192assign_button->set_custom_minimum_size(assign_button_min_size);93if (edited_resource == Ref<Resource>()) {94assign_button->set_button_icon(Ref<Texture2D>());95assign_button->set_text(TTR("<empty>"));96assign_button->set_tooltip_text("");97} else {98assign_button->set_button_icon(EditorNode::get_singleton()->get_object_icon(edited_resource.operator->()));99100if (!edited_resource->get_name().is_empty()) {101assign_button->set_text(edited_resource->get_name());102} else if (edited_resource->get_path().is_resource_file()) {103assign_button->set_text(edited_resource->get_path().get_file());104} else {105assign_button->set_text(class_name);106}107108if (edited_resource->get_path().is_resource_file()) {109resource_path = edited_resource->get_path() + "\n";110}111assign_button->set_tooltip_text(resource_path + TTR("Type:") + " " + class_name);112113// Preview will override the above, so called at the end.114EditorResourcePreview::get_singleton()->queue_edited_resource_preview(edited_resource, callable_mp(this, &EditorResourcePicker::_update_resource_preview).bind(edited_resource->get_instance_id()));115}116} else if (edited_resource.is_valid()) {117assign_button->set_tooltip_text(resource_path + TTR("Type:") + " " + edited_resource->get_class());118}119120if (edited_resource.is_null()) {121make_unique_button->set_visible(false);122} else {123Ref<Resource> parent_res = _has_parent_resource();124bool unique_enable = _is_uniqueness_enabled();125bool unique_recursive_enabled = _is_uniqueness_enabled(true);126bool is_internal = EditorNode::get_singleton()->is_resource_internal_to_scene(edited_resource);127int num_of_copies = EditorNode::get_singleton()->get_resource_count(edited_resource);128make_unique_button->set_button_icon(get_editor_theme_icon(SNAME("Instance")));129make_unique_button->set_visible(num_of_copies > 1 || (!is_internal && !Object::cast_to<Script>(edited_resource.ptr())));130make_unique_button->set_disabled((!unique_enable && !unique_recursive_enabled) || !editable);131132String tooltip;133134String resource_name = "resource";135if (edited_resource.is_valid()) {136resource_name = edited_resource->get_class();137138if (edited_resource->has_meta(SceneStringName(_custom_type_script))) {139const Ref<Script> custom_script = PropertyUtils::get_custom_type_script(edited_resource.ptr());140if (custom_script.is_valid()) {141const String global_name = custom_script->get_global_name();142if (!global_name.is_empty()) {143resource_name = global_name;144}145}146}147}148149if (num_of_copies > 1) {150tooltip = vformat(TTRN("This %s is used in %d place.", "This %s is used in %d places.", num_of_copies), resource_name, num_of_copies);151} else if (!is_internal) {152tooltip = vformat(TTR("This %s is external to scene."), resource_name);153}154155if (!editable) {156tooltip += "\n" + vformat(TTR("The %s cannot be edited in the inspector and can't be made unique directly."), resource_name) + "\n";157} else {158if (unique_enable) {159tooltip += "\n" + TTR("Left-click to make it unique.") + "\n";160}161162if (unique_recursive_enabled) {163tooltip += TTR("It is possible to make its subresources unique.") + "\n" + TTR("Right-click to make them unique.");164}165166if (!unique_enable && EditorNode::get_singleton()->get_editor_selection()->get_full_selected_node_list().size() == 1) {167tooltip += TTR("In order to duplicate it, make its parent Resource unique.") + "\n";168}169}170171make_unique_button->set_tooltip_text(tooltip);172}173174assign_button->set_disabled(!editable && edited_resource.is_null());175quick_load_button->set_visible(editable && edited_resource.is_null());176}177178void EditorResourcePicker::_update_resource_preview(const String &p_path, const Ref<Texture2D> &p_preview, const Ref<Texture2D> &p_small_preview, ObjectID p_obj) {179if (edited_resource.is_null() || edited_resource->get_instance_id() != p_obj) {180return;181}182183if (preview_rect) {184Ref<Script> scr = edited_resource;185if (scr.is_valid()) {186assign_button->set_text(scr->get_path().get_file());187return;188}189190if (p_preview.is_valid()) {191int thumbnail_size = (int)EDITOR_GET("filesystem/file_dialog/thumbnail_size") * EDSCALE;192if (assign_button->get_button_icon().is_valid()) {193preview_rect->set_offset(SIDE_LEFT, assign_button->get_button_icon()->get_width() + assign_button->get_theme_stylebox(CoreStringName(normal))->get_content_margin(SIDE_LEFT) + get_theme_constant(SNAME("h_separation"), SNAME("Button")));194}195196// Resource-specific stretching.197if (Ref<GradientTexture1D>(edited_resource).is_valid() || Ref<Gradient>(edited_resource).is_valid()) {198preview_rect->set_stretch_mode(TextureRect::STRETCH_SCALE);199assign_button->set_custom_minimum_size(assign_button_min_size);200} else {201preview_rect->set_stretch_mode(TextureRect::STRETCH_KEEP_ASPECT_CENTERED);202assign_button->set_custom_minimum_size(assign_button_min_size.max(Size2(1, thumbnail_size)));203}204preview_rect->set_texture(p_preview);205assign_button->set_text("");206207if (preview_rect->get_size().x <= thumbnail_size) {208assign_button->set_button_icon(Ref<Texture2D>());209preview_rect->set_offset(SIDE_LEFT, 0);210}211}212}213}214215void EditorResourcePicker::_resource_selected() {216if (edited_resource.is_null()) {217edit_button->set_pressed(true);218_update_menu();219return;220}221222emit_signal(SNAME("resource_selected"), edited_resource, false);223}224225void EditorResourcePicker::_resource_changed() {226emit_signal(SNAME("resource_changed"), edited_resource);227_update_resource();228}229230void EditorResourcePicker::_file_selected(const String &p_path) {231Ref<Resource> loaded_resource = ResourceLoader::load(p_path);232ERR_FAIL_COND_MSG(loaded_resource.is_null(), "Cannot load resource from path '" + p_path + "'.");233234if (!base_type.is_empty()) {235bool any_type_matches = false;236237String res_type = loaded_resource->get_class();238Ref<Script> res_script = loaded_resource->get_script();239bool is_global_class = false;240if (res_script.is_valid()) {241String script_type = EditorNode::get_editor_data().script_class_get_name(res_script->get_path());242if (!script_type.is_empty()) {243is_global_class = true;244res_type = script_type;245}246}247248for (int i = 0; i < base_type.get_slice_count(","); i++) {249String base = base_type.get_slicec(',', i);250251any_type_matches = is_global_class ? EditorNode::get_editor_data().script_class_is_parent(res_type, base) : loaded_resource->is_class(base);252253if (any_type_matches) {254break;255}256}257258if (!any_type_matches) {259EditorNode::get_singleton()->show_warning(vformat(TTR("The selected resource (%s) does not match any type expected for this property (%s)."), res_type, base_type));260return;261}262}263264edited_resource = loaded_resource;265_resource_changed();266}267268void EditorResourcePicker::_resource_saved(Object *p_resource) {269if (edited_resource.is_valid() && p_resource == edited_resource.ptr()) {270emit_signal(SNAME("resource_changed"), edited_resource);271_update_resource();272}273}274275void EditorResourcePicker::_update_menu() {276if (edit_menu && edit_menu->is_visible()) {277edit_button->set_pressed(false);278edit_menu->hide();279return;280}281282_update_menu_items();283284Rect2 gt = edit_button->get_screen_rect();285edit_menu->reset_size();286int ms = edit_menu->get_contents_minimum_size().width;287Vector2 popup_pos = gt.get_end() - Vector2(ms, 0);288edit_menu->set_position(popup_pos);289edit_menu->popup();290}291292void EditorResourcePicker::_update_menu_items() {293_ensure_resource_menu();294edit_menu->clear();295296// Add options for creating specific subtypes of the base resource type.297if (is_editable()) {298set_create_options(edit_menu);299300// Add an option to load a resource from a file using the QuickOpen dialog.301edit_menu->add_icon_item(get_editor_theme_icon(SNAME("LoadQuick")), TTR("Quick Load..."), OBJ_MENU_QUICKLOAD);302edit_menu->set_item_tooltip(-1, TTR("Opens a quick menu to select from a list of allowed Resource files."));303304// Add an option to load a resource from a file using the regular file dialog.305edit_menu->add_icon_item(get_editor_theme_icon(SNAME("Load")), TTR("Load..."), OBJ_MENU_LOAD);306}307308// Add options for changing existing value of the resource.309if (edited_resource.is_valid()) {310// Determine if the edited resource is part of another scene (foreign) which was imported311bool is_edited_resource_foreign_import = EditorNode::get_singleton()->is_resource_read_only(edited_resource, true);312313// If the resource is determined to be foreign and imported, change the menu entry's description to 'inspect' rather than 'edit'314// since will only be able to view its properties in read-only mode.315if (is_edited_resource_foreign_import) {316// The 'Search' icon is a magnifying glass, which seems appropriate, but maybe a bespoke icon is preferred here.317edit_menu->add_icon_item(get_editor_theme_icon(SNAME("Search")), TTR("Inspect"), OBJ_MENU_INSPECT);318} else {319edit_menu->add_icon_item(get_editor_theme_icon(SNAME("Edit")), TTR("Edit"), OBJ_MENU_INSPECT);320}321322if (is_editable()) {323if (!_is_custom_type_script()) {324edit_menu->add_icon_item(get_editor_theme_icon(SNAME("Clear")), TTR("Clear"), OBJ_MENU_CLEAR);325}326bool unique_enabled = _is_uniqueness_enabled();327edit_menu->add_icon_item(get_editor_theme_icon(SNAME("Duplicate")), TTR("Make Unique"), OBJ_MENU_MAKE_UNIQUE);328edit_menu->set_item_disabled(-1, !unique_enabled);329330String modifier = "Ctrl";331if (OS::get_singleton()->has_feature("macos") || OS::get_singleton()->has_feature("web_macos") || OS::get_singleton()->has_feature("web_ios")) {332modifier = "Cmd";333}334const String drag_and_drop_text = vformat(TTR("Hold %s while drag-and-dropping from the FileSystem dock or another resource picker to automatically make a dropped resource unique."), modifier);335336if (!unique_enabled) {337if (EditorNode::get_singleton()->is_resource_internal_to_scene(edited_resource) && EditorNode::get_singleton()->get_resource_count(edited_resource) == 1) {338edit_menu->set_item_tooltip(-1, TTR("This Resource is already unique.") + "\n" + drag_and_drop_text);339} else if (_has_parent_resource().is_valid()) {340edit_menu->set_item_tooltip(-1, TTR("In order to duplicate it, make its parent Resource unique.") + "\n" + drag_and_drop_text);341}342} else {343edit_menu->set_item_tooltip(-1, drag_and_drop_text);344}345346if (_has_sub_resources(edited_resource)) {347unique_enabled = _is_uniqueness_enabled(true);348edit_menu->add_icon_item(get_editor_theme_icon(SNAME("Duplicate")), TTR("Make Unique (Recursive)"), OBJ_MENU_MAKE_UNIQUE_RECURSIVE);349edit_menu->set_item_disabled(-1, !unique_enabled);350if (!unique_enabled) {351Ref<Resource> parent_res = _has_parent_resource();352if (EditorNode::get_singleton()->get_editor_selection()->get_full_selected_node_list().size() == 1) {353edit_menu->set_item_tooltip(-1, (parent_res.is_valid() && EditorNode::get_singleton()->get_resource_count(parent_res) > 1) ? TTRC("In order to duplicate recursively, make its parent Resource unique.") : TTRC("Subresources have already been made unique."));354}355}356}357358edit_menu->add_icon_item(get_editor_theme_icon(SNAME("Save")), TTR("Save"), OBJ_MENU_SAVE);359edit_menu->add_icon_item(get_editor_theme_icon(SNAME("Save")), TTR("Save As..."), OBJ_MENU_SAVE_AS);360}361362if (edited_resource->get_path().is_resource_file()) {363edit_menu->add_separator();364edit_menu->add_icon_item(get_editor_theme_icon(SNAME("ShowInFileSystem")), TTR("Show in FileSystem"), OBJ_MENU_SHOW_IN_FILE_SYSTEM);365}366}367368// Add options to copy/paste resource.369Ref<Resource> cb = EditorSettings::get_singleton()->get_resource_clipboard();370bool paste_valid = false;371if (is_editable() && cb.is_valid()) {372if (base_type.is_empty()) {373paste_valid = true;374} else {375String res_type = _get_resource_type(cb);376377for (int i = 0; i < base_type.get_slice_count(","); i++) {378String base = base_type.get_slicec(',', i);379380paste_valid = ClassDB::is_parent_class(res_type, base) || EditorNode::get_editor_data().script_class_is_parent(res_type, base);381382if (paste_valid) {383break;384}385}386}387}388389if (edited_resource.is_valid() || paste_valid) {390edit_menu->add_separator();391392if (edited_resource.is_valid()) {393edit_menu->add_item(TTRC("Copy"), OBJ_MENU_COPY);394}395396if (paste_valid) {397edit_menu->add_item(TTRC("Paste"), OBJ_MENU_PASTE);398edit_menu->add_item(TTRC("Paste as Unique"), OBJ_MENU_PASTE_AS_UNIQUE);399}400}401402// Add options to convert existing resource to another type of resource.403if (is_editable() && edited_resource.is_valid()) {404Vector<Ref<EditorResourceConversionPlugin>> conversions = EditorNode::get_singleton()->find_resource_conversion_plugin_for_resource(edited_resource);405if (!conversions.is_empty()) {406edit_menu->add_separator();407}408int relative_id = 0;409for (const Ref<EditorResourceConversionPlugin> &conversion : conversions) {410String what = conversion->converts_to();411Ref<Texture2D> icon;412if (has_theme_icon(what, EditorStringName(EditorIcons))) {413icon = get_editor_theme_icon(what);414} else {415icon = get_editor_theme_icon(SNAME("Object"));416}417418edit_menu->add_icon_item(icon, vformat(TTR("Convert to %s"), what), CONVERT_BASE_ID + relative_id);419relative_id++;420}421}422}423424void EditorResourcePicker::_edit_menu_cbk(int p_which) {425switch (p_which) {426case OBJ_MENU_LOAD: {427List<String> extensions;428for (int i = 0; i < base_type.get_slice_count(","); i++) {429String base = base_type.get_slicec(',', i);430if (base == "Resource") {431base = "";432}433ResourceLoader::get_recognized_extensions_for_type(base, &extensions);434if (ScriptServer::is_global_class(base)) {435ResourceLoader::get_recognized_extensions_for_type(ScriptServer::get_global_class_native_base(base), &extensions);436}437}438439HashSet<String> valid_extensions;440for (const String &E : extensions) {441valid_extensions.insert(E);442}443444if (!file_dialog) {445file_dialog = memnew(EditorFileDialog);446file_dialog->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_FILE);447add_child(file_dialog);448file_dialog->connect("file_selected", callable_mp(this, &EditorResourcePicker::_file_selected));449}450451file_dialog->clear_filters();452for (const String &E : valid_extensions) {453file_dialog->add_filter("*." + E, E.to_upper());454}455456file_dialog->popup_file_dialog();457} break;458459case OBJ_MENU_QUICKLOAD: {460const Vector<String> &base_types_string = base_type.split(",");461462Vector<StringName> base_types;463for (const String &type : base_types_string) {464base_types.push_back(type);465}466467EditorQuickOpenDialog *quick_open = EditorNode::get_singleton()->get_quick_open_dialog();468if (resource_owner) {469quick_open->popup_dialog_for_property(base_types, resource_owner, property_path, callable_mp(this, &EditorResourcePicker::_file_selected));470} else {471quick_open->popup_dialog(base_types, callable_mp(this, &EditorResourcePicker::_file_selected));472}473474} break;475476case OBJ_MENU_INSPECT: {477if (edited_resource.is_valid()) {478emit_signal(SNAME("resource_selected"), edited_resource, true);479}480} break;481482case OBJ_MENU_CLEAR: {483edited_resource = Ref<Resource>();484_resource_changed();485} break;486487case OBJ_MENU_MAKE_UNIQUE: {488if (edited_resource.is_null()) {489return;490}491Ref<Resource> unique_resource = edited_resource->duplicate();492ERR_FAIL_COND(unique_resource.is_null()); // duplicate() may fail.493494edited_resource = unique_resource;495_resource_changed();496} break;497498case OBJ_MENU_MAKE_UNIQUE_RECURSIVE: {499if (edited_resource.is_null()) {500return;501}502503if (!duplicate_resources_dialog) {504duplicate_resources_dialog = memnew(ConfirmationDialog);505add_child(duplicate_resources_dialog);506duplicate_resources_dialog->set_title(TTR("Make Unique (Recursive)"));507duplicate_resources_dialog->connect(SceneStringName(confirmed), callable_mp(this, &EditorResourcePicker::_duplicate_selected_resources));508509VBoxContainer *vb = memnew(VBoxContainer);510duplicate_resources_dialog->add_child(vb);511512Label *label = memnew(Label(TTR("Select resources to make unique:")));513vb->add_child(label);514515duplicate_resources_tree = memnew(Tree);516duplicate_resources_tree->set_accessibility_name(TTRC("Duplicate resources"));517duplicate_resources_tree->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);518vb->add_child(duplicate_resources_tree);519duplicate_resources_tree->set_columns(2);520duplicate_resources_tree->set_v_size_flags(SIZE_EXPAND_FILL);521}522523duplicate_resources_tree->clear();524TreeItem *root = duplicate_resources_tree->create_item();525_gather_resources_to_duplicate(edited_resource, root);526527duplicate_resources_dialog->reset_size();528duplicate_resources_dialog->popup_centered(Vector2(500, 400) * EDSCALE);529} break;530531case OBJ_MENU_SAVE: {532if (edited_resource.is_null()) {533return;534}535EditorNode::get_singleton()->save_resource(edited_resource);536} break;537538case OBJ_MENU_SAVE_AS: {539if (edited_resource.is_null()) {540return;541}542Callable resource_saved = callable_mp(this, &EditorResourcePicker::_resource_saved);543if (!EditorNode::get_singleton()->is_connected("resource_saved", resource_saved)) {544EditorNode::get_singleton()->connect("resource_saved", resource_saved);545}546EditorNode::get_singleton()->save_resource_as(edited_resource);547} break;548549case OBJ_MENU_COPY: {550EditorSettings::get_singleton()->set_resource_clipboard(edited_resource);551} break;552553case OBJ_MENU_PASTE: {554edited_resource = EditorSettings::get_singleton()->get_resource_clipboard();555bool make_unique = true;556557// Automatically make resource unique if it belongs to another scene or resource.558if (!EditorNode::get_singleton()->get_edited_scene() || !edited_resource->is_built_in() || edited_resource->get_path().get_slice("::", 0) == EditorNode::get_singleton()->get_edited_scene()->get_scene_file_path()) {559make_unique = false;560} else if (resource_owner) {561Resource *res = Object::cast_to<Resource>(resource_owner);562if (res && edited_resource->get_path().get_slice("::", 0) == res->get_path().get_slice("::", 0)) {563make_unique = false;564}565}566567if (make_unique) {568_edit_menu_cbk(OBJ_MENU_MAKE_UNIQUE);569} else {570_resource_changed();571}572} break;573574case OBJ_MENU_PASTE_AS_UNIQUE: {575edited_resource = EditorSettings::get_singleton()->get_resource_clipboard();576if (_has_sub_resources(edited_resource)) {577// Use the recursive version when the Resource has sub-resources.578// This will show up a dialog to select which resources to make unique.579_edit_menu_cbk(OBJ_MENU_MAKE_UNIQUE_RECURSIVE);580} else {581_edit_menu_cbk(OBJ_MENU_MAKE_UNIQUE);582}583} break;584585case OBJ_MENU_SHOW_IN_FILE_SYSTEM: {586FileSystemDock::get_singleton()->navigate_to_path(edited_resource->get_path());587} break;588589default: {590// Allow subclasses to handle their own options first, only then fallback on the default branch logic.591if (handle_menu_selected(p_which)) {592break;593}594595if (p_which >= CONVERT_BASE_ID) {596int to_type = p_which - CONVERT_BASE_ID;597Vector<Ref<EditorResourceConversionPlugin>> conversions = EditorNode::get_singleton()->find_resource_conversion_plugin_for_resource(edited_resource);598ERR_FAIL_INDEX(to_type, conversions.size());599600Ref<Resource> converted_resource = conversions[to_type]->convert(edited_resource);601if (converted_resource.is_valid()) {602edited_resource = converted_resource;603_resource_changed();604}605break;606}607608ERR_FAIL_COND(inheritors_array.is_empty());609610String intype = inheritors_array[p_which - TYPE_BASE_ID];611Variant obj;612613if (ScriptServer::is_global_class(intype)) {614obj = EditorNode::get_editor_data().script_class_instance(intype);615} else {616obj = ClassDB::instantiate(intype);617}618619if (!obj) {620obj = EditorNode::get_editor_data().instantiate_custom_type(intype, "Resource");621}622623Resource *resp = Object::cast_to<Resource>(obj);624ERR_BREAK(!resp);625resp->set_path(_get_owner_path() + "::" + resp->generate_scene_unique_id()); // Assign a base path for built-in Resources.626627EditorNode::get_editor_data().instantiate_object_properties(obj);628629// Prevent freeing of the object until the end of the update of the resource (GH-88286).630Ref<Resource> old_edited_resource = edited_resource;631edited_resource = Ref<Resource>(resp);632_resource_changed();633} break;634}635}636637void EditorResourcePicker::set_create_options(Object *p_menu_node) {638_ensure_resource_menu();639// If a subclass implements this method, use it to replace all create items.640if (GDVIRTUAL_CALL(_set_create_options, p_menu_node)) {641return;642}643644// By default provide generic "New ..." options.645if (!base_type.is_empty()) {646int idx = 0;647648_ensure_allowed_types();649HashSet<StringName> allowed_types = allowed_types_without_convert;650if (!allowed_types.is_empty()) {651edit_menu->add_separator(TTRC("New"));652}653654for (const StringName &E : allowed_types) {655const String &t = E;656657if (!ClassDB::can_instantiate(t)) {658continue;659}660661inheritors_array.push_back(t);662663Ref<Texture2D> icon = EditorNode::get_singleton()->get_class_icon(t);664int id = TYPE_BASE_ID + idx;665edit_menu->add_icon_item(icon, t, id);666edit_menu->set_item_auto_translate_mode(-1, AUTO_TRANSLATE_MODE_DISABLED);667668HashMap<String, DocData::ClassDoc>::Iterator class_doc = EditorHelp::get_doc_data()->class_list.find(t);669if (class_doc) {670edit_menu->set_item_tooltip(-1, DTR(class_doc->value.brief_description));671}672673idx++;674}675676if (edit_menu->get_item_count()) {677edit_menu->add_separator();678}679}680}681682bool EditorResourcePicker::handle_menu_selected(int p_which) {683bool success = false;684GDVIRTUAL_CALL(_handle_menu_selected, p_which, success);685return success;686}687688void EditorResourcePicker::_button_draw() {689if (dropping) {690Color color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor));691assign_button->draw_rect(Rect2(Point2(), assign_button->get_size()), color, false);692}693}694695void EditorResourcePicker::_button_input(const Ref<InputEvent> &p_event) {696Ref<InputEventMouseButton> mb = p_event;697698if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::RIGHT) {699// Only attempt to update and show the menu if we have700// a valid resource or the Picker is editable, as701// there will otherwise be nothing to display.702if (edited_resource.is_valid() || is_editable()) {703if (edit_menu && edit_menu->is_visible()) {704edit_button->set_pressed(false);705edit_menu->hide();706return;707}708709_update_menu_items();710711Vector2 pos = get_screen_position() + mb->get_position();712edit_menu->reset_size();713edit_menu->set_position(pos);714edit_menu->popup();715}716}717}718719void EditorResourcePicker::_on_unique_button_pressed() {720if (Input::get_singleton()->is_mouse_button_pressed(MouseButton::LEFT)) {721_edit_menu_cbk(OBJ_MENU_MAKE_UNIQUE);722} else if (Input::get_singleton()->is_mouse_button_pressed(MouseButton::RIGHT) && _is_uniqueness_enabled(true)) {723_edit_menu_cbk(OBJ_MENU_MAKE_UNIQUE_RECURSIVE);724}725}726727String EditorResourcePicker::_get_owner_path() const {728EditorProperty *property = Object::cast_to<EditorProperty>(get_parent());729if (!property) {730return String();731}732Object *obj = property->get_edited_object();733734Node *node = Object::cast_to<Node>(obj);735if (node) {736Node *p_edited_scene_root = EditorNode::get_singleton()->get_editor_data().get_edited_scene_root();737if (node->get_scene_file_path().is_empty()) {738node = node->get_owner();739} else if (p_edited_scene_root != nullptr && p_edited_scene_root->get_scene_file_path() != node->get_scene_file_path()) {740// PackedScene should use root scene path.741return p_edited_scene_root->get_scene_file_path();742}743if (node) {744return node->get_scene_file_path();745}746return String();747}748749Resource *res = Object::cast_to<Resource>(obj);750if (res && !res->is_built_in()) {751return res->get_path();752}753// TODO: It would be nice to handle deeper Resource nesting.754return String();755}756757String EditorResourcePicker::_get_resource_type(const Ref<Resource> &p_resource) const {758if (p_resource.is_null()) {759return String();760}761String res_type = p_resource->get_class();762763Ref<Script> res_script = p_resource->get_script();764if (res_script.is_null()) {765return res_type;766}767768// TODO: Replace with EditorFileSystem when PR #60606 is merged to use cached resource type.769String script_type = EditorNode::get_editor_data().script_class_get_name(res_script->get_path());770if (!script_type.is_empty()) {771res_type = script_type;772}773return res_type;774}775776static bool _should_hide_type(const StringName &p_type) {777if (ClassDB::is_virtual(p_type)) {778return true;779}780781if (p_type == SNAME("MissingResource")) {782return true;783}784785return false;786}787788static void _add_allowed_type(const StringName &p_type, List<StringName> *p_vector) {789if (p_vector->find(p_type)) {790// Already added.791return;792}793794if (ClassDB::class_exists(p_type)) {795// Engine class.796797if (!_should_hide_type(p_type)) {798p_vector->push_back(p_type);799}800801LocalVector<StringName> inheriters;802ClassDB::get_inheriters_from_class(p_type, inheriters);803for (const StringName &S : inheriters) {804_add_allowed_type(S, p_vector);805}806} else {807// Script class.808p_vector->push_back(p_type);809}810811List<StringName> inheriters;812ScriptServer::get_inheriters_list(p_type, &inheriters);813for (const StringName &S : inheriters) {814_add_allowed_type(S, p_vector);815}816}817818void EditorResourcePicker::_ensure_allowed_types() const {819if (!allowed_types_without_convert.is_empty()) {820return;821}822823List<StringName> final_allowed;824825Vector<String> allowed_types = base_type.split(",");826int size = allowed_types.size();827828for (const String &S : allowed_types) {829const String base = S.strip_edges();830if (base.begins_with("-")) {831final_allowed.erase(base.right(-1));832continue;833}834_add_allowed_type(base, &final_allowed);835}836837for (const StringName &SN : final_allowed) {838allowed_types_without_convert.insert(SN);839}840841allowed_types_with_convert = HashSet<StringName>(allowed_types_without_convert);842843for (int i = 0; i < size; i++) {844const String base = allowed_types[i].strip_edges();845if (base == "BaseMaterial3D") {846allowed_types_with_convert.insert("Texture2D");847} else if (ClassDB::is_parent_class("ShaderMaterial", base)) {848allowed_types_with_convert.insert("Shader");849} else if (ClassDB::is_parent_class("ImageTexture", base)) {850allowed_types_with_convert.insert("Image");851}852}853}854855bool EditorResourcePicker::_is_drop_valid(const Dictionary &p_drag_data) const {856{857const ObjectID source_picker = p_drag_data.get("source_picker", ObjectID());858if (source_picker == get_instance_id()) {859return false;860}861}862863if (base_type.is_empty()) {864return true;865}866867Ref<Resource> res = _get_dropped_resource(p_drag_data);868if (res.is_null()) {869return false;870}871872_ensure_allowed_types();873HashSet<StringName> allowed_types = allowed_types_with_convert;874875String res_type = _get_resource_type(res);876877if (_is_type_valid(res_type, allowed_types)) {878return true;879}880881if (res->get_script()) {882StringName custom_class = EditorNode::get_singleton()->get_object_custom_type_name(res->get_script());883if (_is_type_valid(custom_class, allowed_types)) {884return true;885}886}887return false;888}889890bool EditorResourcePicker::_is_type_valid(const String &p_type_name, const HashSet<StringName> &p_allowed_types) const {891for (const StringName &E : p_allowed_types) {892String at = E;893if (p_type_name == at || ClassDB::is_parent_class(p_type_name, at) || EditorNode::get_editor_data().script_class_is_parent(p_type_name, at)) {894return true;895}896}897898return false;899}900901bool EditorResourcePicker::_is_custom_type_script() const {902EditorProperty *editor_property = Object::cast_to<EditorProperty>(get_parent());903if (!editor_property) {904return false;905}906907// Check if the property being edited is 'script'.908if (editor_property->get_edited_property() == CoreStringName(script)) {909// If there's currently a valid script assigned and the owning Node/Resource also has a custom type script assigned, then910// the currently assigned script is either the custom type script itself or an extension of it.911Ref<Script> resource_as_script = edited_resource;912if (resource_as_script.is_valid() && resource_owner && resource_owner->has_meta(SceneStringName(_custom_type_script))) {913return true;914}915}916917return false;918}919920Ref<Resource> EditorResourcePicker::_get_dropped_resource(const Variant &p_data) const {921Dictionary drag_data = p_data;922const String type = drag_data.get("type", "");923if (type.is_empty()) {924return Ref<Resource>();925}926927Ref<Resource> res;928if (type == "script_list_element") {929ScriptEditorBase *se = Object::cast_to<ScriptEditorBase>(drag_data["script_list_element"]);930if (se) {931return se->get_edited_resource();932}933} else if (type == "shader_list_element") {934return ResourceCache::get_ref(drag_data["file_path"]);935} else if (type == "resource") {936return drag_data["resource"];937} else if (type == "files") {938Vector<String> files = drag_data["files"];939940if (files.size() == 1) {941if (ResourceLoader::exists(files[0])) {942// TODO: Extract the typename of the dropped filepath's resource in a more performant way, without fully loading it.943return ResourceLoader::load(files[0]);944}945}946}947return Ref<Resource>();948}949950Variant EditorResourcePicker::get_drag_data_fw(const Point2 &p_point, Control *p_from) {951if (edited_resource.is_valid()) {952Dictionary drag_data = EditorNode::get_singleton()->drag_resource(edited_resource, p_from);953drag_data["source_picker"] = get_instance_id();954return drag_data;955}956957return Variant();958}959960bool EditorResourcePicker::can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const {961return editable && _is_drop_valid(p_data);962}963964void EditorResourcePicker::drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) {965Ref<Resource> dropped_resource = _get_dropped_resource(p_data);966if (dropped_resource.is_valid()) {967_ensure_allowed_types();968HashSet<StringName> allowed_types = allowed_types_without_convert;969970String res_type = _get_resource_type(dropped_resource);971972// If the accepted dropped resource is from the extended list, it requires conversion.973if (!_is_type_valid(res_type, allowed_types)) {974for (const StringName &E : allowed_types) {975String at = E;976977if (at == "BaseMaterial3D" && Ref<Texture2D>(dropped_resource).is_valid()) {978// Use existing resource if possible and only replace its data.979Ref<StandardMaterial3D> mat = edited_resource;980if (mat.is_null()) {981mat.instantiate();982}983mat->set_texture(StandardMaterial3D::TextureParam::TEXTURE_ALBEDO, dropped_resource);984dropped_resource = mat;985break;986}987988if (at == "ShaderMaterial" && Ref<Shader>(dropped_resource).is_valid()) {989Ref<ShaderMaterial> mat = edited_resource;990if (mat.is_null()) {991mat.instantiate();992}993mat->set_shader(dropped_resource);994dropped_resource = mat;995break;996}997998if (at == "ImageTexture" && Ref<Image>(dropped_resource).is_valid()) {999Ref<ImageTexture> texture = edited_resource;1000if (texture.is_null()) {1001texture.instantiate();1002}1003texture->set_image(dropped_resource);1004dropped_resource = texture;1005break;1006}1007}1008}10091010edited_resource = dropped_resource;10111012if (Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL)) {1013// `_edit_menu_cbk(OBJ_MENU_MAKE_UNIQUE)` already calls `_resource_changed()`,1014// so we don't need to manually call it in this case.1015_edit_menu_cbk(OBJ_MENU_MAKE_UNIQUE);1016} else {1017_resource_changed();1018}1019}1020}10211022void EditorResourcePicker::_bind_methods() {1023ClassDB::bind_method(D_METHOD("set_base_type", "base_type"), &EditorResourcePicker::set_base_type);1024ClassDB::bind_method(D_METHOD("get_base_type"), &EditorResourcePicker::get_base_type);1025ClassDB::bind_method(D_METHOD("get_allowed_types"), &EditorResourcePicker::get_allowed_types);1026ClassDB::bind_method(D_METHOD("set_edited_resource", "resource"), &EditorResourcePicker::set_edited_resource);1027ClassDB::bind_method(D_METHOD("get_edited_resource"), &EditorResourcePicker::get_edited_resource);1028ClassDB::bind_method(D_METHOD("set_toggle_mode", "enable"), &EditorResourcePicker::set_toggle_mode);1029ClassDB::bind_method(D_METHOD("is_toggle_mode"), &EditorResourcePicker::is_toggle_mode);1030ClassDB::bind_method(D_METHOD("set_toggle_pressed", "pressed"), &EditorResourcePicker::set_toggle_pressed);1031ClassDB::bind_method(D_METHOD("set_editable", "enable"), &EditorResourcePicker::set_editable);1032ClassDB::bind_method(D_METHOD("is_editable"), &EditorResourcePicker::is_editable);10331034GDVIRTUAL_BIND(_set_create_options, "menu_node");1035GDVIRTUAL_BIND(_handle_menu_selected, "id");10361037ADD_PROPERTY(PropertyInfo(Variant::STRING, "base_type"), "set_base_type", "get_base_type");1038ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "edited_resource", PROPERTY_HINT_RESOURCE_TYPE, Resource::get_class_static(), PROPERTY_USAGE_NONE), "set_edited_resource", "get_edited_resource");1039ADD_PROPERTY(PropertyInfo(Variant::BOOL, "editable"), "set_editable", "is_editable");1040ADD_PROPERTY(PropertyInfo(Variant::BOOL, "toggle_mode"), "set_toggle_mode", "is_toggle_mode");10411042ADD_SIGNAL(MethodInfo("resource_selected", PropertyInfo(Variant::OBJECT, "resource", PROPERTY_HINT_RESOURCE_TYPE, Resource::get_class_static()), PropertyInfo(Variant::BOOL, "inspect")));1043ADD_SIGNAL(MethodInfo("resource_changed", PropertyInfo(Variant::OBJECT, "resource", PROPERTY_HINT_RESOURCE_TYPE, Resource::get_class_static())));1044}10451046void EditorResourcePicker::_notification(int p_what) {1047switch (p_what) {1048case NOTIFICATION_ENTER_TREE: {1049EditorNode::get_singleton()->connect("resource_counter_changed", callable_mp(this, &EditorResourcePicker::_update_resource));1050_update_resource();1051[[fallthrough]];1052}1053case NOTIFICATION_THEME_CHANGED: {1054const int icon_width = get_theme_constant(SNAME("class_icon_size"), EditorStringName(Editor));1055assign_button->add_theme_constant_override("icon_max_width", icon_width);1056make_unique_button->add_theme_constant_override("icon_max_width", icon_width);1057if (edit_menu) {1058edit_menu->add_theme_constant_override("icon_max_width", icon_width);1059}10601061quick_load_button->set_button_icon(get_editor_theme_icon(SNAME("LoadQuick")));1062edit_button->set_button_icon(get_theme_icon(SNAME("select_arrow"), SNAME("Tree")));1063} break;10641065case NOTIFICATION_DRAW: {1066draw_style_box(get_theme_stylebox(SceneStringName(panel), SNAME("Tree")), Rect2(Point2(), get_size()));1067} break;10681069case NOTIFICATION_SORT_CHILDREN: {1070_update_resource();1071} break;10721073case NOTIFICATION_DRAG_BEGIN: {1074if (editable && _is_drop_valid(get_viewport()->gui_get_drag_data())) {1075dropping = true;1076assign_button->queue_redraw();1077}1078} break;10791080case NOTIFICATION_DRAG_END: {1081if (dropping) {1082dropping = false;1083assign_button->queue_redraw();1084}1085} break;10861087case NOTIFICATION_EXIT_TREE: {1088Callable resource_saved = callable_mp(this, &EditorResourcePicker::_resource_saved);1089Callable resource_counter_changed = callable_mp(this, &EditorResourcePicker::_update_resource);1090if (EditorNode::get_singleton()->is_connected("resource_saved", resource_saved)) {1091EditorNode::get_singleton()->disconnect("resource_saved", resource_saved);1092}1093if (EditorNode::get_singleton()->is_connected("resource_counter_changed", resource_counter_changed)) {1094EditorNode::get_singleton()->disconnect("resource_counter_changed", resource_counter_changed);1095}1096} break;1097}1098}10991100void EditorResourcePicker::set_assign_button_min_size(const Size2i &p_size) {1101assign_button_min_size = p_size;1102assign_button->set_custom_minimum_size(assign_button_min_size);1103}11041105void EditorResourcePicker::set_base_type(const String &p_base_type) {1106base_type = p_base_type;11071108// There is a possibility that the new base type is conflicting with the existing value.1109// Keep the value, but warn the user that there is a potential mistake.1110if (!base_type.is_empty() && edited_resource.is_valid()) {1111_ensure_allowed_types();1112HashSet<StringName> allowed_types = allowed_types_with_convert;11131114StringName custom_class;1115bool is_custom = false;1116if (edited_resource->get_script()) {1117custom_class = EditorNode::get_singleton()->get_object_custom_type_name(edited_resource->get_script());1118is_custom = _is_type_valid(custom_class, allowed_types);1119}11201121if (!is_custom && !_is_type_valid(edited_resource->get_class(), allowed_types)) {1122String class_str = (custom_class == StringName() ? edited_resource->get_class() : vformat("%s (%s)", custom_class, edited_resource->get_class()));1123WARN_PRINT(vformat("Value mismatch between the new base type of this EditorResourcePicker, '%s', and the type of the value it already has, '%s'.", base_type, class_str));1124}1125}1126}11271128String EditorResourcePicker::get_base_type() const {1129return base_type;1130}11311132Vector<String> EditorResourcePicker::get_allowed_types() const {1133_ensure_allowed_types();1134HashSet<StringName> allowed_types = allowed_types_without_convert;11351136Vector<String> types;1137types.resize(allowed_types.size());11381139int i = 0;1140String *w = types.ptrw();1141for (const StringName &E : allowed_types) {1142w[i] = E;1143i++;1144}11451146return types;1147}11481149bool EditorResourcePicker::is_resource_allowed(const Ref<Resource> &p_resource) {1150if (p_resource.is_null()) {1151return true;1152}11531154if (!base_type.is_empty()) {1155_ensure_allowed_types();1156HashSet<StringName> allowed_types = allowed_types_with_convert;11571158StringName custom_class;1159bool is_custom = false;1160if (p_resource->get_script()) {1161custom_class = EditorNode::get_singleton()->get_object_custom_type_name(p_resource->get_script());1162is_custom = _is_type_valid(custom_class, allowed_types);1163}11641165if (!is_custom && !_is_type_valid(p_resource->get_class(), allowed_types)) {1166return false;1167}1168}1169return true;1170}11711172void EditorResourcePicker::set_edited_resource(Ref<Resource> p_resource) {1173if (!is_resource_allowed(p_resource)) {1174StringName custom_class;1175if (p_resource->get_script()) {1176custom_class = EditorNode::get_singleton()->get_object_custom_type_name(p_resource->get_script());1177}1178const String class_str = (custom_class.is_empty() ? p_resource->get_class() : vformat("%s (%s)", custom_class, p_resource->get_class()));1179ERR_FAIL_MSG(vformat("Failed to set a resource of the type '%s' because this EditorResourcePicker only accepts '%s' and its derivatives.", class_str, base_type));1180}1181set_edited_resource_no_check(p_resource);1182}11831184void EditorResourcePicker::set_edited_resource_no_check(Ref<Resource> p_resource) {1185edited_resource = p_resource;1186_update_resource();1187}11881189Ref<Resource> EditorResourcePicker::get_edited_resource() {1190return edited_resource;1191}11921193void EditorResourcePicker::set_toggle_mode(bool p_enable) {1194assign_button->set_toggle_mode(p_enable);1195}11961197bool EditorResourcePicker::is_toggle_mode() const {1198return assign_button->is_toggle_mode();1199}12001201void EditorResourcePicker::set_toggle_pressed(bool p_pressed) {1202if (!is_toggle_mode()) {1203return;1204}12051206assign_button->set_pressed(p_pressed);1207}12081209bool EditorResourcePicker::is_toggle_pressed() const {1210return assign_button->is_pressed();1211}12121213void EditorResourcePicker::set_resource_owner(Object *p_object) {1214resource_owner = p_object;1215}12161217void EditorResourcePicker::set_editable(bool p_editable) {1218editable = p_editable;1219assign_button->set_disabled(!editable && edited_resource.is_null());1220quick_load_button->set_visible(editable && edited_resource.is_null());1221edit_button->set_visible(editable);1222}12231224bool EditorResourcePicker::is_editable() const {1225return editable;1226}12271228void EditorResourcePicker::_ensure_resource_menu() {1229if (edit_menu) {1230return;1231}1232edit_menu = memnew(PopupMenu);1233edit_menu->add_theme_constant_override("icon_max_width", get_theme_constant(SNAME("class_icon_size"), EditorStringName(Editor)));1234add_child(edit_menu);1235edit_menu->connect(SceneStringName(id_pressed), callable_mp(this, &EditorResourcePicker::_edit_menu_cbk));1236edit_menu->connect("popup_hide", callable_mp((BaseButton *)edit_button, &BaseButton::set_pressed).bind(false));1237}12381239void EditorResourcePicker::_gather_resources_to_duplicate(const Ref<Resource> p_resource, TreeItem *p_item, const String &p_property_name) const {1240p_item->set_cell_mode(0, TreeItem::CELL_MODE_CHECK);12411242String res_name = p_resource->get_name();1243if (res_name.is_empty() && !p_resource->is_built_in()) {1244res_name = p_resource->get_path().get_file();1245}12461247if (res_name.is_empty()) {1248p_item->set_text(0, _get_resource_type(p_resource));1249} else {1250p_item->set_text(0, vformat("%s (%s)", _get_resource_type(p_resource), res_name));1251}12521253p_item->set_icon(0, EditorNode::get_singleton()->get_object_icon(p_resource.ptr()));1254p_item->set_editable(0, true);12551256Array meta = { p_resource };1257p_item->set_metadata(0, meta);12581259if (!p_property_name.is_empty()) {1260p_item->set_text(1, p_property_name);1261}12621263static Vector<String> unique_exceptions = { "Image", "Shader", "Mesh", "FontFile" };1264if (!unique_exceptions.has(p_resource->get_class())) {1265// Automatically select resource, unless it's something that shouldn't be duplicated.1266p_item->set_checked(0, true);1267}12681269List<PropertyInfo> plist;1270p_resource->get_property_list(&plist);12711272for (const PropertyInfo &E : plist) {1273if (!(E.usage & PROPERTY_USAGE_STORAGE) || (E.type != Variant::OBJECT && E.type != Variant::ARRAY && E.type != Variant::DICTIONARY)) {1274continue;1275}12761277Variant value = p_resource->get(E.name);1278TreeItem *child = nullptr;12791280if (E.type == Variant::ARRAY) {1281Array arr = value;1282for (int i = 0; i < arr.size(); i++) {1283Ref<Resource> res = arr[i];1284if (res.is_valid()) {1285child = p_item->create_child();1286_gather_resources_to_duplicate(res, child, E.name);1287meta = child->get_metadata(0);1288meta.push_back(E.name);1289meta.push_back(i); // Remember index.1290}1291}1292continue;1293} else if (E.type == Variant::DICTIONARY) {1294Dictionary dict = value;1295for (const KeyValue<Variant, Variant> &kv : dict) {1296Ref<Resource> key_res = kv.key;1297Ref<Resource> value_res = kv.value;1298if (key_res.is_valid()) {1299child = p_item->create_child();1300_gather_resources_to_duplicate(key_res, child, E.name);1301meta = child->get_metadata(0);1302meta.push_back(E.name);1303meta.push_back(key_res);1304}1305if (value_res.is_valid()) {1306child = p_item->create_child();1307_gather_resources_to_duplicate(value_res, child, E.name);1308meta = child->get_metadata(0);1309meta.push_back(E.name);1310meta.push_back(value_res);1311meta.push_back(kv.key);1312}1313}1314continue;1315}13161317Ref<Resource> res = p_resource->get(E.name);1318if (res.is_null()) {1319continue;1320}1321child = p_item->create_child();1322_gather_resources_to_duplicate(res, child, E.name);1323meta = child->get_metadata(0);1324// Remember property name.1325meta.push_back(E.name);13261327if ((E.usage & PROPERTY_USAGE_NEVER_DUPLICATE)) {1328// The resource can't be duplicated, but make it appear on the list anyway.1329child->set_checked(0, false);1330child->set_editable(0, false);1331}1332}1333}13341335void EditorResourcePicker::_duplicate_selected_resources() {1336for (TreeItem *item = duplicate_resources_tree->get_root(); item; item = item->get_next_in_tree()) {1337if (!item->is_checked(0)) {1338continue;1339}13401341Array meta = item->get_metadata(0);1342Ref<Resource> res = meta[0];1343Ref<Resource> unique_resource = res->duplicate();1344ERR_FAIL_COND(unique_resource.is_null()); // duplicate() may fail.1345meta[0] = unique_resource;13461347if (meta.size() == 1) { // Root.1348edited_resource = unique_resource;1349continue;1350}1351Array parent_meta = item->get_parent()->get_metadata(0);1352Ref<Resource> parent = parent_meta[0];1353Variant::Type property_type = parent->get(meta[1]).get_type();13541355if (property_type == Variant::OBJECT) {1356parent->set(meta[1], unique_resource);1357continue;1358}13591360Variant property = parent->get(meta[1]);13611362if (!parent_meta.has(property)) {1363property = property.duplicate();1364parent->set(meta[1], property);1365parent_meta.push_back(property); // Append Duplicated Type so we can check if it's already been duplicated.1366}13671368if (property_type == Variant::ARRAY) {1369Array arr = property;1370arr[meta[2]] = unique_resource;1371continue;1372}13731374Dictionary dict = property;1375LocalVector<Variant> keys = dict.get_key_list();13761377if (meta[2].get_type() == Variant::OBJECT) {1378if (keys.has(meta[2])) {1379//It's a key.1380dict[unique_resource] = dict[meta[2]];1381dict.erase(meta[2]);1382parent_meta.push_back(unique_resource);1383} else {1384// If key has been erased, use last appended Resource key instead.1385Variant key = keys.has(meta[3]) ? meta[3] : parent_meta.back();1386dict[key] = unique_resource;1387}1388} else {1389dict[meta[2]] = unique_resource;1390}1391}1392_resource_changed();1393}13941395bool EditorResourcePicker::_is_uniqueness_enabled(bool p_check_recursive) {1396if (force_allow_unique) {1397return true;1398}1399Ref<Resource> parent_resource = _has_parent_resource();1400EditorNode *en = EditorNode::get_singleton();1401bool internal_to_scene = en->is_resource_internal_to_scene(edited_resource);1402List<Node *> node_list = en->get_editor_selection()->get_full_selected_node_list();14031404// Todo: Implement a more elegant solution for multiple selected Nodes. This should suffice for the time being.1405if (node_list.size() > 1 && !p_check_recursive) {1406return node_list.size() != en->get_resource_count(edited_resource) || !internal_to_scene;1407}14081409if (!internal_to_scene) {1410if (parent_resource.is_valid() && (!en->is_resource_internal_to_scene(parent_resource) || en->get_resource_count(parent_resource) > 1)) {1411return !parent_resource->is_built_in();1412} else if (!p_check_recursive) {1413return true;1414}1415}14161417int parent_counter = en->get_resource_count(parent_resource);1418bool above_threshold = parent_resource.is_valid() ? (en->get_resource_count(parent_resource) <= 1 && en->get_resource_count(edited_resource) > 1) : en->get_resource_count(edited_resource) > 1;1419if (!p_check_recursive) {1420return above_threshold;1421}14221423if (p_check_recursive && parent_counter <= 1) {1424List<Ref<Resource>> nested_resources;1425en->gather_resources(edited_resource, nested_resources, true, true);14261427for (Ref<Resource> R : nested_resources) {1428// Take into account Nested External Resources.1429if (en->get_resource_count(R) > 1 || !EditorNode::get_singleton()->is_resource_internal_to_scene(R)) {1430return true;1431}1432}1433return false;1434}1435return false;1436}14371438Ref<Resource> EditorResourcePicker::_has_parent_resource() {1439Node *current_node = this->get_parent();1440while (current_node != nullptr) {1441EditorProperty *ep = Object::cast_to<EditorProperty>(current_node);1442if (ep && Object::cast_to<Resource>(ep->get_edited_object())) {1443return Object::cast_to<Resource>(ep->get_edited_object());1444}1445current_node = current_node->get_parent();1446}1447return nullptr;1448}14491450EditorResourcePicker::EditorResourcePicker(bool p_hide_assign_button_controls) {1451make_unique_button = memnew(Button);1452make_unique_button->set_flat(true);1453make_unique_button->set_accessibility_name(TTRC("Number of Linked Resources."));1454make_unique_button->set_tooltip_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);1455make_unique_button->set_button_mask(MouseButtonMask::LEFT | MouseButtonMask::RIGHT);1456make_unique_button->set_action_mode(BaseButton::ACTION_MODE_BUTTON_PRESS);1457make_unique_button->set_theme_type_variation(SNAME("EditorInspectorButton"));1458add_child(make_unique_button);1459make_unique_button->connect(SceneStringName(pressed), callable_mp(this, &EditorResourcePicker::_on_unique_button_pressed));14601461assign_button = memnew(Button);1462assign_button->set_flat(true);1463assign_button->set_h_size_flags(SIZE_EXPAND_FILL);1464assign_button->set_accessibility_name(TTRC("Assign Resource"));1465assign_button->set_expand_icon(true);1466assign_button->set_clip_text(true);1467assign_button->set_theme_type_variation(SNAME("EditorInspectorButton"));1468assign_button->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);1469SET_DRAG_FORWARDING_GCD(assign_button, EditorResourcePicker);1470add_child(assign_button);1471assign_button->connect(SceneStringName(pressed), callable_mp(this, &EditorResourcePicker::_resource_selected));1472assign_button->connect(SceneStringName(draw), callable_mp(this, &EditorResourcePicker::_button_draw));1473assign_button->connect(SceneStringName(gui_input), callable_mp(this, &EditorResourcePicker::_button_input));14741475if (!p_hide_assign_button_controls) {1476preview_rect = memnew(TextureRect);1477preview_rect->set_expand_mode(TextureRect::EXPAND_IGNORE_SIZE);1478preview_rect->set_anchors_and_offsets_preset(PRESET_FULL_RECT);1479preview_rect->set_offset(SIDE_TOP, 1);1480preview_rect->set_offset(SIDE_BOTTOM, -1);1481preview_rect->set_offset(SIDE_RIGHT, -1);1482preview_rect->set_texture_filter(TEXTURE_FILTER_NEAREST_WITH_MIPMAPS);1483assign_button->add_child(preview_rect);1484}14851486quick_load_button = memnew(Button);1487quick_load_button->set_theme_type_variation(SNAME("EditorInspectorFlatButton"));1488quick_load_button->set_tooltip_text(TTRC("Quick Load"));1489add_child(quick_load_button);1490quick_load_button->connect(SceneStringName(pressed), callable_mp(this, &EditorResourcePicker::_edit_menu_cbk).bind(OBJ_MENU_QUICKLOAD));14911492edit_button = memnew(Button);1493edit_button->set_theme_type_variation(SNAME("EditorInspectorFlatButton"));1494edit_button->set_toggle_mode(true);1495edit_button->set_action_mode(BaseButton::ACTION_MODE_BUTTON_PRESS);1496edit_button->set_accessibility_name(TTRC("Edit"));1497add_child(edit_button);1498edit_button->connect(SceneStringName(pressed), callable_mp(this, &EditorResourcePicker::_update_menu));1499edit_button->connect(SceneStringName(gui_input), callable_mp(this, &EditorResourcePicker::_button_input));15001501add_theme_constant_override("separation", 0);1502}15031504// EditorScriptPicker15051506void EditorScriptPicker::set_create_options(Object *p_menu_node) {1507PopupMenu *menu_node = Object::cast_to<PopupMenu>(p_menu_node);1508if (!menu_node) {1509return;1510}15111512if (!(script_owner && script_owner->has_meta(SceneStringName(_custom_type_script)))) {1513menu_node->add_icon_item(get_editor_theme_icon(SNAME("ScriptCreate")), TTR("New Script..."), OBJ_MENU_NEW_SCRIPT);1514}15151516if (script_owner) {1517Ref<Script> scr = script_owner->get_script();1518if (scr.is_valid()) {1519menu_node->add_icon_item(get_editor_theme_icon(SNAME("ScriptExtend")), TTR("Extend Script..."), OBJ_MENU_EXTEND_SCRIPT);1520}1521}1522menu_node->add_separator();1523}15241525bool EditorScriptPicker::handle_menu_selected(int p_which) {1526switch (p_which) {1527case OBJ_MENU_NEW_SCRIPT: {1528if (script_owner) {1529SceneTreeDock::get_singleton()->open_script_dialog(script_owner, false);1530}1531return true;1532}15331534case OBJ_MENU_EXTEND_SCRIPT: {1535if (script_owner) {1536SceneTreeDock::get_singleton()->open_script_dialog(script_owner, true);1537}1538return true;1539}1540}15411542return false;1543}15441545void EditorScriptPicker::set_script_owner(Node *p_owner) {1546script_owner = p_owner;1547}15481549Node *EditorScriptPicker::get_script_owner() const {1550return script_owner;1551}15521553void EditorScriptPicker::_bind_methods() {1554ClassDB::bind_method(D_METHOD("set_script_owner", "owner_node"), &EditorScriptPicker::set_script_owner);1555ClassDB::bind_method(D_METHOD("get_script_owner"), &EditorScriptPicker::get_script_owner);15561557ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "script_owner", PROPERTY_HINT_RESOURCE_TYPE, Node::get_class_static(), PROPERTY_USAGE_NONE), "set_script_owner", "get_script_owner");1558}15591560// EditorShaderPicker15611562void EditorShaderPicker::set_create_options(Object *p_menu_node) {1563PopupMenu *menu_node = Object::cast_to<PopupMenu>(p_menu_node);1564if (!menu_node) {1565return;1566}15671568menu_node->add_icon_item(get_editor_theme_icon(SNAME("Shader")), TTR("New Shader..."), OBJ_MENU_NEW_SHADER);1569menu_node->add_separator();1570}15711572bool EditorShaderPicker::handle_menu_selected(int p_which) {1573Ref<ShaderMaterial> ed_material = Ref<ShaderMaterial>(get_edited_material());15741575switch (p_which) {1576case OBJ_MENU_NEW_SHADER: {1577if (ed_material.is_valid()) {1578SceneTreeDock::get_singleton()->open_shader_dialog(ed_material, preferred_mode);1579return true;1580}1581} break;1582default:1583break;1584}1585return false;1586}15871588void EditorShaderPicker::set_edited_material(ShaderMaterial *p_material) {1589edited_material = p_material;1590}15911592ShaderMaterial *EditorShaderPicker::get_edited_material() const {1593return edited_material;1594}15951596void EditorShaderPicker::set_preferred_mode(int p_mode) {1597preferred_mode = p_mode;1598}15991600//////////////16011602void EditorAudioStreamPicker::_notification(int p_what) {1603switch (p_what) {1604case NOTIFICATION_READY:1605case NOTIFICATION_THEME_CHANGED: {1606_update_resource();1607} break;1608case NOTIFICATION_INTERNAL_PROCESS: {1609Ref<AudioStream> audio_stream = get_edited_resource();1610if (audio_stream.is_valid()) {1611if (audio_stream->get_length() > 0) {1612Ref<AudioStreamPreview> preview = AudioStreamPreviewGenerator::get_singleton()->generate_preview(audio_stream);1613if (preview.is_valid()) {1614if (preview->get_version() != last_preview_version) {1615stream_preview_rect->queue_redraw();1616last_preview_version = preview->get_version();1617}1618}1619}16201621uint64_t tagged_frame = audio_stream->get_tagged_frame();1622uint64_t diff_frames = AudioServer::get_singleton()->get_mixed_frames() - tagged_frame;1623uint64_t diff_msec = diff_frames * 1000 / AudioServer::get_singleton()->get_mix_rate();16241625if (diff_msec < 300) {1626uint32_t count = audio_stream->get_tagged_frame_count();16271628bool differ = false;16291630if (count != tagged_frame_offset_count) {1631differ = true;1632}1633float offsets[MAX_TAGGED_FRAMES];16341635for (uint32_t i = 0; i < MIN(count, uint32_t(MAX_TAGGED_FRAMES)); i++) {1636offsets[i] = audio_stream->get_tagged_frame_offset(i);1637if (offsets[i] != tagged_frame_offsets[i]) {1638differ = true;1639}1640}16411642if (differ) {1643tagged_frame_offset_count = count;1644for (uint32_t i = 0; i < count; i++) {1645tagged_frame_offsets[i] = offsets[i];1646}1647}16481649stream_preview_rect->queue_redraw();1650} else {1651if (tagged_frame_offset_count != 0) {1652stream_preview_rect->queue_redraw();1653}1654tagged_frame_offset_count = 0;1655}1656}1657} break;1658}1659}16601661void EditorAudioStreamPicker::_update_resource() {1662EditorResourcePicker::_update_resource();16631664Ref<Font> font = get_theme_font(SceneStringName(font), SNAME("Label"));1665int font_size = get_theme_font_size(SceneStringName(font_size), SNAME("Label"));1666Ref<AudioStream> audio_stream = get_edited_resource();1667if (audio_stream.is_valid() && audio_stream->get_length() > 0.0) {1668set_assign_button_min_size(Size2(1, font->get_height(font_size) * 3));1669} else {1670set_assign_button_min_size(Size2(1, font->get_height(font_size) * 1.5));1671}16721673stream_preview_rect->queue_redraw();1674}16751676void EditorAudioStreamPicker::_preview_draw() {1677Ref<AudioStream> audio_stream = get_edited_resource();1678if (audio_stream.is_null()) {1679get_assign_button()->set_text(TTR("<empty>"));1680return;1681}16821683int font_size = get_theme_font_size(SceneStringName(font_size), SNAME("Label"));16841685get_assign_button()->set_text("");16861687Size2i size = stream_preview_rect->get_size();1688Ref<Font> font = get_theme_font(SceneStringName(font), SNAME("Label"));16891690Rect2 rect(Point2(), size);16911692if (audio_stream->get_length() > 0 && size.width > 0) {1693rect.size.height *= 0.5;16941695Ref<AudioStreamPreview> preview = AudioStreamPreviewGenerator::get_singleton()->generate_preview(audio_stream);1696float preview_len = preview->get_length();16971698Vector<Vector2> points;1699points.resize(size.width * 2);17001701for (int i = 0; i < size.width; i++) {1702float ofs = i * preview_len / size.width;1703float ofs_n = (i + 1) * preview_len / size.width;1704float max = preview->get_max(ofs, ofs_n) * 0.5 + 0.5;1705float min = preview->get_min(ofs, ofs_n) * 0.5 + 0.5;17061707int idx = i;1708points.write[idx * 2 + 0] = Vector2(i + 1, rect.position.y + min * rect.size.y);1709points.write[idx * 2 + 1] = Vector2(i + 1, rect.position.y + max * rect.size.y);1710}17111712Vector<Color> colors = { get_theme_color(SNAME("contrast_color_2"), EditorStringName(Editor)) };17131714RS::get_singleton()->canvas_item_add_multiline(stream_preview_rect->get_canvas_item(), points, colors);17151716if (tagged_frame_offset_count) {1717Color accent = get_theme_color(SNAME("accent_color"), EditorStringName(Editor));17181719for (uint32_t i = 0; i < tagged_frame_offset_count; i++) {1720int x = CLAMP(tagged_frame_offsets[i] * size.width / preview_len, 0, size.width);1721if (x == 0) {1722continue; // Because some may always return 0, ignore offset 0.1723}1724stream_preview_rect->draw_rect(Rect2i(x, 0, 2, rect.size.height), accent);1725}1726}1727rect.position.y += rect.size.height;1728}17291730Ref<Texture2D> icon;1731Color icon_modulate(1, 1, 1, 1);17321733if (tagged_frame_offset_count > 0) {1734icon = get_editor_theme_icon(SNAME("Play"));1735if ((OS::get_singleton()->get_ticks_msec() % 500) > 250) {1736icon_modulate = Color(1, 0.5, 0.5, 1); // get_theme_color(SNAME("accent_color"), EditorStringName(Editor));1737}1738} else {1739icon = EditorNode::get_singleton()->get_object_icon(audio_stream.operator->());1740}1741String text;1742if (!audio_stream->get_name().is_empty()) {1743text = audio_stream->get_name();1744} else if (audio_stream->get_path().is_resource_file()) {1745text = audio_stream->get_path().get_file();1746} else {1747text = audio_stream->get_class().replace_first("AudioStream", "");1748}17491750stream_preview_rect->draw_texture(icon, Point2i(EDSCALE * 4, rect.position.y + (rect.size.height - icon->get_height()) / 2), icon_modulate);1751stream_preview_rect->draw_string(font, Point2i(EDSCALE * 4 + icon->get_width(), rect.position.y + font->get_ascent(font_size) + (rect.size.height - font->get_height(font_size)) / 2), text, HORIZONTAL_ALIGNMENT_CENTER, size.width - 4 * EDSCALE - icon->get_width(), font_size, get_theme_color(SceneStringName(font_color), EditorStringName(Editor)));1752}17531754EditorAudioStreamPicker::EditorAudioStreamPicker() :1755EditorResourcePicker(true) {1756stream_preview_rect = memnew(Control);17571758stream_preview_rect->set_anchors_and_offsets_preset(PRESET_FULL_RECT);1759stream_preview_rect->set_offset(SIDE_TOP, 1);1760stream_preview_rect->set_offset(SIDE_BOTTOM, -1);1761stream_preview_rect->set_offset(SIDE_RIGHT, -1);1762stream_preview_rect->set_mouse_filter(MOUSE_FILTER_IGNORE);1763stream_preview_rect->connect(SceneStringName(draw), callable_mp(this, &EditorAudioStreamPicker::_preview_draw));17641765get_assign_button()->add_child(stream_preview_rect);1766get_assign_button()->move_child(stream_preview_rect, 0);1767set_process_internal(true);1768}176917701771