diff --git a/doc/classes/EditorResourcePicker.xml b/doc/classes/EditorResourcePicker.xml
new file mode 100644
index 00000000000..0803dc25811
--- /dev/null
+++ b/doc/classes/EditorResourcePicker.xml
@@ -0,0 +1,115 @@
+
+
+
+ Godot editor's control for selecting [Resource] type properties.
+
+
+ This [Control] node is used in the editor's Inspector dock to allow editing of [Resource] type properties. It provides options for creating, loading, saving and converting resources. Can be used with [EditorInspectorPlugin] to recreate the same behavior.
+ [b]Note:[/b] This [Control] does not include any editor for the resource, as editing is controlled by the Inspector dock itself or sub-Inspectors.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Returns a list of all allowed types and subtypes corresponding to the [member base_type]. If the [member base_type] is empty, an empty list is returned.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This virtual method can be implemented to handle context menu items not handled by default. See [method set_create_options].
+
+
+
+
+
+
+
+
+ This virtual method is called when updating the context menu of [EditorResourcePicker]. Implement this method to override the "New ..." items with your own options. [code]menu_node[/code] is a reference to the [PopupMenu] node.
+ [b]Note:[/b] Implement [method handle_menu_selected] to handle these custom items.
+
+
+
+
+
+
+
+
+ Sets the toggle mode state for the main button. Works only if [member toggle_mode] is set to [code]true[/code].
+
+
+
+
+
+ The base type of allowed resource types. Can be a comma-separated list of several options.
+
+
+ If [code]true[/code], the value can be selected and edited.
+
+
+ The edited resource value.
+
+
+ If [code]true[/code], the main button with the resource preview works in the toggle mode. Use [method set_toggle_pressed] to manually set the state.
+
+
+
+
+
+
+
+ Emitted when the value of the edited resource was changed.
+
+
+
+
+
+
+ Emitted when the resource value was set and user clicked to edit it.
+
+
+
+
+
+
diff --git a/doc/classes/EditorScriptPicker.xml b/doc/classes/EditorScriptPicker.xml
new file mode 100644
index 00000000000..761219d319e
--- /dev/null
+++ b/doc/classes/EditorScriptPicker.xml
@@ -0,0 +1,21 @@
+
+
+
+ Godot editor's control for selecting the [code]script[/code] property of a [Node].
+
+
+ Similar to [EditorResourcePicker] this [Control] node is used in the editor's Inspector dock, but only to edit the [code]script[/code] property of a [Node]. Default options for creating new resources of all possible subtypes are replaced with dedicated buttons that open the "Attach Node Script" dialog. Can be used with [EditorInspectorPlugin] to recreate the same behavior.
+ [b]Note:[/b] You must set the [member script_owner] for the custom context menu items to work.
+
+
+
+
+
+
+
+ The owner [Node] of the script property that holds the edited resource.
+
+
+
+
+
diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp
index ef04cafadf8..e8245fe687f 100644
--- a/editor/editor_node.cpp
+++ b/editor/editor_node.cpp
@@ -79,6 +79,7 @@
#include "editor/editor_log.h"
#include "editor/editor_plugin.h"
#include "editor/editor_properties.h"
+#include "editor/editor_resource_picker.h"
#include "editor/editor_resource_preview.h"
#include "editor/editor_run_native.h"
#include "editor/editor_run_script.h"
@@ -3799,6 +3800,8 @@ void EditorNode::register_editor_types() {
ClassDB::register_class();
ClassDB::register_class();
ClassDB::register_class();
+ ClassDB::register_class();
+ ClassDB::register_class();
ClassDB::register_virtual_class();
// FIXME: Is this stuff obsolete, or should it be ported to new APIs?
diff --git a/editor/editor_resource_picker.cpp b/editor/editor_resource_picker.cpp
new file mode 100644
index 00000000000..767f39a2715
--- /dev/null
+++ b/editor/editor_resource_picker.cpp
@@ -0,0 +1,869 @@
+/*************************************************************************/
+/* editor_resource_picker.cpp */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#include "editor_resource_picker.h"
+
+#include "editor/editor_resource_preview.h"
+#include "editor_node.h"
+#include "editor_scale.h"
+#include "editor_settings.h"
+#include "filesystem_dock.h"
+#include "scene/main/viewport.h"
+#include "scene/resources/dynamic_font.h"
+
+void EditorResourcePicker::_update_resource() {
+ preview_rect->set_texture(Ref());
+ assign_button->set_custom_minimum_size(Size2(1, 1));
+
+ if (edited_resource == RES()) {
+ assign_button->set_icon(Ref());
+ assign_button->set_text(TTR("[empty]"));
+ } else {
+ assign_button->set_icon(EditorNode::get_singleton()->get_object_icon(edited_resource.operator->(), "Object"));
+
+ if (edited_resource->get_name() != String()) {
+ assign_button->set_text(edited_resource->get_name());
+ } else if (edited_resource->get_path().is_resource_file()) {
+ assign_button->set_text(edited_resource->get_path().get_file());
+ assign_button->set_tooltip(edited_resource->get_path());
+ } else {
+ assign_button->set_text(edited_resource->get_class());
+ }
+
+ if (edited_resource->get_path().is_resource_file()) {
+ assign_button->set_tooltip(edited_resource->get_path());
+ }
+
+ // Preview will override the above, so called at the end.
+ EditorResourcePreview::get_singleton()->queue_edited_resource_preview(edited_resource, this, "_update_resource_preview", edited_resource->get_instance_id());
+ }
+}
+
+void EditorResourcePicker::_update_resource_preview(const String &p_path, const Ref &p_preview, const Ref &p_small_preview, ObjectID p_obj) {
+ if (!edited_resource.is_valid() || edited_resource->get_instance_id() != p_obj) {
+ return;
+ }
+
+ String type = edited_resource->get_class_name();
+ if (ClassDB::class_exists(type) && ClassDB::is_parent_class(type, "Script")) {
+ assign_button->set_text(edited_resource->get_path().get_file());
+ return;
+ }
+
+ if (p_preview.is_valid()) {
+ preview_rect->set_margin(MARGIN_LEFT, assign_button->get_icon()->get_width() + assign_button->get_stylebox("normal")->get_default_margin(MARGIN_LEFT) + get_constant("hseparation", "Button"));
+
+ if (type == "GradientTexture") {
+ preview_rect->set_stretch_mode(TextureRect::STRETCH_SCALE);
+ assign_button->set_custom_minimum_size(Size2(1, 1));
+ } else {
+ preview_rect->set_stretch_mode(TextureRect::STRETCH_KEEP_ASPECT_CENTERED);
+ int thumbnail_size = EditorSettings::get_singleton()->get("filesystem/file_dialog/thumbnail_size");
+ thumbnail_size *= EDSCALE;
+ assign_button->set_custom_minimum_size(Size2(1, thumbnail_size));
+ }
+
+ preview_rect->set_texture(p_preview);
+ assign_button->set_text("");
+ }
+}
+
+void EditorResourcePicker::_resource_selected() {
+ if (edited_resource.is_null()) {
+ edit_button->set_pressed(true);
+ _update_menu();
+ return;
+ }
+
+ emit_signal("resource_selected", edited_resource);
+}
+
+void EditorResourcePicker::_file_selected(const String &p_path) {
+ RES loaded_resource = ResourceLoader::load(p_path);
+ ERR_FAIL_COND_MSG(loaded_resource.is_null(), "Cannot load resource from path '" + p_path + "'.");
+
+ if (base_type != "") {
+ bool any_type_matches = false;
+
+ for (int i = 0; i < base_type.get_slice_count(","); i++) {
+ String base = base_type.get_slice(",", i);
+ if (loaded_resource->is_class(base)) {
+ any_type_matches = true;
+ break;
+ }
+ }
+
+ if (!any_type_matches) {
+ EditorNode::get_singleton()->show_warning(vformat(TTR("The selected resource (%s) does not match any type expected for this property (%s)."), loaded_resource->get_class(), base_type));
+ return;
+ }
+ }
+
+ edited_resource = loaded_resource;
+ emit_signal("resource_changed", edited_resource);
+ _update_resource();
+}
+
+void EditorResourcePicker::_update_menu() {
+ _update_menu_items();
+
+ Rect2 gt = edit_button->get_global_rect();
+ edit_menu->set_as_minsize();
+ int ms = edit_menu->get_combined_minimum_size().width;
+ Vector2 popup_pos = gt.position + gt.size - Vector2(ms, 0);
+ edit_menu->set_global_position(popup_pos);
+ edit_menu->popup();
+}
+
+void EditorResourcePicker::_update_menu_items() {
+ edit_menu->clear();
+
+ // Add options for creating specific subtypes of the base resource type.
+ set_create_options(edit_menu);
+
+ // Add an option to load a resource from a file.
+ edit_menu->add_icon_item(get_icon("Load", "EditorIcons"), TTR("Load"), OBJ_MENU_LOAD);
+
+ // Add options for changing existing value of the resource.
+ if (edited_resource.is_valid()) {
+ edit_menu->add_icon_item(get_icon("Edit", "EditorIcons"), TTR("Edit"), OBJ_MENU_EDIT);
+ edit_menu->add_icon_item(get_icon("Clear", "EditorIcons"), TTR("Clear"), OBJ_MENU_CLEAR);
+ edit_menu->add_icon_item(get_icon("Duplicate", "EditorIcons"), TTR("Make Unique"), OBJ_MENU_MAKE_UNIQUE);
+ edit_menu->add_icon_item(get_icon("Save", "EditorIcons"), TTR("Save"), OBJ_MENU_SAVE);
+
+ if (edited_resource->get_path().is_resource_file()) {
+ edit_menu->add_separator();
+ edit_menu->add_item(TTR("Show in FileSystem"), OBJ_MENU_SHOW_IN_FILE_SYSTEM);
+ }
+ }
+
+ // Add options to copy/paste resource.
+ RES cb = EditorSettings::get_singleton()->get_resource_clipboard();
+ bool paste_valid = false;
+ if (cb.is_valid()) {
+ if (base_type == "") {
+ paste_valid = true;
+ } else {
+ for (int i = 0; i < base_type.get_slice_count(","); i++) {
+ if (ClassDB::class_exists(cb->get_class()) && ClassDB::is_parent_class(cb->get_class(), base_type.get_slice(",", i))) {
+ paste_valid = true;
+ break;
+ }
+ }
+ }
+ }
+
+ if (edited_resource.is_valid() || paste_valid) {
+ edit_menu->add_separator();
+
+ if (edited_resource.is_valid()) {
+ edit_menu->add_item(TTR("Copy"), OBJ_MENU_COPY);
+ }
+
+ if (paste_valid) {
+ edit_menu->add_item(TTR("Paste"), OBJ_MENU_PASTE);
+ }
+ }
+
+ // Add options to convert existing resource to another type of resource.
+ if (edited_resource.is_valid()) {
+ Vector[> conversions = EditorNode::get_singleton()->find_resource_conversion_plugin(edited_resource);
+ if (conversions.size()) {
+ edit_menu->add_separator();
+ }
+ for (int i = 0; i < conversions.size(); i++) {
+ String what = conversions[i]->converts_to();
+ Ref icon;
+ if (has_icon(what, "EditorIcons")) {
+ icon = get_icon(what, "EditorIcons");
+ } else {
+ icon = get_icon(what, "Resource");
+ }
+
+ edit_menu->add_icon_item(icon, vformat(TTR("Convert To %s"), what), CONVERT_BASE_ID + i);
+ }
+ }
+}
+
+void EditorResourcePicker::_edit_menu_cbk(int p_which) {
+ switch (p_which) {
+ case OBJ_MENU_LOAD: {
+ List extensions;
+ for (int i = 0; i < base_type.get_slice_count(","); i++) {
+ String base = base_type.get_slice(",", i);
+ ResourceLoader::get_recognized_extensions_for_type(base, &extensions);
+ }
+
+ Set valid_extensions;
+ for (List::Element *E = extensions.front(); E; E = E->next()) {
+ valid_extensions.insert(E->get());
+ }
+
+ if (!file_dialog) {
+ file_dialog = memnew(EditorFileDialog);
+ file_dialog->set_mode(EditorFileDialog::MODE_OPEN_FILE);
+ add_child(file_dialog);
+ file_dialog->connect("file_selected", this, "_file_selected");
+ }
+
+ file_dialog->clear_filters();
+ for (Set::Element *E = valid_extensions.front(); E; E = E->next()) {
+ file_dialog->add_filter("*." + E->get() + " ; " + E->get().to_upper());
+ }
+
+ file_dialog->popup_centered_ratio();
+ } break;
+
+ case OBJ_MENU_EDIT: {
+ if (edited_resource.is_valid()) {
+ emit_signal("resource_selected", edited_resource);
+ }
+ } break;
+
+ case OBJ_MENU_CLEAR: {
+ edited_resource = RES();
+ emit_signal("resource_changed", edited_resource);
+ _update_resource();
+ } break;
+
+ case OBJ_MENU_MAKE_UNIQUE: {
+ if (edited_resource.is_null()) {
+ return;
+ }
+
+ List property_list;
+ edited_resource->get_property_list(&property_list);
+ List> propvalues;
+ for (List::Element *E = property_list.front(); E; E = E->next()) {
+ Pair p;
+ PropertyInfo &pi = E->get();
+ if (pi.usage & PROPERTY_USAGE_STORAGE) {
+ p.first = pi.name;
+ p.second = edited_resource->get(pi.name);
+ }
+
+ propvalues.push_back(p);
+ }
+
+ String orig_type = edited_resource->get_class();
+ Object *inst = ClassDB::instance(orig_type);
+ Ref unique_resource = Ref(Object::cast_to(inst));
+ ERR_FAIL_COND(unique_resource.is_null());
+
+ for (List>::Element *E = propvalues.front(); E; E = E->next()) {
+ Pair &p = E->get();
+ unique_resource->set(p.first, p.second);
+ }
+
+ edited_resource = unique_resource;
+ emit_signal("resource_changed", edited_resource);
+ _update_resource();
+ } break;
+
+ case OBJ_MENU_SAVE: {
+ if (edited_resource.is_null()) {
+ return;
+ }
+ EditorNode::get_singleton()->save_resource(edited_resource);
+ } break;
+
+ case OBJ_MENU_COPY: {
+ EditorSettings::get_singleton()->set_resource_clipboard(edited_resource);
+ } break;
+
+ case OBJ_MENU_PASTE: {
+ edited_resource = EditorSettings::get_singleton()->get_resource_clipboard();
+ emit_signal("resource_changed", edited_resource);
+ _update_resource();
+ } break;
+
+ case OBJ_MENU_SHOW_IN_FILE_SYSTEM: {
+ FileSystemDock *file_system_dock = EditorNode::get_singleton()->get_filesystem_dock();
+ file_system_dock->navigate_to_path(edited_resource->get_path());
+
+ // Ensure that the FileSystem dock is visible.
+ TabContainer *tab_container = (TabContainer *)file_system_dock->get_parent_control();
+ tab_container->set_current_tab(file_system_dock->get_index());
+ } break;
+
+ default: {
+ // Allow subclasses to handle their own options first, only then fallback on the default branch logic.
+ if (handle_menu_selected(p_which)) {
+ break;
+ }
+
+ if (p_which >= CONVERT_BASE_ID) {
+ int to_type = p_which - CONVERT_BASE_ID;
+ Vector][> conversions = EditorNode::get_singleton()->find_resource_conversion_plugin(edited_resource);
+ ERR_FAIL_INDEX(to_type, conversions.size());
+
+ edited_resource = conversions[to_type]->convert(edited_resource);
+ emit_signal("resource_changed", edited_resource);
+ _update_resource();
+ break;
+ }
+
+ ERR_FAIL_COND(inheritors_array.empty());
+
+ String intype = inheritors_array[p_which - TYPE_BASE_ID];
+ Variant obj;
+
+ if (ScriptServer::is_global_class(intype)) {
+ obj = ClassDB::instance(ScriptServer::get_global_class_native_base(intype));
+ if (obj) {
+ Ref]