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)