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;