aboutsummaryrefslogtreecommitdiff
path: root/game/addons/zylann.hterrain/tools/inspector/inspector.gd
diff options
context:
space:
mode:
Diffstat (limited to 'game/addons/zylann.hterrain/tools/inspector/inspector.gd')
-rw-r--r--game/addons/zylann.hterrain/tools/inspector/inspector.gd479
1 files changed, 479 insertions, 0 deletions
diff --git a/game/addons/zylann.hterrain/tools/inspector/inspector.gd b/game/addons/zylann.hterrain/tools/inspector/inspector.gd
new file mode 100644
index 0000000..6039cee
--- /dev/null
+++ b/game/addons/zylann.hterrain/tools/inspector/inspector.gd
@@ -0,0 +1,479 @@
+
+# GDScript implementation of an inspector.
+# It generates controls for a provided list of properties,
+# which is easier to maintain than placing them by hand and connecting things in the editor.
+
+@tool
+extends Control
+
+const USAGE_FILE = "file"
+const USAGE_ENUM = "enum"
+
+signal property_changed(key, value)
+
+# Used for most simple types
+class HT_InspectorEditor:
+ var control = null
+ var getter := Callable()
+ var setter := Callable()
+ var key_label : Label
+
+
+# Used when the control cannot hold the actual value
+class HT_InspectorResourceEditor extends HT_InspectorEditor:
+ var value = null
+ var label = null
+
+ func get_value():
+ return value
+
+ func set_value(v):
+ value = v
+ label.text = "null" if v == null else v.resource_path
+
+
+class HT_InspectorVectorEditor extends HT_InspectorEditor:
+ signal value_changed(v)
+
+ var value := Vector2()
+ var xed = null
+ var yed = null
+
+ func get_value():
+ return value
+
+ func set_value(v):
+ xed.value = v.x
+ yed.value = v.y
+ value = v
+
+ func _component_changed(v, i):
+ value[i] = v
+ value_changed.emit(value)
+
+
+# TODO Rename _schema
+var _prototype = null
+var _edit_signal := true
+# name => editor
+var _editors := {}
+
+# Had to separate the container because otherwise I can't open dialogs properly...
+@onready var _grid_container = get_node("GridContainer")
+@onready var _file_dialog = get_node("OpenFileDialog")
+
+
+func _ready():
+ _file_dialog.visibility_changed.connect(
+ call_deferred.bind("_on_file_dialog_visibility_changed"))
+# Test
+# set_prototype({
+# "seed": {
+# "type": TYPE_INT,
+# "randomizable": true
+# },
+# "base_height": {
+# "type": TYPE_REAL,
+# "range": {"min": -1000.0, "max": 1000.0, "step": 0.1}
+# },
+# "height_range": {
+# "type": TYPE_REAL,
+# "range": {"min": -1000.0, "max": 1000.0, "step": 0.1 },
+# "default_value": 500.0
+# },
+# "streamed": {
+# "type": TYPE_BOOL
+# },
+# "texture": {
+# "type": TYPE_OBJECT,
+# "object_type": Resource
+# }
+# })
+
+
+# TODO Rename clear_schema
+func clear_prototype():
+ _editors.clear()
+ var i = _grid_container.get_child_count() - 1
+ while i >= 0:
+ var child = _grid_container.get_child(i)
+ _grid_container.remove_child(child)
+ child.call_deferred("free")
+ i -= 1
+ _prototype = null
+
+
+func get_value(key: String):
+ var editor = _editors[key]
+ return editor.getter.call()
+
+
+func get_values():
+ var values = {}
+ for key in _editors:
+ var editor = _editors[key]
+ values[key] = editor.getter.call()
+ return values
+
+
+func set_value(key: String, value):
+ var editor = _editors[key]
+ editor.setter.call(value)
+
+
+func set_values(values: Dictionary):
+ for key in values:
+ if _editors.has(key):
+ var editor = _editors[key]
+ var v = values[key]
+ editor.setter.call(v)
+
+
+# TODO Rename set_schema
+func set_prototype(proto: Dictionary):
+ clear_prototype()
+
+ for key in proto:
+ var prop = proto[key]
+
+ var label := Label.new()
+ label.text = str(key).capitalize()
+ _grid_container.add_child(label)
+
+ var editor := _make_editor(key, prop)
+ editor.key_label = label
+
+ if prop.has("default_value"):
+ editor.setter.call(prop.default_value)
+
+ _editors[key] = editor
+
+ if prop.has("enabled"):
+ set_property_enabled(key, prop.enabled)
+
+ _grid_container.add_child(editor.control)
+
+ _prototype = proto
+
+
+func trigger_all_modified():
+ for key in _prototype:
+ var value = _editors[key].getter.call_func()
+ property_changed.emit(key, value)
+
+
+func set_property_enabled(prop_name: String, enabled: bool):
+ var ed = _editors[prop_name]
+
+ if ed.control is BaseButton:
+ ed.control.disabled = not enabled
+
+ elif ed.control is SpinBox:
+ ed.control.editable = enabled
+
+ elif ed.control is LineEdit:
+ ed.control.editable = enabled
+
+ # TODO Support more editors
+
+ var col = ed.key_label.modulate
+ if enabled:
+ col.a = 1.0
+ else:
+ col.a = 0.5
+ ed.key_label.modulate = col
+
+
+func _make_editor(key: String, prop: Dictionary) -> HT_InspectorEditor:
+ var ed : HT_InspectorEditor = null
+
+ var editor : Control = null
+ var getter : Callable
+ var setter : Callable
+ var extra = null
+
+ match prop.type:
+ TYPE_INT, \
+ TYPE_FLOAT:
+ var pre = null
+ if prop.has("randomizable") and prop.randomizable:
+ editor = HBoxContainer.new()
+ pre = Button.new()
+ pre.pressed.connect(_randomize_property_pressed.bind(key))
+ pre.text = "Randomize"
+ editor.add_child(pre)
+
+ if prop.type == TYPE_INT and prop.has("usage") and prop.usage == USAGE_ENUM:
+ # Enumerated value
+ assert(prop.has("enum_items"))
+ var option_button := OptionButton.new()
+
+ for i in len(prop.enum_items):
+ var item = prop.enum_items[i]
+ option_button.add_item(item)
+
+ # TODO We assume index, actually
+ getter = option_button.get_selected_id
+ setter = option_button.select
+ option_button.item_selected.connect(_property_edited.bind(key))
+
+ editor = option_button
+
+ else:
+ # Numeric value
+ var spinbox := SpinBox.new()
+ # Spinboxes have shit UX when not expanded...
+ spinbox.custom_minimum_size = Vector2(120, 16)
+ _setup_range_control(spinbox, prop)
+ spinbox.value_changed.connect(_property_edited.bind(key))
+
+ # TODO In case the type is INT, the getter should return an integer!
+ getter = spinbox.get_value
+ setter = spinbox.set_value
+
+ var show_slider = prop.has("range") \
+ and not (prop.has("slidable") \
+ and prop.slidable == false)
+
+ if show_slider:
+ if editor == null:
+ editor = HBoxContainer.new()
+ var slider := HSlider.new()
+ # Need to give some size because otherwise the slider is hard to click...
+ slider.custom_minimum_size = Vector2(32, 16)
+ _setup_range_control(slider, prop)
+ slider.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ spinbox.share(slider)
+ editor.add_child(slider)
+ editor.add_child(spinbox)
+ else:
+ spinbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ if editor == null:
+ editor = spinbox
+ else:
+ editor.add_child(spinbox)
+
+ TYPE_STRING:
+ if prop.has("usage") and prop.usage == USAGE_FILE:
+ editor = HBoxContainer.new()
+
+ var line_edit := LineEdit.new()
+ line_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ editor.add_child(line_edit)
+
+ var exts = []
+ if prop.has("exts"):
+ exts = prop.exts
+
+ var load_button := Button.new()
+ load_button.text = "..."
+ load_button.pressed.connect(_on_ask_load_file.bind(key, exts))
+ editor.add_child(load_button)
+
+ line_edit.text_submitted.connect(_property_edited.bind(key))
+ getter = line_edit.get_text
+ setter = line_edit.set_text
+
+ else:
+ editor = LineEdit.new()
+ editor.text_submitted.connect(_property_edited.bind(key))
+ getter = editor.get_text
+ setter = editor.set_text
+
+ TYPE_COLOR:
+ editor = ColorPickerButton.new()
+ editor.color_changed.connect(_property_edited.bind(key))
+ getter = editor.get_pick_color
+ setter = editor.set_pick_color
+
+ TYPE_BOOL:
+ editor = CheckBox.new()
+ editor.toggled.connect(_property_edited.bind(key))
+ getter = editor.is_pressed
+ setter = editor.set_pressed
+
+ TYPE_OBJECT:
+ # TODO How do I even check inheritance if I work on the class themselves, not instances?
+ if prop.object_type == Resource:
+ editor = HBoxContainer.new()
+
+ var label := Label.new()
+ label.text = "null"
+ label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ label.clip_text = true
+ label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
+ editor.add_child(label)
+
+ var load_button := Button.new()
+ load_button.text = "Load..."
+ load_button.pressed.connect(_on_ask_load_texture.bind(key))
+ editor.add_child(load_button)
+
+ var clear_button := Button.new()
+ clear_button.text = "Clear"
+ clear_button.pressed.connect(_on_ask_clear_texture.bind(key))
+ editor.add_child(clear_button)
+
+ ed = HT_InspectorResourceEditor.new()
+ ed.label = label
+ getter = ed.get_value
+ setter = ed.set_value
+
+ TYPE_VECTOR2:
+ editor = HBoxContainer.new()
+
+ ed = HT_InspectorVectorEditor.new()
+
+ var xlabel := Label.new()
+ xlabel.text = "x"
+ editor.add_child(xlabel)
+ var xed := SpinBox.new()
+ xed.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ xed.step = 0.01
+ xed.min_value = -10000
+ xed.max_value = 10000
+ # TODO This will fire twice (for each coordinate), hmmm...
+ xed.value_changed.connect(ed._component_changed.bind(0))
+ editor.add_child(xed)
+
+ var ylabel := Label.new()
+ ylabel.text = "y"
+ editor.add_child(ylabel)
+ var yed = SpinBox.new()
+ yed.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ yed.step = 0.01
+ yed.min_value = -10000
+ yed.max_value = 10000
+ yed.value_changed.connect(ed._component_changed.bind(1))
+ editor.add_child(yed)
+
+ ed.xed = xed
+ ed.yed = yed
+ ed.value_changed.connect(_property_edited.bind(key))
+ getter = ed.get_value
+ setter = ed.set_value
+
+ _:
+ editor = Label.new()
+ editor.text = "<not editable>"
+ getter = _dummy_getter
+ setter = _dummy_setter
+
+ if not(editor is CheckButton):
+ editor.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+
+ if ed == null:
+ # Default
+ ed = HT_InspectorEditor.new()
+ ed.control = editor
+ ed.getter = getter
+ ed.setter = setter
+
+ return ed
+
+
+static func _setup_range_control(range_control: Range, prop):
+ if prop.type == TYPE_INT:
+ range_control.step = 1
+ range_control.rounded = true
+ else:
+ range_control.step = 0.1
+ if prop.has("range"):
+ range_control.min_value = prop.range.min
+ range_control.max_value = prop.range.max
+ if prop.range.has("step"):
+ range_control.step = prop.range.step
+ else:
+ # Where is INT_MAX??
+ range_control.min_value = -0x7fffffff
+ range_control.max_value = 0x7fffffff
+
+
+func _property_edited(value, key):
+ if _edit_signal:
+ property_changed.emit(key, value)
+
+
+func _randomize_property_pressed(key):
+ var prop = _prototype[key]
+ var v = 0
+
+ # TODO Support range step
+ match prop.type:
+ TYPE_INT:
+ if prop.has("range"):
+ v = randi() % (prop.range.max - prop.range.min) + prop.range.min
+ else:
+ v = randi() - 0x7fffffff
+ TYPE_FLOAT:
+ if prop.has("range"):
+ v = randf_range(prop.range.min, prop.range.max)
+ else:
+ v = randf()
+
+ _editors[key].setter.call(v)
+
+
+func _dummy_getter():
+ pass
+
+
+func _dummy_setter(v):
+ # TODO Could use extra data to store the value anyways?
+ pass
+
+
+func _on_ask_load_texture(key):
+ _open_file_dialog(["*.png ; PNG files"], _on_texture_selected.bind(key),
+ FileDialog.ACCESS_RESOURCES)
+
+
+func _open_file_dialog(filters: Array, callback: Callable, access: int):
+ _file_dialog.access = access
+ _file_dialog.clear_filters()
+ for filter in filters:
+ _file_dialog.add_filter(filter)
+
+ # Can't just use one-shot signals because the dialog could be closed without choosing a file...
+# if not _file_dialog.file_selected.is_connected(callback):
+# _file_dialog.file_selected.connect(callback, Object.CONNECT_ONE_SHOT)
+ _file_dialog.file_selected.connect(callback)
+
+ _file_dialog.popup_centered_ratio(0.7)
+
+
+func _on_file_dialog_visibility_changed():
+ if _file_dialog.visible == false:
+ # Disconnect listeners automatically,
+ # so we can re-use the same dialog with different listeners
+ var cons = _file_dialog.get_signal_connection_list("file_selected")
+ for con in cons:
+ _file_dialog.file_selected.disconnect(con.callable)
+
+
+func _on_texture_selected(path: String, key):
+ var tex = load(path)
+ if tex == null:
+ return
+ var ed = _editors[key]
+ ed.setter.call(tex)
+ _property_edited(tex, key)
+
+
+func _on_ask_clear_texture(key):
+ var ed = _editors[key]
+ ed.setter.call(null)
+ _property_edited(null, key)
+
+
+func _on_ask_load_file(key, exts):
+ var filters := []
+ for ext in exts:
+ filters.append(str("*.", ext, " ; ", ext.to_upper(), " files"))
+ _open_file_dialog(filters, _on_file_selected.bind(key), FileDialog.ACCESS_FILESYSTEM)
+
+
+func _on_file_selected(path, key):
+ var ed = _editors[key]
+ ed.setter.call(path)
+ _property_edited(path, key)