diff --git a/icons/collapsed.svg b/icons/collapsed.svg
new file mode 100644
index 0000000..2bfd6e3
--- /dev/null
+++ b/icons/collapsed.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/icons/expanded.svg b/icons/expanded.svg
new file mode 100644
index 0000000..11468e6
--- /dev/null
+++ b/icons/expanded.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/icons/inspector.svg b/icons/inspector.svg
index 780ff96..6072d61 100644
--- a/icons/inspector.svg
+++ b/icons/inspector.svg
@@ -1,3 +1 @@
-
+
\ No newline at end of file
diff --git a/icons/inspector_container.svg b/icons/inspector_container.svg
index 9af3acc..bd61783 100644
--- a/icons/inspector_container.svg
+++ b/icons/inspector_container.svg
@@ -1,3 +1 @@
-
+
\ No newline at end of file
diff --git a/icons/maximize.svg b/icons/maximize.svg
index 97611e7..38eb7b4 100644
--- a/icons/maximize.svg
+++ b/icons/maximize.svg
@@ -1,3 +1 @@
-
+
\ No newline at end of file
diff --git a/icons/search.svg b/icons/search.svg
index 760adff..6efbef1 100644
--- a/icons/search.svg
+++ b/icons/search.svg
@@ -1,3 +1 @@
-
+
\ No newline at end of file
diff --git a/inspector_theme.tres b/inspector_theme.tres
new file mode 100644
index 0000000..62b4707
--- /dev/null
+++ b/inspector_theme.tres
@@ -0,0 +1,80 @@
+[gd_resource type="Theme" load_steps=9 format=3 uid="uid://dyf67c0ud50ts"]
+
+[ext_resource type="Texture2D" uid="uid://cr6jrnuywr8g4" path="res://addons/object-inspector/icons/maximize.svg" id="1_hfaeu"]
+[ext_resource type="Texture2D" uid="uid://cfw5fv6chuy5o" path="res://addons/object-inspector/icons/collapsed.svg" id="1_hpca1"]
+[ext_resource type="Texture2D" uid="uid://dec8bwwwl2po1" path="res://addons/object-inspector/icons/expanded.svg" id="2_0jwgf"]
+[ext_resource type="Texture2D" uid="uid://bq3g4y2emis6p" path="res://addons/object-inspector/icons/search.svg" id="2_vebvf"]
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_h08j0"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_3ctcn"]
+content_margin_left = 2.0
+content_margin_top = 2.0
+content_margin_right = 2.0
+content_margin_bottom = 2.0
+bg_color = Color(0.5, 0.25, 0.25, 0.25)
+border_width_left = 1
+border_width_top = 1
+border_width_right = 1
+border_width_bottom = 1
+border_color = Color(0.5, 0.25, 0.25, 0.5)
+corner_radius_top_left = 2
+corner_radius_top_right = 2
+corner_radius_bottom_right = 2
+corner_radius_bottom_left = 2
+corner_detail = 2
+anti_aliasing = false
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_idiuy"]
+content_margin_left = 2.0
+content_margin_top = 2.0
+content_margin_right = 2.0
+content_margin_bottom = 2.0
+bg_color = Color(0.45, 0.5, 0.25, 0.25)
+border_width_left = 1
+border_width_top = 1
+border_width_right = 1
+border_width_bottom = 1
+border_color = Color(0.45, 0.5, 0.25, 0.5)
+corner_radius_top_left = 2
+corner_radius_top_right = 2
+corner_radius_bottom_right = 2
+corner_radius_bottom_left = 2
+corner_detail = 2
+anti_aliasing = false
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_cncyr"]
+content_margin_left = 2.0
+content_margin_top = 2.0
+content_margin_right = 2.0
+content_margin_bottom = 2.0
+bg_color = Color(0.25, 0.5, 0.35, 0.25)
+border_width_left = 1
+border_width_top = 1
+border_width_right = 1
+border_width_bottom = 1
+border_color = Color(0.25, 0.5, 0.35, 0.5)
+corner_radius_top_left = 2
+corner_radius_top_right = 2
+corner_radius_bottom_right = 2
+corner_radius_bottom_left = 2
+corner_detail = 2
+anti_aliasing = false
+
+[resource]
+BoxContainer/constants/separation = 1
+FlowContainer/constants/h_separation = 1
+FlowContainer/constants/v_separation = 1
+Inspector/base_type = &"VBoxContainer"
+Inspector/icons/maximize = ExtResource("1_hfaeu")
+Inspector/icons/search = ExtResource("2_vebvf")
+InspectorProperty/base_type = &"PanelContainer"
+InspectorProperty/styles/panel = SubResource("StyleBoxEmpty_h08j0")
+InspectorPropertyCategory/base_type = &"PanelContainer"
+InspectorPropertyCategory/styles/panel = SubResource("StyleBoxFlat_3ctcn")
+InspectorPropertyGroup/base_type = &"PanelContainer"
+InspectorPropertyGroup/icons/collapsed = ExtResource("1_hpca1")
+InspectorPropertyGroup/icons/expanded = ExtResource("2_0jwgf")
+InspectorPropertyGroup/styles/panel = SubResource("StyleBoxFlat_idiuy")
+InspectorPropertySubGroup/base_type = &"InspectorPropertyGroup"
+InspectorPropertySubGroup/styles/panel = SubResource("StyleBoxFlat_cncyr")
diff --git a/scripts/inspector.gd b/scripts/inspector.gd
index ee4208e..c5d2d74 100644
--- a/scripts/inspector.gd
+++ b/scripts/inspector.gd
@@ -9,9 +9,8 @@ extends VBoxContainer
## Emitted when object changed.
signal object_changed(object: Object)
-# Magic numbers, but otherwise the SpinBox does not work correctly.
-const FLOAT_MIN = -999999999999.9
-const FLOAT_MAX = 999999999999.9
+# INFO: Required for static initialization.
+const InspectorProperties = preload("res://addons/object-inspector/scripts/inspector_properties.gd")
@export
@@ -24,59 +23,58 @@ var _search_enabled := true:
set = set_search_enabled,
get = is_search_enabled
+@export
+var _category_enadled: bool = true:
+ set = set_category_enabled,
+ get = is_category_enabled
+
+@export
+var _group_enabled: bool = true:
+ set = set_group_enabled,
+ get = is_group_enabled
+
+@export
+var _subgroup_enabled: bool = true:
+ set = set_subgroup_enabled,
+ get = is_subgroup_enabled
-var _properties : Array[InspectorProperty]
-var _object : Object
+var _object : Object = null
+var _valid_properties: Array[Dictionary] = []
-var _search : LineEdit
+var _search : LineEdit = null
+
+var _scroll_container : ScrollContainer = null
+var _container : VBoxContainer = null
+
+var _group_states: Dictionary = {}
+var _subgroup_states: Dictionary = {}
-var _scroll_container : ScrollContainer
-var _container : VBoxContainer
-var _group_states: Dictionary
-var _subgroup_states: Dictionary
func _init() -> void:
+ self.set_theme_type_variation(&"Inspector")
+
+ # INFO: Required for static initialization.
+ load("res://addons/object-inspector/scripts/inspector_property_array.gd")
+ load("res://addons/object-inspector/scripts/inspector_property_dictionary.gd")
+
_search = LineEdit.new()
- _search.placeholder_text = tr("Filter properties")
- _search.editable = false
- _search.clear_button_enabled = true
- _search.right_icon = load("addons/object-inspector/icons/search.svg")
- _search.visible = _search_enabled
- _search.size_flags_horizontal = SIZE_EXPAND_FILL
- _search.text_changed.connect(update_inspector)
+ _search.set_placeholder("Filter properties")
+ _search.set_editable(false)
+ _search.set_clear_button_enabled(true)
+ _search.set_visible(is_search_enabled())
+ _search.set_h_size_flags(Control.SIZE_EXPAND_FILL)
+ _search.text_changed.connect(_on_filter_text_chnaged)
self.add_child(_search)
_scroll_container = ScrollContainer.new()
- _scroll_container.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
- _scroll_container.size_flags_horizontal = SIZE_EXPAND_FILL
- _scroll_container.size_flags_vertical = SIZE_EXPAND_FILL
+ _scroll_container.set_horizontal_scroll_mode(ScrollContainer.SCROLL_MODE_DISABLED)
+ _scroll_container.set_h_size_flags(Control.SIZE_EXPAND_FILL)
+ _scroll_container.set_v_size_flags(Control.SIZE_EXPAND_FILL)
self.add_child(_scroll_container)
-
- _group_states = {}
- _subgroup_states = {}
-
- _init_properties()
-
-## Override for add([method add_inspector_property]) custom [Inspector.InspectorProperty].
-func _init_properties() -> void:
- self.add_inspector_property(InspectorPropertyCheck.new())
- self.add_inspector_property(InspectorPropertySpin.new())
- self.add_inspector_property(InspectorPropertyLine.new())
- self.add_inspector_property(InspectorPropertyMultiline.new())
- self.add_inspector_property(InspectorPropertyVector2.new())
- self.add_inspector_property(InspectorPropertyVector3.new())
- self.add_inspector_property(InspectorPropertyColor.new())
- self.add_inspector_property(InspectorPropertyEnum.new())
- self.add_inspector_property(InspectorPropertyFlags.new())
- self.add_inspector_property(InspectorPropertyCategory.new())
- self.add_inspector_property(InspectorPropertyGroup.new())
- self.add_inspector_property(InspectorPropertySubGroup.new())
-
-## Add a custom [Inspector.InspectorProperty].
-func add_inspector_property(property: InspectorProperty) -> void:
- assert(is_instance_valid(property), "Invalid InspectorProperty.")
- if is_instance_valid(property):
- _properties.push_front(property)
+
+
+func _enter_tree() -> void:
+ _search.set_right_icon(get_theme_icon(&"search"))
## Set Inspector readonly.
func set_readonly(value: bool) -> void:
@@ -91,7 +89,7 @@ func is_readonly() -> bool:
## Set search line visible.
func set_search_enabled(value: bool) -> void:
_search_enabled = value
- _search.visible = value
+ _search.set_visible(value)
## Return [param true] if search line is enabled.
func is_search_enabled() -> bool:
@@ -101,17 +99,57 @@ func is_search_enabled() -> bool:
func set_object(object: Object) -> void:
if is_same(_object, object):
return
-
- if _object:
- _object.property_list_changed.disconnect(update_inspector)
+
+ if is_instance_valid(_object) and _object.property_list_changed.is_connected(_update_property_list):
+ _object.property_list_changed.disconnect(_update_property_list)
+
+ if is_instance_valid(object) and not object.property_list_changed.is_connected(_update_property_list):
+ var error: Error = object.property_list_changed.connect(_update_property_list)
+ assert(error == OK, error_string(error))
+
_object = object
_group_states = {}
_subgroup_states = {}
- _object.property_list_changed.connect(update_inspector)
+
object_changed.emit(object)
+ _update_property_list()
+## Set category handling enabled.
+func set_category_enabled(enabled: bool) -> void:
+ if _category_enadled == enabled:
+ return
+
+ _category_enadled = enabled
update_inspector()
+## Returns [param true] if category handling is enabled.
+func is_category_enabled() -> bool:
+ return _category_enadled
+
+## Set group handling enabled.
+func set_group_enabled(enabled: bool) -> void:
+ if _group_enabled == enabled:
+ return
+
+ _group_enabled = enabled
+ update_inspector()
+
+## Returns [param true] if group handling is enabled.
+func is_group_enabled() -> bool:
+ return _group_enabled
+
+## Set sub-group handling enabled.
+func set_subgroup_enabled(enabled: bool) -> void:
+ if _subgroup_enabled == enabled:
+ return
+
+ _subgroup_enabled = enabled
+ update_inspector()
+
+## Returns [param true] if sub-group handling is enabled.
+func is_subgroup_enabled() -> bool:
+ return _subgroup_enabled
+
## Return edited object.
func get_object() -> Object:
return _object
@@ -123,530 +161,160 @@ func clear() -> void:
## Return [param true] if property is valid.
## Override for custom available properties.
func is_valid_property(property: Dictionary) -> bool:
- if (property["usage"] == PROPERTY_USAGE_CATEGORY or \
- property["usage"] == PROPERTY_USAGE_GROUP or \
- property["usage"] == PROPERTY_USAGE_SUBGROUP):
- return true
- if property["hint"] == PROPERTY_HINT_ENUM:
+ if property["usage"] == PROPERTY_USAGE_CATEGORY:
+ return is_category_enabled()
+
+ elif property["usage"] == PROPERTY_USAGE_GROUP:
+ return is_group_enabled()
+
+ elif property["usage"] == PROPERTY_USAGE_SUBGROUP:
+ return is_subgroup_enabled()
+
+ elif property["hint"] == PROPERTY_HINT_ENUM:
return property["usage"] == PROPERTY_USAGE_SCRIPT_VARIABLE + PROPERTY_USAGE_DEFAULT + PROPERTY_USAGE_CLASS_IS_ENUM
return property["usage"] == PROPERTY_USAGE_SCRIPT_VARIABLE + PROPERTY_USAGE_DEFAULT
## Return [Control] for property.
func create_property_control(object: Object, property: Dictionary) -> Control:
- for p in _properties:
- if p.can_handle(object, property, is_readonly()):
- return p.create_control(object, property, is_readonly())
-
- return null
+ return InspectorProperty.create_property(object, property, not is_readonly())
## Update Inspector properties.
-func update_inspector(filter: String = _search.text) -> void:
+func update_inspector() -> void:
if is_instance_valid(_container):
- ## Backup all group and subgroup states
- for child in _container.get_children():
- if child.is_in_group("inspector_group"):
- var child_name = (child.text as String).strip_edges().right(-2)
- _group_states[child_name] = child.button_pressed
- elif child.is_in_group("inspector_subgroup"):
- var child_name = (child.text as String).strip_edges().right(-2)
- _subgroup_states[child_name] = child.button_pressed
_container.queue_free()
- _search.editable = is_instance_valid(_object)
- if not _search.editable:
+ _search.set_editable(is_instance_valid(_object))
+ if not _search.is_editable():
return
_container = VBoxContainer.new()
- _container.size_flags_horizontal = SIZE_EXPAND_FILL
- _container.size_flags_vertical = SIZE_EXPAND_FILL
-
- # Do not start populating properties until we find the first script property.
- var _start_populate = false
- for property in _object.get_property_list():
- if _start_populate:
- if filter.is_subsequence_ofn(property["name"]) and is_valid_property(property):
- var property_control = create_property_control(_object, property)
- if is_instance_valid(property_control):
- _container.add_child(property_control)
- elif property["name"].ends_with(".gd"):
- _start_populate = true
-
- # Collapse all groups and subgroups.
- for child in _container.get_children():
- if child.is_in_group("inspector_group"):
- var child_name = (child.text as String).strip_edges().right(-2)
- if _group_states.has(child_name):
- child.emit_signal("toggled", _group_states[child_name])
- child.button_pressed = _group_states[child_name]
- else:
- child.emit_signal("toggled", false)
- elif child.is_in_group("inspector_subgroup"):
- var child_name = (child.text as String).strip_edges().right(-2)
- if _subgroup_states.has(child_name):
- child.emit_signal("toggled", _subgroup_states[child_name])
- child.button_pressed = _subgroup_states[child_name]
- else:
- child.emit_signal("toggled", false)
-
+ _container.set_name("Container")
+ _container.set_h_size_flags(Control.SIZE_EXPAND_FILL)
+ _container.set_v_size_flags(Control.SIZE_EXPAND_FILL)
_scroll_container.add_child(_container)
-## Base InspectorProperty class.
-##
-## For inherited classes, override [method can_handle] and [method create_control] methods.
-class InspectorProperty extends RefCounted:
- ## Return [param true] if [InspectorProperty] can handle the object and property.
- func can_handle(object: Object, property: Dictionary, readonly: bool) -> bool:
- return false
-
- ## Return [param true] if [InspectorProperty] is editable.
- ## By default [param true] if [param NOT] [method Inspector.is_readonly].
- func is_editable(object: Object, property: Dictionary, readonly: bool) -> bool:
- return not readonly
-
- ## Factory method. Should be overridden.
- ## Return [Control] for edit property value.
- func create_control(object: Object, property: Dictionary, readonly: bool) -> Control:
- return null
-
- ## Return [BoxContainer] with [Label] and custom [Control] as children.
- func create_combo_container(name: StringName, control: Control, vertical: bool = false) -> BoxContainer:
- assert(is_instance_valid(control), "Invalid Control.")
+ var parent: Control = _container
+ var category: Control = null
+ var group: Control = null
+ var subgroup: Control = null
+
+ for property: Dictionary in _valid_properties:
+ var control: Control = create_property_control(_object, property)
if not is_instance_valid(control):
- return null
-
- var container := BoxContainer.new()
- container.vertical = vertical
-
- var label = Label.new()
- label.text = tr(name).capitalize()
- label.tooltip_text = label.text
- label.mouse_filter = Control.MOUSE_FILTER_STOP
- label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
- label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- label.size_flags_vertical = Control.SIZE_EXPAND_FILL
- container.add_child(label)
-
- control.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- control.size_flags_vertical = Control.SIZE_EXPAND_FILL
- container.add_child(control)
-
- return container
-
-## Handle [bool] property.
-class InspectorPropertyCheck extends InspectorProperty:
- func can_handle(object: Object, property: Dictionary, readonly: bool) -> bool:
- return property["type"] == TYPE_BOOL
-
- func create_control(object: Object, property: Dictionary, readonly: bool) -> Control:
- var property_name := StringName(property["name"])
-
- var check := CheckBox.new()
- check.button_pressed = object.get(property_name)
- check.text = tr("On")
- check.tooltip_text = str(check.button_pressed)
- check.disabled = not is_editable(object, property, readonly)
- check.size_flags_horizontal = Control.SIZE_EXPAND_FILL
-
- check.toggled.connect(func(value: bool) -> void:
- object.set(property_name, value)
- check.button_pressed = object.get(property_name)
- check.tooltip_text = str(check.button_pressed)
- )
-
- return create_combo_container(property_name, check)
-
-## Handle [int] or [float] property.
-class InspectorPropertySpin extends InspectorProperty:
- func can_handle(object: Object, property: Dictionary, readonly: bool) -> bool:
- return property["type"] == TYPE_INT or property["type"] == TYPE_FLOAT
-
- func create_control(object: Object, property: Dictionary, readonly: bool) -> Control:
- var property_name := StringName(property["name"])
-
- var spin := SpinBox.new()
- spin.min_value = FLOAT_MIN
- spin.max_value = FLOAT_MAX
- spin.step = 1.0 if property["type"] == TYPE_INT else 0.001
- spin.rounded = property["type"] == TYPE_INT
- spin.value = object.get(property_name)
- spin.editable = is_editable(object, property, readonly)
- spin.tooltip_text = str(spin.value)
- spin.size_flags_horizontal = Control.SIZE_EXPAND_FILL
-
- spin.value_changed.connect(func(value: float) -> void:
- object.set(property_name, value)
- spin.set_value_no_signal(object.get(property_name))
- spin.set_tooltip_text(str(spin.value))
- )
-
- var split : PackedStringArray = String(property["hint_string"]).split(',', false)
- if split.size() >= 2:
- spin.min_value = split[0].to_float()
- spin.max_value = split[1].to_float()
-
- if split.size() >= 3:
- spin.step = split[2].to_float()
-
- return create_combo_container(property_name, spin)
-
-## Handle [String] or [StringName] property.
-class InspectorPropertyLine extends InspectorProperty:
- func can_handle(object: Object, property: Dictionary, readonly: bool) -> bool:
- return property["type"] == TYPE_STRING or property["type"] == TYPE_STRING_NAME
-
- func create_control(object: Object, property: Dictionary, readonly: bool) -> Control:
- var property_name := StringName(property["name"])
-
- var line := LineEdit.new()
- line.text = object.get(property_name)
- line.tooltip_text = line.text
- line.editable = is_editable(object, property, readonly)
- line.size_flags_horizontal = Control.SIZE_EXPAND_FILL
-
- line.text_changed.connect(func(value: String) -> void:
- object.set(property_name, value)
-
- var caret := line.caret_column
- line.text = object.get(property_name)
- line.tooltip_text = line.text
- line.caret_column = caret
- )
-
- return create_combo_container(property_name, line)
-
-## Handle [String] or [StringName] property with [param @export_multiline] annotation.
-class InspectorPropertyMultiline extends InspectorProperty:
- func can_handle(object: Object, property: Dictionary, readonly: bool) -> bool:
- return property["hint"] == PROPERTY_HINT_MULTILINE_TEXT and (property["type"] == TYPE_STRING or property["type"] == TYPE_STRING_NAME)
-
- func create_control(object: Object, property: Dictionary, readonly: bool) -> Control:
- var property_name := StringName(property["name"])
- var hbox := HBoxContainer.new()
-
- var text_edit := TextEdit.new()
- text_edit.text = object.get(property_name)
- text_edit.tooltip_text = text_edit.text
- text_edit.editable = is_editable(object, property, readonly)
- text_edit.wrap_mode = TextEdit.LINE_WRAPPING_BOUNDARY
- text_edit.custom_minimum_size = Vector2(24.0, 96.0)
- text_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- hbox.add_child(text_edit)
-
- var maximize := Button.new()
- maximize.icon = load("addons/object-inspector/icons/maximize.svg")
- maximize.disabled = not text_edit.editable
- maximize.size_flags_vertical = Control.SIZE_EXPAND_FILL
- hbox.add_child(maximize)
-
- var window := AcceptDialog.new()
- window.title = "Text edit"
- window.min_size = Vector2(640, 480)
- hbox.add_child(window)
-
- var window_edit := TextEdit.new()
- window_edit.text = text_edit.text
- window_edit.tooltip_text = window_edit.text
- window.add_child(window_edit)
- # TextEdit don't emit changed text.
- var callable = func(edit: TextEdit) -> void:
- object.set(property_name, edit.text)
-
- var column := text_edit.get_caret_column()
- var line := text_edit.get_caret_line()
-
- text_edit.text = object.get(property_name)
- text_edit.tooltip_text = text_edit.text
- text_edit.set_caret_column(column)
- text_edit.set_caret_line(line)
-
- column = window_edit.get_caret_column()
- line = window_edit.get_caret_line()
-
- window_edit.text = text_edit.text
- window_edit.tooltip_text = window_edit.text
- window_edit.set_caret_column(column)
- window_edit.set_caret_line(line)
-
- maximize.pressed.connect(window.popup_centered)
- text_edit.text_changed.connect(callable.bind(text_edit))
- window.confirmed.connect(callable.bind(window_edit))
-
- return create_combo_container(property_name, hbox, true)
-
-## Handle [Vector2] or [Vector2i] property.
-class InspectorPropertyVector2 extends InspectorProperty:
- func can_handle(object: Object, property: Dictionary, readonly: bool) -> bool:
- return property["type"] == TYPE_VECTOR2 or property["type"] == TYPE_VECTOR2I
-
- func create_control(object: Object, property: Dictionary, readonly: bool) -> Control:
- var property_name := StringName(property["name"])
- var value : Vector2 = object.get(property_name)
-
- var vbox = VBoxContainer.new()
- vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
-
- var x_spin := SpinBox.new()
- x_spin.editable = is_editable(object, property, readonly)
- x_spin.prefix = "x"
- x_spin.min_value = FLOAT_MIN
- x_spin.max_value = FLOAT_MAX
- x_spin.step = 1.0 if property["type"] == TYPE_VECTOR2I else 0.001
- x_spin.value = value.x
- x_spin.tooltip_text = str(value.x)
- x_spin.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- vbox.add_child(x_spin)
-
- var y_spin := x_spin.duplicate() as SpinBox
- y_spin.prefix = "y"
- y_spin.value = value.y
- y_spin.tooltip_text = str(value.y)
- vbox.add_child(y_spin)
-
- var callable = func(_value) -> void:
- object.set(property_name, Vector2(x_spin.value, y_spin.value))
- value = object.get(property_name)
-
- x_spin.set_value_no_signal(value.x)
- x_spin.set_tooltip_text(str(value.x))
-
- y_spin.set_value_no_signal(value.y)
- y_spin.set_tooltip_text(str(value.y))
-
- x_spin.value_changed.connect(callable)
- y_spin.value_changed.connect(callable)
-
- return create_combo_container(property_name, vbox)
-
-## Handle [Vector3] or [Vector3i] property.
-class InspectorPropertyVector3 extends InspectorProperty:
- func can_handle(object: Object, property: Dictionary, readonly: bool) -> bool:
- return property["type"] == TYPE_VECTOR3 or property["type"] == TYPE_VECTOR3I
-
- func create_control(object: Object, property: Dictionary, readonly: bool) -> Control:
- var property_name := StringName(property["name"])
- var value : Vector3 = object.get(property_name)
-
- var hbox := HBoxContainer.new()
-
- var x_spin := SpinBox.new()
- x_spin.editable = is_editable(object, property, readonly)
- x_spin.prefix = "x"
- x_spin.min_value = FLOAT_MIN
- x_spin.max_value = FLOAT_MAX
- x_spin.step = 1.0 if property["type"] == TYPE_VECTOR3I else 0.001
- x_spin.value = value.x
- x_spin.tooltip_text = str(x_spin.value)
- x_spin.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- hbox.add_child(x_spin)
-
- var y_spin := x_spin.duplicate() as SpinBox
- y_spin.prefix = "y"
- y_spin.value = value.y
- y_spin.tooltip_text = str(y_spin.value)
- hbox.add_child(y_spin)
-
- var z_spin := x_spin.duplicate() as SpinBox
- z_spin.prefix = "z"
- z_spin.value = value.z
- z_spin.tooltip_text = str(z_spin.value)
- hbox.add_child(z_spin)
-
- var callable = func(_value) -> void:
- object.set(property_name, Vector3(x_spin.value, y_spin.value, z_spin.value))
- value = object.get(property_name)
-
- x_spin.set_value_no_signal(value.x)
- x_spin.set_tooltip_text(str(value.x))
-
- y_spin.set_value_no_signal(value.y)
- y_spin.set_tooltip_text(str(value.y))
-
- z_spin.set_value_no_signal(value.z)
- z_spin.set_tooltip_text(str(value.z))
-
- x_spin.value_changed.connect(callable)
- y_spin.value_changed.connect(callable)
- z_spin.value_changed.connect(callable)
-
- return create_combo_container(property_name, hbox, true)
-
-## Handle [Color] property.
-class InspectorPropertyColor extends InspectorProperty:
- func can_handle(object: Object, property: Dictionary, readonly: bool) -> bool:
- return property["type"] == TYPE_COLOR
-
- func create_control(object: Object, property: Dictionary, readonly: bool) -> Control:
- var property_name := StringName(property["name"])
-
- var picker := ColorPickerButton.new()
- picker.color = object.get(property_name)
- picker.tooltip_text = str(picker.color)
- picker.disabled = not is_editable(object, property, readonly)
- picker.edit_alpha = not property["hint"] == PROPERTY_HINT_COLOR_NO_ALPHA
- picker.size_flags_horizontal = Control.SIZE_EXPAND_FILL
-
- picker.color_changed.connect(func(value: Color) -> void:
- object.set(property_name, value)
- picker.color = object.get(property_name)
- picker.tooltip_text = str(picker.color)
- )
-
- return create_combo_container(property_name, picker)
-
-## Handle [param enum] property.
-class InspectorPropertyEnum extends InspectorProperty:
- func can_handle(object: Object, property: Dictionary, readonly: bool) -> bool:
- return property["hint"] == PROPERTY_HINT_ENUM and property["type"] == TYPE_INT
-
- func create_control(object: Object, property: Dictionary, readonly: bool) -> Control:
- var option_button := OptionButton.new()
- option_button.clip_text = true
- option_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- option_button.disabled = not is_editable(object, property, readonly)
-
- var popup : PopupMenu = option_button.get_popup()
- var hint_split: PackedStringArray = String(property["hint_string"]).split(",", false)
-
- for i: int in hint_split.size():
- var split := hint_split[i].split(":", false)
-
- # If key-value pair.
- if split.size() > 1 and split[1].is_valid_int():
- popup.add_item(split[0], split[1].to_int())
+ continue
+
+ # TODO: Do something. I really don't like all the code below...
+ if property["usage"] == PROPERTY_USAGE_SUBGROUP:
+ if is_instance_valid(group):
+ group.find_child("PropertyContainer", true, false).add_child(control)
+ elif is_instance_valid(category):
+ category.find_child("PropertyContainer", true, false).add_child(control)
else:
- popup.add_item(split[0], i)
-
- var property_name := StringName(property["name"])
- option_button.selected = popup.get_item_index(object.get(property_name))
-
- popup.id_pressed.connect(func(value: int) -> void:
- object.set(property_name, value)
- option_button.selected = popup.get_item_index(object.get(property_name))
- )
-
- return create_combo_container(property_name, option_button)
-
-## Handle [int] property with [param @export_flags] annotation.
-class InspectorPropertyFlags extends InspectorProperty:
- func can_handle(object: Object, property: Dictionary, readonly: bool) -> bool:
- return property["hint"] == PROPERTY_HINT_FLAGS and property["type"] == TYPE_INT
-
- func create_control(object: Object, property: Dictionary, readonly: bool) -> Control:
- var property_name := StringName(property["name"])
- var value : int = object.get(property_name)
-
- var vbox = VBoxContainer.new()
- vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
-
- var split : PackedStringArray = String(property["hint_string"]).split(",", false)
- for i in split.size():
- var check := CheckBox.new()
- check.text = split[i]
- check.button_pressed = value & (1 << i)
- check.disabled = not is_editable(object, property, readonly)
-
- check.toggled.connect(func(pressed: bool) -> void:
- if pressed:
- object.set(property_name, object.get(property_name) | (1 << i))
- else:
- object.set(property_name, object.get(property_name) & ~(1 << i))
-
- check.button_pressed = object.get(property_name) & 1 << i
- )
-
- vbox.add_child(check)
-
- return create_combo_container(property_name, vbox)
-
-## Handle [param @export_category] property.
-class InspectorPropertyCategory extends InspectorProperty:
- func can_handle(object: Object, property: Dictionary, readonly: bool) -> bool:
- return property["usage"] == PROPERTY_USAGE_CATEGORY
-
- func create_control(object: Object, property: Dictionary, readonly: bool) -> Control:
- var button := Button.new()
- button.text = tr(property["name"]).capitalize()
- button.disabled = true
- button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- button.alignment = HORIZONTAL_ALIGNMENT_CENTER
- button.add_theme_color_override("font_disabled_color", Color(1.0, 1.0, 1.0, 1.0))
- var button_settings := StyleBoxFlat.new()
- button_settings.bg_color = Color(0.2, 0.2, 0.2, 0.5)
- button_settings.corner_detail = 5
- button_settings.set_corner_radius_all(10)
- button.add_theme_stylebox_override("disabled", button_settings)
-
- button.add_to_group("inspector_category")
-
- return button
-
-## Handle [param @export_group] property.
-class InspectorPropertyGroup extends InspectorProperty:
- func can_handle(object: Object, property: Dictionary, readonly: bool) -> bool:
- return property["usage"] == PROPERTY_USAGE_GROUP
-
- func create_control(object: Object, property: Dictionary, readonly: bool) -> Control:
- var button := Button.new()
- button.text = " + " + tr(property["name"]).capitalize()
- button.toggle_mode = true
- button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- button.alignment = HORIZONTAL_ALIGNMENT_LEFT
-
- button.add_to_group("inspector_group")
-
- ## Search all children of the parent and show/hide them until next category or group
- var callable = func(toggled: bool, button: Button) -> void:
- button.text = (" - " if toggled else " + ") + tr(property["name"]).capitalize()
- var objects = button.get_parent().get_children()
- var self_index = objects.find(button)
- var subgroup_found = false
- var subgroup_state = false
- for i in range(self_index + 1, objects.size()):
- if objects[i].is_in_group("inspector_group") or objects[i].is_in_group("inspector_category"):
- break
- elif objects[i].is_in_group("inspector_subgroup"):
- subgroup_state = objects[i].button_pressed
- subgroup_found = true
- objects[i].set_visible(toggled)
- else:
- if toggled:
- if subgroup_found:
- objects[i].set_visible(subgroup_state)
- else:
- objects[i].set_visible(toggled)
- else:
- objects[i].set_visible(false)
-
- button.toggled.connect(callable.bind(button))
-
- return button
-
-## Handle [param @export_subgroup] property.
-class InspectorPropertySubGroup extends InspectorProperty:
- func can_handle(object: Object, property: Dictionary, readonly: bool) -> bool:
- return property["usage"] == PROPERTY_USAGE_SUBGROUP
-
- func create_control(object: Object, property: Dictionary, readonly: bool) -> Control:
- var button := Button.new()
- button.text = " + " + tr(property["name"]).capitalize()
- button.toggle_mode = true
- button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- button.alignment = HORIZONTAL_ALIGNMENT_LEFT
-
- button.add_to_group("inspector_subgroup")
-
- ## Search all children of the parent and show/hide them until next category, group or subgroup
- var callable = func(toggled: bool, button: Button) -> void:
- button.text = (" - " if toggled else " + ") + tr(property["name"]).capitalize()
- var objects = button.get_parent().get_children()
- var self_index = objects.find(button)
- for i in range(self_index + 1, objects.size()):
- if objects[i].is_in_group("inspector_subgroup") or objects[i].is_in_group("inspector_group") or objects[i].is_in_group("inspector_category"):
- break
- else:
- objects[i].set_visible(toggled)
-
- button.toggled.connect(callable.bind(button))
-
- return button
+ _container.add_child(control)
+
+ parent = control.find_child("PropertyContainer", true, false)
+ assert(is_instance_valid(parent), "Subgroup property does not have a `PropertyContainer` node!")
+
+ subgroup = control
+ subgroup.call(&"set_toggled", _subgroup_states.get(property["name"], false))
+ subgroup.set_meta(&"group", group)
+
+ var error: Error = subgroup.connect(&"toggled", _on_subgroup_toggled.bind(property["name"]))
+ assert(error == OK, error_string(error))
+
+ elif property["usage"] == PROPERTY_USAGE_GROUP:
+ if is_instance_valid(category):
+ category.find_child("PropertyContainer", true, false).add_child(control)
+ else:
+ _container.add_child(control)
+
+ parent = control.find_child("PropertyContainer", true, false)
+ assert(is_instance_valid(parent), "Group property does not have a `PropertyContainer` node!")
+
+ group = control
+ group.call(&"set_toggled", _group_states.get(property["name"], false))
+ group.set_meta(&"category", category)
+
+ var error: Error = group.connect(&"toggled", _on_group_toggled.bind(property["name"]))
+ assert(error == OK, error_string(error))
+
+ elif property["usage"] == PROPERTY_USAGE_CATEGORY:
+ _container.add_child(control)
+
+ parent = control.find_child("PropertyContainer", true, false)
+ assert(is_instance_valid(parent), "Category property does not have a `PropertyContainer` node!")
+
+ category = control
+
+ else:
+ control.set_meta(&"category", category)
+ control.set_meta(&"group", group)
+ control.set_meta(&"subgroup", subgroup)
+
+ parent.add_child(control)
+
+ property["control"] = control
+
+# Potentially should be replaced by on-the-fly computing...
+func _update_property_list() -> void:
+ if not is_instance_valid(_object):
+ return
+
+ _valid_properties = _object.get_property_list()
+
+ var counter: int = 0
+ # INFO: I know it's shitty code, but it works...
+ var i: int = _valid_properties.size() - 1
+ while i >= 0:
+ var property: Dictionary = _valid_properties[i]
+ if property["usage"] == PROPERTY_USAGE_SUBGROUP or property["usage"] == PROPERTY_USAGE_GROUP:
+ if counter < 1:
+ _valid_properties.remove_at(i)
+
+ counter -= 1
+ elif property["usage"] == PROPERTY_USAGE_CATEGORY:
+ if counter < 1:
+ _valid_properties.remove_at(i)
+
+ counter = 0
+ elif not is_valid_property(property):
+ _valid_properties.remove_at(i)
+ else:
+ counter += 1
+
+ i -= 1
+
+ update_inspector()
+
+func _on_filter_text_chnaged(filter: String) -> void:
+ for property: Dictionary in _valid_properties:
+ var control: Control = property["control"]
+
+ if filter.is_subsequence_ofn(property["name"]):
+ if control.has_meta(&"category"):
+ var category := control.get_meta(&"category") as Control
+ if is_instance_valid(category):
+ category.show()
+
+ if control.has_meta(&"group"):
+ var group := control.get_meta(&"group") as Control
+ if is_instance_valid(group):
+ group.show()
+
+ if control.has_meta(&"subgroup"):
+ var subgroup := control.get_meta(&"subgroup") as Control
+ if is_instance_valid(subgroup):
+ subgroup.show()
+
+ control.show()
+
+ else:
+ control.hide()
+
+
+func _on_group_toggled(expanded: bool, property: String) -> void:
+ _group_states[property] = expanded
+
+func _on_subgroup_toggled(expanded: bool, property: String) -> void:
+ _subgroup_states[property] = expanded
diff --git a/scripts/inspector_properties.gd b/scripts/inspector_properties.gd
new file mode 100644
index 0000000..10cb4b1
--- /dev/null
+++ b/scripts/inspector_properties.gd
@@ -0,0 +1,565 @@
+# Copyright (c) 2022-2024 Mansur Isaev and contributors - MIT License
+# See `LICENSE.md` included in the source distribution for details.
+
+# Magic numbers, but otherwise the SpinBox does not work correctly.
+const INT32_MIN = -2147483648
+const INT32_MAX = 2147483647
+
+## Handle [annotation @GDScript.@export_category] property.
+class InspectorPropertyCategory extends InspectorProperty:
+ var _container: VBoxContainer = null
+ var _title: Label = null
+
+ func _enter_tree() -> void:
+ self.set_theme_type_variation(&"InspectorPropertyCategory")
+
+ _container = VBoxContainer.new()
+ _container.set_name("PropertyContainer")
+
+ _title = Label.new()
+ _title.set_text_overrun_behavior(TextServer.OVERRUN_TRIM_ELLIPSIS)
+ _title.set_name("Title")
+ _title.set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER)
+ _title.set_text(get_property().capitalize())
+ _container.add_child(_title, false, Node.INTERNAL_MODE_FRONT)
+
+ self.add_child(_container)
+
+ static func can_handle(_object: Object, property: Dictionary, _editable: bool) -> bool:
+ return property["usage"] == PROPERTY_USAGE_CATEGORY
+
+## Handle [annotation @GDScript.@export_group] property.
+class InspectorPropertyGroup extends InspectorProperty:
+ signal toggled(expanded: bool)
+
+ var _container: VBoxContainer = null
+ var _button: Button = null
+
+ func _enter_tree() -> void:
+ self.set_theme_type_variation(&"InspectorPropertyGroup")
+
+ var vbox := VBoxContainer.new()
+
+ _container = VBoxContainer.new()
+ _container.set_name("PropertyContainer")
+ _container.hide() # By default group is collapsed.
+ vbox.add_child(_container)
+
+ _button = Button.new()
+ _button.set_text_overrun_behavior(TextServer.OVERRUN_TRIM_ELLIPSIS)
+ _button.set_name("Button")
+ _button.set_toggle_mode(true)
+ _button.set_flat(true)
+ _button.set_text_alignment(HORIZONTAL_ALIGNMENT_LEFT)
+ _button.set_text(get_property().capitalize())
+ _button.set_button_icon(get_theme_icon(&"collapsed"))
+ _button.toggled.connect(_on_button_toggled)
+ vbox.add_child(_button, false, Node.INTERNAL_MODE_FRONT)
+
+ self.add_child(vbox)
+
+ func _on_button_toggled(expanded: bool) -> void:
+ _button.set_button_icon(get_theme_icon(&"expanded") if expanded else get_theme_icon(&"collapsed"))
+ _container.set_visible(expanded)
+
+ toggled.emit(expanded)
+
+ func set_toggled(toggled: bool) -> void:
+ _button.set_pressed(toggled)
+
+ static func can_handle(_object: Object, property: Dictionary, _editable: bool) -> bool:
+ return property["usage"] == PROPERTY_USAGE_GROUP
+
+## Handle [annotation @GDScript.@export_subgroup] property.
+class InspectorPropertySubgroup extends InspectorPropertyGroup:
+ func _enter_tree() -> void:
+ super()
+ self.set_theme_type_variation(&"InspectorPropertySubGroup")
+
+ static func can_handle(_object: Object, property: Dictionary, _editable: bool) -> bool:
+ return property["usage"] == PROPERTY_USAGE_SUBGROUP
+
+## Handle [bool] property.
+class InspectorPropertyBool extends InspectorProperty:
+ var check_box: CheckBox = null
+
+ func _enter_tree() -> void:
+ check_box = create_bool_control(set_value, get_value, is_editable())
+ create_flow_container(get_property(), check_box)
+
+ static func _static_init() -> void:
+ InspectorPropertyType.register_type(TYPE_BOOL, "bool", create_bool_control)
+
+ static func create_bool_control(setter: Callable, getter: Callable, editable: bool) -> CheckBox:
+ var check_box := CheckBox.new()
+ check_box.set_disabled(not editable)
+ check_box.set_text("On")
+ check_box.set_pressed_no_signal(getter.call())
+
+ check_box.toggled.connect(func(value: bool) -> void:
+ setter.call(value)
+ check_box.set_pressed_no_signal(getter.call())
+ )
+
+ return check_box
+
+ static func can_handle(_object: Object, property: Dictionary, _editable: bool) -> bool:
+ return property["type"] == TYPE_BOOL
+
+## Handle [int] or [float] property.
+class InspectorPropertyNumber extends InspectorProperty:
+ var spin_box: SpinBox = null
+
+ func _enter_tree() -> void:
+ if get_type() == TYPE_INT:
+ spin_box = create_int_control(set_value, get_value, is_editable())
+ else:
+ spin_box = create_float_control(set_value, get_value, is_editable())
+
+ if get_hint() == PROPERTY_HINT_RANGE:
+ var split: PackedStringArray = get_hint_string().split(',', false)
+
+ spin_box.set_min(split[0].to_float() if split.size() >= 1 and split[0].is_valid_float() else INT32_MIN)
+ spin_box.set_max(split[1].to_float() if split.size() >= 2 and split[1].is_valid_float() else INT32_MAX)
+ spin_box.set_step(split[2].to_float() if split.size() >= 3 and split[2].is_valid_float() else 1.0 if get_type() == TYPE_INT else 0.001)
+
+ create_flow_container(get_property(), spin_box)
+
+
+ static func _static_init() -> void:
+ InspectorPropertyType.register_type(TYPE_INT, "int", create_int_control)
+ InspectorPropertyType.register_type(TYPE_FLOAT, "float", create_float_control)
+
+ static func create_int_control(setter: Callable, getter: Callable, editable: bool) -> SpinBox:
+ var spin_box := SpinBox.new()
+ spin_box.set_editable(editable)
+ spin_box.set_min(INT32_MIN)
+ spin_box.set_max(INT32_MAX)
+ spin_box.set_step(1.0)
+ spin_box.set_use_rounded_values(true)
+ spin_box.set_value_no_signal(getter.call())
+
+ spin_box.value_changed.connect(func(value: int) -> void:
+ setter.call(value)
+ spin_box.set_value_no_signal(getter.call())
+ )
+
+ return spin_box
+
+ static func create_float_control(setter: Callable, getter: Callable, editable: bool) -> SpinBox:
+ var spin_box := SpinBox.new()
+ spin_box.set_editable(editable)
+ spin_box.set_min(INT32_MIN)
+ spin_box.set_max(INT32_MAX)
+ spin_box.set_step(0.001)
+ spin_box.set_value_no_signal(getter.call())
+
+ spin_box.value_changed.connect(func(value: float) -> void:
+ setter.call(value)
+ spin_box.set_value_no_signal(getter.call())
+ )
+
+ return spin_box
+
+ static func can_handle(_object: Object, property: Dictionary, _editable: bool) -> bool:
+ return property["type"] == TYPE_INT or property["type"] == TYPE_FLOAT
+
+## Handle [String] or [StringName] property.
+class InspectorPropertyString extends InspectorProperty:
+ var line_edit: LineEdit = null
+
+ func _enter_tree() -> void:
+ if get_type() == TYPE_STRING:
+ line_edit = create_string_control(set_value, get_value, is_editable())
+ else:
+ line_edit = create_string_name_control(set_value, get_value, is_editable())
+
+ create_flow_container(get_property(), line_edit)
+
+ static func _static_init() -> void:
+ InspectorPropertyType.register_type(TYPE_STRING, "String", create_string_control)
+ InspectorPropertyType.register_type(TYPE_STRING_NAME, "StringName", create_string_name_control)
+
+ static func _create_line_edit(setter: Callable, getter: Callable, editable: bool, string_name: bool) -> LineEdit:
+ var line_edit := LineEdit.new()
+ line_edit.set_editable(editable)
+ line_edit.set_text(getter.call())
+
+ if string_name:
+ line_edit.set_placeholder("StringName")
+ line_edit.text_changed.connect(func(value: StringName) -> void:
+ var caret: int = line_edit.get_caret_column()
+
+ setter.call(value)
+ line_edit.set_text(getter.call())
+ line_edit.set_caret_column(caret)
+ )
+ else:
+ line_edit.text_changed.connect(func(value: String) -> void:
+ var caret: int = line_edit.get_caret_column()
+
+ setter.call(value)
+ line_edit.set_text(getter.call())
+ line_edit.set_caret_column(caret)
+ )
+
+ return line_edit
+
+ static func create_string_control(setter: Callable, getter: Callable, editable: bool) -> LineEdit:
+ return _create_line_edit(setter, getter, editable, false)
+
+ static func create_string_name_control(setter: Callable, getter: Callable, editable: bool) -> LineEdit:
+ return _create_line_edit(setter, getter, editable, true)
+
+ static func can_handle(_object: Object, property: Dictionary, _editable: bool) -> bool:
+ return property["type"] == TYPE_STRING or property["type"] == TYPE_STRING_NAME
+
+## Handle [String] or [StringName] property with [param @export_multiline] annotation.
+class InspectorPropertyMultiline extends InspectorProperty:
+ var text_edit: TextEdit = null
+ var maximize: Button = null
+
+ var window: AcceptDialog = null
+ var window_text_edit: TextEdit = null
+
+ func _enter_tree() -> void:
+ var container := VBoxContainer.new()
+ container.set_name("Container")
+
+ var label := Label.new()
+ label.set_name("Label")
+ label.set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER)
+ label.set_text(get_property().capitalize())
+ label.set_mouse_filter(Control.MOUSE_FILTER_IGNORE)
+ label.set_text_overrun_behavior(TextServer.OVERRUN_TRIM_ELLIPSIS)
+ label.set_h_size_flags(Control.SIZE_EXPAND_FILL)
+ label.set_v_size_flags(Control.SIZE_EXPAND_FILL)
+ label.set_stretch_ratio(0.75)
+ container.add_child(label)
+
+ var hbox := HBoxContainer.new()
+ hbox.set_name("Property")
+ hbox.set_h_size_flags(Control.SIZE_EXPAND_FILL)
+ hbox.set_h_size_flags(Control.SIZE_EXPAND_FILL)
+ hbox.set_v_size_flags(Control.SIZE_EXPAND_FILL)
+ container.add_child(hbox)
+
+ text_edit = TextEdit.new()
+ text_edit.set_editable(is_editable())
+ text_edit.set_name("TextEdit")
+ text_edit.set_text(get_value())
+ text_edit.set_tooltip_text(text_edit.get_text())
+ text_edit.set_line_wrapping_mode(TextEdit.LINE_WRAPPING_BOUNDARY)
+ text_edit.set_custom_minimum_size(Vector2(0.0, 96.0))
+ text_edit.set_h_size_flags(Control.SIZE_EXPAND_FILL)
+ text_edit.set_v_size_flags(Control.SIZE_EXPAND_FILL)
+ text_edit.text_changed.connect(_on_text_edit_text_changed)
+ hbox.add_child(text_edit)
+
+ maximize = Button.new()
+ maximize.set_name("Maximize")
+ maximize.set_flat(true)
+ maximize.set_v_size_flags(Control.SIZE_SHRINK_CENTER)
+ maximize.set_button_icon(get_theme_icon(&"maximize", &"Inspector"))
+ maximize.pressed.connect(_on_maximize_pressed)
+ hbox.add_child(maximize)
+
+ self.add_child(container)
+
+ func _on_text_edit_text_changed() -> void:
+ var column: int = text_edit.get_caret_column()
+ var line: int = text_edit.get_caret_line()
+
+ text_edit.set_text(set_and_return_value(text_edit.get_text()))
+ text_edit.set_caret_column(column)
+ text_edit.set_caret_line(line)
+
+ func _on_window_confirmed() -> void:
+ var column: int = window_text_edit.get_caret_column()
+ var line: int = window_text_edit.get_caret_line()
+
+ window_text_edit.set_text(set_and_return_value(window_text_edit.get_text()))
+ window_text_edit.set_caret_column(column)
+ window_text_edit.set_caret_line(line)
+ text_edit.set_text(window_text_edit.get_text())
+
+ func _on_maximize_pressed() -> void:
+ if not is_instance_valid(window):
+ window = AcceptDialog.new()
+ window.set_name("EditTextDialog")
+ window.set_title("Text edit")
+ window.set_min_size(Vector2(640, 480))
+ window.add_cancel_button("Cancel")
+ window.set_ok_button_text("Save")
+ window.confirmed.connect(_on_window_confirmed)
+
+ window_text_edit = TextEdit.new()
+ window_text_edit.set_editable(is_editable())
+ window_text_edit.set_name("TextEdit")
+ window_text_edit.set_text(get_value())
+ window.add_child(window_text_edit)
+
+ self.add_child(window)
+
+ window_text_edit.set_text(get_value())
+ window.popup_centered_clamped(Vector2(640, 480))
+
+ static func can_handle(_object: Object, property: Dictionary, _editable: bool) -> bool:
+ return property["hint"] == PROPERTY_HINT_MULTILINE_TEXT and (property["type"] == TYPE_STRING or property["type"] == TYPE_STRING_NAME)
+
+## Handle [Vector2] or [Vector2i] property.
+class InspectorPropertyVector2 extends InspectorProperty:
+ func _enter_tree() -> void:
+ var box: BoxContainer = null
+ if get_type() == TYPE_VECTOR2:
+ box = create_vector2_control(set_value, get_value, is_editable())
+ else:
+ box = create_vector2i_control(set_value, get_value, is_editable())
+
+ box.set_h_size_flags(Control.SIZE_EXPAND_FILL)
+
+ var label: Label = create_flow_container(get_property(), box).get_node(^"Label")
+ label.set_v_size_flags(Control.SIZE_SHRINK_BEGIN)
+
+ static func _static_init() -> void:
+ InspectorPropertyType.register_type(TYPE_VECTOR2, "Vector2", create_vector2_control)
+ InspectorPropertyType.register_type(TYPE_VECTOR2I, "Vector2i", create_vector2i_control)
+
+ static func _create_vector2_control(setter: Callable, getter: Callable, editable: bool, is_vector2i: bool) -> BoxContainer:
+ var box := BoxContainer.new()
+ box.set_h_size_flags(Control.SIZE_EXPAND_FILL)
+
+ var value: Vector2 = getter.call()
+
+ var x_spin := SpinBox.new()
+ x_spin.set_editable(editable)
+ x_spin.set_name("X")
+ x_spin.set_prefix("x")
+ x_spin.set_min(INT32_MIN)
+ x_spin.set_max(INT32_MAX)
+ x_spin.set_step(1.0 if is_vector2i else 0.001)
+ x_spin.set_use_rounded_values(is_vector2i)
+ x_spin.set_value_no_signal(value.x)
+ x_spin.set_h_size_flags(Control.SIZE_EXPAND_FILL)
+ box.add_child(x_spin)
+
+ var y_spin: SpinBox = x_spin.duplicate()
+ y_spin.set_name("Y")
+ y_spin.set_prefix("y")
+ y_spin.set_value_no_signal(value.y)
+ box.add_child(y_spin)
+
+ var value_changed: Callable
+ if is_vector2i:
+ value_changed = func(_value) -> void:
+ setter.call(Vector2i(x_spin.get_value(), y_spin.get_value()))
+ var vector2i: Vector2i = getter.call()
+
+ x_spin.set_value_no_signal(vector2i.x)
+ y_spin.set_value_no_signal(vector2i.y)
+ else:
+ value_changed = func(_value) -> void:
+ setter.call(Vector2(x_spin.get_value(), y_spin.get_value()))
+ value = getter.call()
+
+ x_spin.set_value_no_signal(value.x)
+ y_spin.set_value_no_signal(value.y)
+
+ x_spin.value_changed.connect(value_changed)
+ y_spin.value_changed.connect(value_changed)
+
+ return box
+
+ static func create_vector2_control(setter: Callable, getter: Callable, editable: bool) -> BoxContainer:
+ return _create_vector2_control(setter, getter, editable, false)
+
+ static func create_vector2i_control(setter: Callable, getter: Callable, editable: bool) -> BoxContainer:
+ return _create_vector2_control(setter, getter, editable, true)
+
+ static func can_handle(_object: Object, property: Dictionary, _editable: bool) -> bool:
+ return property["type"] == TYPE_VECTOR2 or property["type"] == TYPE_VECTOR2I
+
+## Handle [Vector3] or [Vector3i] property.
+class InspectorPropertyVector3 extends InspectorProperty:
+ func _enter_tree() -> void:
+ var box: BoxContainer = null
+ if get_type() == TYPE_VECTOR3I:
+ box = create_vector3i_control(set_value, get_value, is_editable())
+ else:
+ box = create_vector3_control(set_value, get_value, is_editable())
+
+ create_flow_container(get_property(), box).add_to_group(&"vertical")
+
+ static func _static_init() -> void:
+ InspectorPropertyType.register_type(TYPE_VECTOR3, "Vector3", create_vector3_control)
+ InspectorPropertyType.register_type(TYPE_VECTOR3I, "Vector3i", create_vector3i_control)
+
+ static func _create_vector3_control(setter: Callable, getter: Callable, editable: bool, is_vector3i: bool) -> BoxContainer:
+ var box := BoxContainer.new()
+ box.add_to_group(&"vertical")
+
+ var value: Vector3 = getter.call()
+
+ var x_spin := SpinBox.new()
+ x_spin.set_editable(editable)
+ x_spin.set_name("X")
+ x_spin.set_prefix("x")
+ x_spin.set_min(INT32_MIN)
+ x_spin.set_max(INT32_MAX)
+ x_spin.set_step(1.0 if is_vector3i else 0.001)
+ x_spin.set_use_rounded_values(is_vector3i)
+ x_spin.set_value_no_signal(value.x)
+ x_spin.set_h_size_flags(Control.SIZE_EXPAND_FILL)
+ box.add_child(x_spin)
+
+ var y_spin: SpinBox = x_spin.duplicate()
+ y_spin.set_name("Y")
+ y_spin.set_prefix("y")
+ y_spin.set_value_no_signal(value.y)
+ box.add_child(y_spin)
+
+ var z_spin: SpinBox = x_spin.duplicate()
+ z_spin.set_name("Z")
+ z_spin.set_prefix("z")
+ z_spin.set_value_no_signal(value.z)
+ box.add_child(z_spin)
+
+ var on_value_changed: Callable
+ if is_vector3i:
+ on_value_changed = func(_value) -> void:
+ setter.call(Vector3i(x_spin.get_value(), y_spin.get_value(), z_spin.get_value()))
+ var vector3i: Vector3i = getter.call()
+
+ x_spin.set_value_no_signal(vector3i.x)
+ y_spin.set_value_no_signal(vector3i.y)
+ z_spin.set_value_no_signal(vector3i.z)
+ else:
+ on_value_changed = func(_value) -> void:
+ setter.call(Vector3(x_spin.get_value(), y_spin.get_value(), z_spin.get_value()))
+ value = getter.call()
+
+ x_spin.set_value_no_signal(value.x)
+ y_spin.set_value_no_signal(value.y)
+ z_spin.set_value_no_signal(value.z)
+
+ x_spin.value_changed.connect(on_value_changed)
+ y_spin.value_changed.connect(on_value_changed)
+ z_spin.value_changed.connect(on_value_changed)
+
+ return box
+
+ static func create_vector3_control(setter: Callable, getter: Callable, editable: bool) -> BoxContainer:
+ return _create_vector3_control(setter, getter, editable, false)
+
+ static func create_vector3i_control(setter: Callable, getter: Callable, editable: bool) -> BoxContainer:
+ return _create_vector3_control(setter, getter, editable, true)
+
+ static func can_handle(_object: Object, property: Dictionary, _editable: bool) -> bool:
+ return property["type"] == TYPE_VECTOR3 or property["type"] == TYPE_VECTOR3I
+
+## Handle [Color] property.
+class InspectorPropertyColor extends InspectorProperty:
+ var color_picker: ColorPickerButton = null
+
+ func _enter_tree() -> void:
+ color_picker = create_color_control(set_value, get_value, is_editable())
+ color_picker.set_edit_alpha(get_hint() == PROPERTY_HINT_COLOR_NO_ALPHA)
+
+ create_flow_container(get_property(), color_picker)
+
+ static func _static_init() -> void:
+ InspectorPropertyType.register_type(TYPE_COLOR, "Color", create_color_control)
+
+ static func create_color_control(setter: Callable, getter: Callable, editable: bool) -> ColorPickerButton:
+ var color_picker := ColorPickerButton.new()
+ color_picker.set_disabled(not editable)
+ color_picker.set_pick_color(getter.call())
+
+ color_picker.color_changed.connect(func(value: Color) -> void:
+ setter.call(value)
+ color_picker.set_pick_color(getter.call())
+ )
+
+ var picker: ColorPicker = color_picker.get_picker()
+ picker.set_presets_visible(false)
+
+ return color_picker
+
+ static func can_handle(_object: Object, property: Dictionary, _editable: bool) -> bool:
+ return property["type"] == TYPE_COLOR
+
+## Handle [param enum] property.
+class InspectorPropertyEnum extends InspectorProperty:
+ var option_button: OptionButton = null
+
+ func _enter_tree() -> void:
+ option_button = OptionButton.new()
+ option_button.set_disabled(not is_editable())
+ option_button.set_clip_text(true)
+
+ var hint_split: PackedStringArray = get_hint_string().split(",", false)
+
+ for i: int in hint_split.size():
+ var split := hint_split[i].split(":", false)
+
+ # If key-value pair.
+ if split.size() > 1 and split[1].is_valid_int():
+ option_button.add_item(split[0], split[1].to_int())
+ else:
+ option_button.add_item(split[0], i)
+
+ option_button.select(option_button.get_item_index(get_value()))
+ option_button.get_popup().id_pressed.connect(_on_id_pressed)
+
+ create_flow_container(get_property(), option_button)
+
+ func _on_id_pressed(id: int) -> void:
+ option_button.select(option_button.get_item_index(set_and_return_value(id)))
+
+ static func can_handle(_object: Object, property: Dictionary, _editable: bool) -> bool:
+ return property["hint"] == PROPERTY_HINT_ENUM and property["type"] == TYPE_INT
+
+## Handle [int] property with [param @export_flags] annotation.
+class InspectorPropertyFlags extends InspectorProperty:
+ func _enter_tree() -> void:
+ var vbox := VBoxContainer.new()
+ var value: int = get_value()
+
+ var split : PackedStringArray = get_hint_string().split(",", false)
+ for i in split.size():
+ var check_box := CheckBox.new()
+ check_box.set_disabled(not is_editable())
+ check_box.set_text(split[i])
+ check_box.set_pressed(value & (1 << i))
+
+ check_box.toggled.connect(func(pressed: bool) -> void:
+ if pressed:
+ set_value(get_value() | (1 << i))
+ else:
+ set_value(get_value() & ~(1 << i))
+
+ check_box.set_pressed(get_value() & 1 << i)
+ )
+
+ vbox.add_child(check_box)
+
+ var label: Label = create_flow_container(get_property(), vbox).get_node(^"Label")
+ label.set_v_size_flags(Control.SIZE_SHRINK_BEGIN)
+
+ static func can_handle(_object: Object, property: Dictionary, _editable: bool) -> bool:
+ return property["hint"] == PROPERTY_HINT_FLAGS and property["type"] == TYPE_INT
+
+
+static func _static_init() -> void:
+ InspectorProperty.declare_property(InspectorPropertyCategory.can_handle, InspectorPropertyCategory.new)
+ InspectorProperty.declare_property(InspectorPropertyGroup.can_handle, InspectorPropertyGroup.new)
+ InspectorProperty.declare_property(InspectorPropertySubgroup.can_handle, InspectorPropertySubgroup.new)
+ InspectorProperty.declare_property(InspectorPropertyBool.can_handle, InspectorPropertyBool.new)
+ InspectorProperty.declare_property(InspectorPropertyNumber.can_handle, InspectorPropertyNumber.new)
+ InspectorProperty.declare_property(InspectorPropertyString.can_handle, InspectorPropertyString.new)
+ InspectorProperty.declare_property(InspectorPropertyMultiline.can_handle, InspectorPropertyMultiline.new)
+ InspectorProperty.declare_property(InspectorPropertyVector2.can_handle, InspectorPropertyVector2.new)
+ InspectorProperty.declare_property(InspectorPropertyVector3.can_handle, InspectorPropertyVector3.new)
+ InspectorProperty.declare_property(InspectorPropertyColor.can_handle, InspectorPropertyColor.new)
+ InspectorProperty.declare_property(InspectorPropertyEnum.can_handle, InspectorPropertyEnum.new)
+ InspectorProperty.declare_property(InspectorPropertyFlags.can_handle, InspectorPropertyFlags.new)
diff --git a/scripts/inspector_property.gd b/scripts/inspector_property.gd
new file mode 100644
index 0000000..a524e6c
--- /dev/null
+++ b/scripts/inspector_property.gd
@@ -0,0 +1,151 @@
+# Copyright (c) 2022-2024 Mansur Isaev and contributors - MIT License
+# See `LICENSE.md` included in the source distribution for details.
+
+## Base InspectorProperty class.
+class_name InspectorProperty
+extends PanelContainer
+
+
+static var _declarations: Array[Dictionary] = []
+
+## Declares a supported type for properties.
+## [param Validation] must take two arguments [Object] and [Dictionary] and return [param true] if the property can be handled.
+## [codeblock] func can_handle(object: Object, property: Dictionary) -> bool:
+## return property["type"] == TYPE_BOOL
+## [/codeblock]
+## [br][param Constructor] must take two arguments [Object] and [Dictionary] and return [Control].
+## [codeblock] func create_control(object: Object, property: Dictionary) -> Control:
+## var label := Label.new()
+## label.set_text(property["name"])
+##
+## return label
+## [/codeblock]
+static func declare_property(validation: Callable, constructor: Callable) -> void:
+ assert(validation.is_valid(), "Invalid validation Callable.")
+ assert(constructor.is_valid(), "Invalid constructor Callable.")
+
+ if validation.is_valid() and constructor.is_valid():
+ _declarations.push_front({"validation": validation, "constructor": constructor})
+
+## Create and returns a [Control] node for a property. If property is not supported returns [param null].
+static func create_property(object: Object, property: Dictionary, editable: bool) -> Control:
+ assert(is_instance_valid(object), "Invalid Object!")
+ if not is_instance_valid(object):
+ return null
+
+ for decl: Dictionary in _declarations:
+ var validation: Callable = decl["validation"]
+ if not validation.is_valid() or not validation.call(object, property, editable):
+ continue
+
+ var constructor: Callable = decl["constructor"]
+ if not constructor.is_valid():
+ continue
+
+ var control: Control = constructor.call(object, property, editable)
+ if is_instance_valid(control):
+ control.set_name(property["name"])
+ return control
+
+ return null
+
+
+var _object: Object = null
+
+var _property: StringName = &""
+var _class_name: StringName = &""
+var _type: Variant.Type = TYPE_NIL
+var _hint: PropertyHint = PROPERTY_HINT_NONE
+var _hint_string: String = ""
+var _usage: int = PROPERTY_USAGE_NONE
+
+var _editable: bool = true
+
+
+func _init(object: Object, property: Dictionary, editable: bool) -> void:
+ self.set_theme_type_variation(&"InspectorProperty")
+
+ _object = object
+
+ _property = property["name"]
+ _class_name = property["class_name"]
+ _type = property["type"]
+ _hint = property["hint"]
+ _hint_string = property["hint_string"]
+ _usage = property["usage"]
+
+ _editable = editable
+
+
+func get_object() -> Object:
+ return _object
+
+func get_property() -> StringName:
+ return _property
+
+func get_class_name() -> StringName:
+ return _class_name
+
+func get_type() -> Variant.Type:
+ return _type
+
+func is_compatible_type(type: Variant.Type) -> bool:
+ return get_type() == type
+
+func get_hint() -> PropertyHint:
+ return _hint
+
+func get_hint_string() -> String:
+ return _hint_string
+
+func get_usage() -> PropertyUsageFlags:
+ return _usage
+
+
+func is_editable() -> bool:
+ return _editable
+
+
+func set_value(new_value: Variant) -> void:
+ get_object().set(get_property(), new_value)
+
+func get_value() -> Variant:
+ return get_object().get(get_property())
+
+func set_and_return_value(new_value: Variant) -> Variant:
+ set_value(new_value)
+ return get_value()
+
+## Returns created child [FlowContainer] node with [Label] and custom [Control] as children.
+func create_flow_container(title: String, control: Control, parent: Control = self) -> FlowContainer:
+ const MINIMUM_SIZE = Vector2(96.0, 16.0)
+
+ var container := FlowContainer.new()
+ container.set_name("Container")
+
+ if title:
+ var label := Label.new()
+ label.set_name("Label")
+ label.set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER)
+ label.set_text(title.capitalize())
+ label.set_mouse_filter(Control.MOUSE_FILTER_IGNORE)
+ label.set_text_overrun_behavior(TextServer.OVERRUN_TRIM_ELLIPSIS)
+ label.set_h_size_flags(Control.SIZE_EXPAND_FILL)
+ label.set_v_size_flags(Control.SIZE_EXPAND_FILL)
+ label.set_stretch_ratio(0.75)
+ label.set_custom_minimum_size(MINIMUM_SIZE)
+ container.add_child(label)
+
+ if is_instance_valid(control):
+ control.set_name("Property")
+ control.set_h_size_flags(Control.SIZE_EXPAND_FILL)
+ control.set_v_size_flags(Control.SIZE_EXPAND_FILL)
+ control.set_custom_minimum_size(MINIMUM_SIZE)
+ container.add_child(control)
+
+ parent.add_child(container)
+ return container
+
+## Return [param true] if [InspectorProperty] can handle the object and property.
+static func can_handle(object: Object, property: Dictionary, editable: bool) -> bool:
+ return false
diff --git a/scripts/inspector_property_type.gd b/scripts/inspector_property_type.gd
new file mode 100644
index 0000000..c95ef2e
--- /dev/null
+++ b/scripts/inspector_property_type.gd
@@ -0,0 +1,48 @@
+# Copyright (c) 2022-2024 Mansur Isaev and contributors - MIT License
+# See `LICENSE.md` included in the source distribution for details.
+
+class_name InspectorPropertyType
+
+
+static var _declarations: Dictionary = {}
+
+
+static func register_type(type: Variant.Type, name: StringName, constructor: Callable) -> void:
+ assert(constructor.is_valid(), "Invalid constructor Callable.")
+
+ if constructor.is_valid():
+ _declarations[type] = {"type": type, "name": name, "constructor": constructor}
+
+static func unregister_type(type: Variant.Type) -> bool:
+ return _declarations.erase(type)
+
+
+static func get_type_list() -> Array[Dictionary]:
+ var type_list: Array[Dictionary] = []
+
+ for type: Variant.Type in _declarations:
+ type_list.push_back({"type": type, "name": _declarations[type]["name"]})
+
+ return type_list
+
+
+static func is_valid_type(type: Variant.Type) -> bool:
+ return _declarations.has(type)
+
+static func create_control(type: Variant.Type, setter: Callable, getter: Callable, editable: bool) -> Control:
+ var value: Variant = getter.call()
+
+ if value == null:
+ value = type_convert(null, type)
+ elif type == TYPE_NIL:
+ type = typeof(value)
+
+ var decl: Dictionary = _declarations.get(type, {})
+ if decl.is_empty():
+ return null
+
+ var constructor: Callable = decl["constructor"]
+ if not constructor.is_valid():
+ return null
+
+ return constructor.call(setter, getter, editable)