diff --git a/core/core_constants.cpp b/core/core_constants.cpp index 4b858c55142..06238371eb2 100644 --- a/core/core_constants.cpp +++ b/core/core_constants.cpp @@ -678,6 +678,7 @@ void register_global_constants() { BIND_CORE_ENUM_CONSTANT(PROPERTY_HINT_PASSWORD); BIND_CORE_ENUM_CONSTANT(PROPERTY_HINT_TOOL_BUTTON); BIND_CORE_ENUM_CONSTANT(PROPERTY_HINT_ONESHOT); + BIND_CORE_ENUM_CONSTANT(PROPERTY_HINT_ORDERED_LIST); BIND_CORE_ENUM_CONSTANT(PROPERTY_HINT_MAX); BIND_CORE_BITFIELD_FLAG(PROPERTY_USAGE_NONE); diff --git a/core/object/object.h b/core/object/object.h index 16d8e19eaa0..b4563cb1961 100644 --- a/core/object/object.h +++ b/core/object/object.h @@ -90,6 +90,7 @@ enum PropertyHint { PROPERTY_HINT_TOOL_BUTTON, PROPERTY_HINT_ONESHOT, ///< the property will be changed by self after setting, such as AudioStreamPlayer.playing, Particles.emitting. PROPERTY_HINT_NO_NODEPATH, /// < this property will not contain a NodePath, regardless of type (Array, Dictionary, List, etc.). Needed for SceneTreeDock. + PROPERTY_HINT_ORDERED_LIST, PROPERTY_HINT_MAX, }; diff --git a/doc/classes/@GlobalScope.xml b/doc/classes/@GlobalScope.xml index 38b1ec3554b..2f3a911fcaf 100644 --- a/doc/classes/@GlobalScope.xml +++ b/doc/classes/@GlobalScope.xml @@ -2954,7 +2954,11 @@ Hints that a property will be changed on its own after setting, such as [member AudioStreamPlayer.playing] or [member GPUParticles3D.emitting]. - + + Hints that a property is ordered list of values. + The hint string is a comma separated list of display name/value pairs separated by [code]:[/code] such as [code]"Hello:hello,Something Else:else"[/code]. + + Represents the size of the [enum PropertyHint] enum. diff --git a/editor/editor_properties.cpp b/editor/editor_properties.cpp index 52780d7c588..e8095ca6415 100644 --- a/editor/editor_properties.cpp +++ b/editor/editor_properties.cpp @@ -52,6 +52,7 @@ #include "scene/3d/gpu_particles_3d.h" #include "scene/gui/color_picker.h" #include "scene/gui/grid_container.h" +#include "scene/gui/tree.h" #include "scene/main/window.h" #include "scene/resources/font.h" #include "scene/resources/mesh.h" @@ -249,6 +250,225 @@ EditorPropertyMultilineText::EditorPropertyMultilineText(bool p_expression) { } } +///////////////////// ORDERED LIST ///////////////////////// + +void EditorPropertyOrderedList::_set_read_only(bool p_read_only) { + read_only = p_read_only; + _update_tree(); +} + +void EditorPropertyOrderedList::_update_tree() { + tree->clear(); + + TreeItem *root = tree->create_item(nullptr); + // Add missing keys to the value. + for (const KeyValue &E : names) { + if (!values.has(E.key)) { + values.push_back(E.key); + } + } + + // Update tree. + for (int i = 0; i < values.size(); i++) { + if (!names.has(values[i])) { + WARN_PRINT(vformat("Invalid list item %s for property %s.", values[i], get_edited_property())); + continue; + } + TreeItem *it = tree->create_item(root); + it->set_text(0, names[values[i]]); + it->set_metadata(0, values[i]); + if (!read_only) { + it->add_button(0, get_editor_theme_icon(SNAME("ArrowUp")), BUTTON_UP, (i == 0), TTR("Move Up")); + it->add_button(0, get_editor_theme_icon(SNAME("ArrowDown")), BUTTON_DOWN, (i == values.size() - 1), TTR("Move Down")); + } + } + + // Update size. + Ref font = get_theme_font(SceneStringName(font), SNAME("TextEdit")); + int font_size = get_theme_font_size(SceneStringName(font_size), SNAME("TextEdit")); + tree->set_custom_minimum_size(tree->get_background_size() + Size2i(0, MIN(tree->get_internal_min_size().height, 6 * font->get_height(font_size)))); +} + +Variant EditorPropertyOrderedList::get_drag_data_fw(const Point2 &p_point, Control *p_from) { + if (read_only) { + return Variant(); + } + + if (tree->get_button_id_at_position(p_point) != -1) { + return Variant(); + } + + TreeItem *item = tree->get_next_selected(nullptr); + if (!item) { + return Variant(); + } + String value = item->get_metadata(0); + if (!names.has(value)) { + return Variant(); + } + String name = names[value]; + + // Preview. + HBoxContainer *hb = memnew(HBoxContainer); + Label *label_prev = memnew(Label(vformat("%s (%s)", name, value))); + label_prev->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED); + hb->add_child(label_prev); + set_drag_preview(hb); + + // Drag data. + Dictionary drag_data; + drag_data["type"] = "list_reorder"; + drag_data["id"] = get_instance_id(); + drag_data["value"] = value; + + tree->set_drop_mode_flags(Tree::DROP_MODE_INBETWEEN); + + return drag_data; +} + +bool EditorPropertyOrderedList::can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const { + if (read_only) { + return false; + } + + Dictionary d = p_data; + if (!d.has("type") || !d.has("id") || !d.has("value")) { + return false; + } + + if (d["type"] != "list_reorder" || d["id"] != get_instance_id()) { + return false; + } + + TreeItem *item = tree->get_item_at_position(p_point); + if (!item) { + return false; + } + + int section = tree->get_drop_section_at_position(p_point); + if (section == -100) { + return false; + } + + return true; +} + +void EditorPropertyOrderedList::drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) { + if (!can_drop_data_fw(p_point, p_data, p_from)) { + return; + } + Dictionary d = p_data; + String drop_value = d["value"]; + + TreeItem *item = tree->get_item_at_position(p_point); + ERR_FAIL_COND(!item); + String target_value = item->get_metadata(0); + + int section = tree->get_drop_section_at_position(p_point); + ERR_FAIL_COND(section == -100); + + tree->set_drop_mode_flags(Tree::DROP_MODE_DISABLED); + + Vector new_values = values; + new_values.erase(drop_value); + if (section == 1) { + int pos = new_values.find(target_value); + if (pos == -1) { + return; + } + new_values.insert(pos + 1, drop_value); + } else { + int pos = new_values.find(target_value); + if (pos == -1) { + return; + } + new_values.insert(pos, drop_value); + } + values = new_values; + _update_tree(); + emit_changed(get_edited_property(), String(",").join(values)); +} + +void EditorPropertyOrderedList::_tree_button_pressed(TreeItem *p_item, int p_column, int p_id, MouseButton p_button) { + ERR_FAIL_COND(!p_item); + if (p_button != MouseButton::LEFT) { + return; + } + + String drop_value = p_item->get_metadata(0); + Vector new_values = values; + if (p_id == BUTTON_DOWN) { + int pos = new_values.find(drop_value); + if (pos == -1) { + return; + } + new_values.erase(drop_value); + new_values.insert(pos + 1, drop_value); + } else { + int pos = new_values.find(drop_value); + if (pos == -1) { + return; + } + new_values.erase(drop_value); + new_values.insert(pos - 1, drop_value); + } + values = new_values; + _update_tree(); + emit_changed(get_edited_property(), String(",").join(values)); +} + +void EditorPropertyOrderedList::update_property() { + String current_value = get_edited_property_value(); + values = current_value.split(","); + print_line(values); + _update_tree(); +} + +void EditorPropertyOrderedList::setup(const Vector &p_options) { + values.clear(); + names.clear(); + + for (const String &option : p_options) { + Vector text_split = option.split(":"); + if (text_split.size() != 1) { + names[text_split[1]] = text_split[0]; + } else { + names[text_split[0]] = text_split[0]; + } + } + _update_tree(); +} + +void EditorPropertyOrderedList::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_ENTER_TREE: + case NOTIFICATION_THEME_CHANGED: { + tree->add_theme_style_override(SNAME("panel"), get_theme_stylebox(SNAME("normal"), SNAME("Button"))); + tree->add_theme_style_override(SNAME("focus"), get_theme_stylebox(SNAME("focus"), SNAME("Button"))); + _update_tree(); + } break; + } +} + +EditorPropertyOrderedList::EditorPropertyOrderedList() { + HBoxContainer *hb = memnew(HBoxContainer); + add_child(hb); + + default_layout = memnew(HBoxContainer); + default_layout->set_h_size_flags(SIZE_EXPAND_FILL); + hb->add_child(default_layout); + + tree = memnew(Tree); + tree->set_h_size_flags(SIZE_EXPAND_FILL); + tree->set_hide_root(true); + tree->connect("button_clicked", callable_mp(this, &EditorPropertyOrderedList::_tree_button_pressed)); + default_layout->add_child(tree); + + SET_DRAG_FORWARDING_GCD(tree, EditorPropertyOrderedList); + + add_focusable(tree); +} + ///////////////////// TEXT ENUM ///////////////////////// void EditorPropertyTextEnum::_set_read_only(bool p_read_only) { @@ -3615,7 +3835,12 @@ EditorProperty *EditorInspectorDefaultPlugin::get_editor_for_property(Object *p_ } } break; case Variant::STRING: { - if (p_hint == PROPERTY_HINT_ENUM || p_hint == PROPERTY_HINT_ENUM_SUGGESTION) { + if (p_hint == PROPERTY_HINT_ORDERED_LIST) { + EditorPropertyOrderedList *editor = memnew(EditorPropertyOrderedList); + Vector options = p_hint_text.split(",", false); + editor->setup(options); + return editor; + } else if (p_hint == PROPERTY_HINT_ENUM || p_hint == PROPERTY_HINT_ENUM_SUGGESTION) { EditorPropertyTextEnum *editor = memnew(EditorPropertyTextEnum); Vector options = p_hint_text.split(",", false); editor->setup(options, false, (p_hint == PROPERTY_HINT_ENUM_SUGGESTION)); diff --git a/editor/editor_properties.h b/editor/editor_properties.h index 6fe8cc3c3f3..66c5f282dd3 100644 --- a/editor/editor_properties.h +++ b/editor/editor_properties.h @@ -45,6 +45,8 @@ class PropertySelector; class SceneTreeDialog; class TextEdit; class TextureButton; +class Tree; +class TreeItem; class EditorPropertyNil : public EditorProperty { GDCLASS(EditorPropertyNil, EditorProperty); @@ -132,6 +134,39 @@ public: EditorPropertyTextEnum(); }; +class EditorPropertyOrderedList : public EditorProperty { + GDCLASS(EditorPropertyOrderedList, EditorProperty); + + enum EditorPropertyOrderedListButtonID { + BUTTON_UP, + BUTTON_DOWN, + }; + + HBoxContainer *default_layout = nullptr; + + bool read_only = false; + Tree *tree = nullptr; + + HashMap names; + Vector values; + + void _update_tree(); + void _tree_button_pressed(TreeItem *p_item, int p_column, int p_id, MouseButton p_button); + +protected: + virtual void _set_read_only(bool p_read_only) override; + void _notification(int p_what); + + Variant get_drag_data_fw(const Point2 &p_point, Control *p_from); + bool can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const; + void drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from); + +public: + void setup(const Vector &p_options); + virtual void update_property() override; + EditorPropertyOrderedList(); +}; + class EditorPropertyPath : public EditorProperty { GDCLASS(EditorPropertyPath, EditorProperty); Vector extensions; diff --git a/scene/gui/tree.cpp b/scene/gui/tree.cpp index cd6c51de4b2..899fb4bfb71 100644 --- a/scene/gui/tree.cpp +++ b/scene/gui/tree.cpp @@ -4196,10 +4196,20 @@ void Tree::set_editor_selection(int p_from_line, int p_to_line, int p_from_colum } } +Size2 Tree::get_background_size() const { + const Ref background = theme_cache.panel_style; + + // This is the background stylebox's content rect. + const real_t width = background->get_margin(SIDE_LEFT) + background->get_margin(SIDE_RIGHT); + const real_t height = background->get_margin(SIDE_TOP) + background->get_margin(SIDE_BOTTOM); + return Size2(width, height); +} + Size2 Tree::get_internal_min_size() const { Size2i size; if (root) { size.height += get_item_height(root); + size.height -= theme_cache.v_separation; } for (int i = 0; i < columns.size(); i++) { size.width += get_column_minimum_width(i); diff --git a/scene/gui/tree.h b/scene/gui/tree.h index 9d3324bd3db..17632361518 100644 --- a/scene/gui/tree.h +++ b/scene/gui/tree.h @@ -657,7 +657,6 @@ private: bool h_scroll_enabled = true; bool v_scroll_enabled = true; - Size2 get_internal_min_size() const; void update_scrollbars(); Rect2 search_item_rect(TreeItem *p_from, TreeItem *p_item); @@ -798,6 +797,8 @@ public: void ensure_cursor_is_visible(); Rect2 get_custom_popup_rect() const; + Size2 get_background_size() const; + Size2 get_internal_min_size() const; int get_item_offset(TreeItem *p_item) const; Rect2 get_item_rect(TreeItem *p_item, int p_column = -1, int p_button = -1) const;