From 343ba504d30db1232acbf599c83462d9b76daa05 Mon Sep 17 00:00:00 2001 From: Yuri Sizov Date: Thu, 10 Jun 2021 19:07:48 +0300 Subject: [PATCH 1/2] Add EditorResourcePicker control based on the Inspector editor for Resources Backported from #47260 --- doc/classes/EditorResourcePicker.xml | 115 ++++ doc/classes/EditorScriptPicker.xml | 21 + editor/editor_node.cpp | 3 + editor/editor_resource_picker.cpp | 869 +++++++++++++++++++++++++++ editor/editor_resource_picker.h | 141 +++++ 5 files changed, 1149 insertions(+) create mode 100644 doc/classes/EditorResourcePicker.xml create mode 100644 doc/classes/EditorScriptPicker.xml create mode 100644 editor/editor_resource_picker.cpp create mode 100644 editor/editor_resource_picker.h diff --git a/doc/classes/EditorResourcePicker.xml b/doc/classes/EditorResourcePicker.xml new file mode 100644 index 000000000000..0803dc25811f --- /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 000000000000..761219d319e6 --- /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 ef04cafadf81..e8245fe687f9 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 000000000000..767f39a27150 --- /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