484 lines
12 KiB
GDScript
484 lines
12 KiB
GDScript
|
|
# 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:Array = prop.enum_items[i]
|
|
var value:int = item[0]
|
|
var text:String = item[1]
|
|
option_button.add_item(text, value)
|
|
|
|
getter = option_button.get_selected_id
|
|
setter = func select_id(id: int):
|
|
var index:int = option_button.get_item_index(id)
|
|
assert(index >= 0)
|
|
option_button.select(index)
|
|
|
|
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)
|