# (c) 2023-present Eroax # (c) 2023-present Yagich # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) extends VBoxContainer class_name VariableViewer const REMOVE_ICON = preload("res://graph_node_renderer/textures/remove-icon.svg") @onready var new_variable_button: Button = %NewVariableButton @onready var variable_tree: Tree = %VariableTree @onready var types_popup: PopupMenu = %TypesPopup var root: TreeItem var _old_name: String signal top_field_edited(old_name: String, new_name: String, new_value: Variant) signal top_field_removed(field_name: String) func _ready() -> void: variable_tree.set_column_title(0, "Name") variable_tree.set_column_title(1, "Value") variable_tree.set_column_title(2, "Type") variable_tree.set_column_expand(1, true) variable_tree.set_column_expand_ratio(0, 15) variable_tree.set_column_expand_ratio(1, 70) variable_tree.set_column_expand_ratio(2, 15) root = variable_tree.create_item() rebuild_variable_tree() variable_tree.item_activated.connect(edit_item) variable_tree.item_edited.connect(commit_item_change) variable_tree.nothing_selected.connect( func(): var item := variable_tree.get_edited() if item == null: item = variable_tree.get_selected() if item != null: commit_item_change(item) item.deselect(variable_tree.get_selected_column()) ) variable_tree.button_clicked.connect(_on_variable_tree_button_clicked) for i in DeckType.Types.size() - 1: types_popup.add_radio_check_item(DeckType.type_str(i)) types_popup.id_pressed.connect(_on_types_popup_id_pressed) new_variable_button.pressed.connect(_on_new_variable_button_pressed) disable_new_button() func commit_item_change(item: TreeItem = variable_tree.get_edited()) -> void: var new_name := item.get_text(0) var new_value var type: DeckType.Types = item.get_metadata(2) match type: DeckType.Types.STRING: new_value = item.get_text(1) DeckType.Types.BOOL: new_value = item.is_checked(1) DeckType.Types.NUMERIC: new_value = item.get_range(1) _: new_value = item.get_meta(&"value") if not item.has_meta(&"container"): top_field_edited.emit(_old_name, new_name, new_value) else: var container = item.get_meta(&"container") # array if item.get_meta(&"indexed", false): var index := item.get_index() (container as Array)[index] = new_value # dictionary else: var key := new_name (container as Dictionary).erase(_old_name) (container as Dictionary)[key] = new_value set_item_value(item, new_value) func edit_item() -> void: var column := variable_tree.get_selected_column() var item := variable_tree.get_selected() # do nothing if this is an array and user is trying to edit index # (TODO: proper reordering) if column == 0 and item.get_meta(&"indexed"): return if column < 2: var value = item.get_meta(&"value") if column == 1 and (value is Array or value is Dictionary): item.collapsed = not item.collapsed return _old_name = item.get_text(0) variable_tree.edit_selected(true) elif column == 2: var pos := get_global_mouse_position() var r := Rect2i(Vector2i(pos), Vector2i(0, 100)) for i in types_popup.get_item_count(): types_popup.set_item_checked(i, false) types_popup.set_item_checked(item.get_metadata(2), true) types_popup.popup(r) func rebuild_variable_tree(data: Dictionary = {}) -> void: #variable_tree.clear() # godot will raw dog a nullptr later if we clear the whole tree for i in root.get_children(): i.free() for i in data: add_item(i, data[i]) func add_item(item_name: String, item_value: Variant, parent: TreeItem = root, container: Variant = null, indexed: bool = false) -> TreeItem: # the container parameter is -1 instead of null because Object#get_meta() logs an error # if the default parameter is null var item := variable_tree.create_item(parent) item.set_text(0, item_name) # TODO: proper null handling var value_type := typeof(item_value) var type: DeckType.Types if value_type == TYPE_NIL: type = DeckType.Types.STRING item_value = "" else: type = DeckType.INVERSE_GODOT_TYPES_MAP[typeof(item_value)] item.set_meta(&"container", container) item.set_meta(&"indexed", indexed) set_item_value(item, item_value) item.set_text(2, DeckType.type_str(type)) item.set_metadata(2, type) if item_value is Dictionary: for i in item_value: add_item(i, item_value[i], item, item_value) if item_value is Array: for i in (item_value as Array).size(): add_item(str(i), item_value[i], item, item_value, true) #item.collapsed = true item.add_button(2, REMOVE_ICON) return item func set_item_value(item: TreeItem, value: Variant) -> void: item.set_meta(&"value", value) var type: DeckType.Types = DeckType.INVERSE_GODOT_TYPES_MAP[typeof(value)] match type: DeckType.Types.NUMERIC: item.set_cell_mode(1, TreeItem.CELL_MODE_RANGE) item.set_range_config(1, -9999, 9999, 0.0001) item.set_range(1, value) DeckType.Types.BOOL: item.set_cell_mode(1, TreeItem.CELL_MODE_CHECK) item.set_checked(1, value) _: item.set_cell_mode(1, TreeItem.CELL_MODE_STRING) item.set_text(1, str(value)) if item.has_meta(&"container"): var container = item.get_meta(&"container") if container is Array: var index := item.get_index() container[index] = value else: var key := item.get_text(0) container[key] = value if item.get_parent() != root: refresh_item_value(item.get_parent()) func refresh_item_value(item: TreeItem) -> void: var value = item.get_meta(&"value") var type: DeckType.Types = DeckType.INVERSE_GODOT_TYPES_MAP[typeof(value)] match type: DeckType.Types.NUMERIC: item.set_cell_mode(1, TreeItem.CELL_MODE_RANGE) item.set_range_config(1, -9999, 9999, 0.0001) item.set_range(1, value) DeckType.Types.BOOL: item.set_cell_mode(1, TreeItem.CELL_MODE_CHECK) item.set_checked(1, value) _: item.set_cell_mode(1, TreeItem.CELL_MODE_STRING) item.set_text(1, str(value)) func _on_variable_tree_button_clicked(item: TreeItem, _column: int, _id: int, mouse_button_index: int) -> void: if mouse_button_index != MOUSE_BUTTON_LEFT: return # we only have a delete button for now, so assume it is what's clicked if not item.has_meta(&"container"): var key := item.get_text(0) top_field_removed.emit(key) item.free() else: var container = item.get_meta(&"container") var parent := item.get_parent() # array if item.get_meta(&"indexed", false): var index := item.get_index() (container as Array).remove_at(index) item.free() # go through the array and reset the index strings for i in (container as Array).size(): parent.get_child(i).set_text(0, str(i)) # dictionary else: (container as Dictionary).erase(item.get_text(0)) item.free() refresh_item_value(parent) func _on_types_popup_id_pressed(id: int) -> void: var current_item := variable_tree.get_selected() if current_item.get_metadata(2) == id: return current_item.set_metadata(2, id) var new_value: Variant match id as DeckType.Types: DeckType.Types.BOOL, DeckType.Types.STRING, DeckType.Types.NUMERIC: # simple types that can generally be converted between each other var target_type: Variant.Type = DeckType.GODOT_TYPES_MAP[id] new_value = type_convert(current_item.get_meta(&"value"), target_type) DeckType.Types.ARRAY: new_value = [] for i in current_item.get_children(): i.free() DeckType.Types.DICTIONARY: new_value = {} for i in current_item.get_children(): i.free() set_item_value(current_item, new_value) current_item.set_text(2, DeckType.type_str(id)) if not current_item.has_meta(&"container"): var field_name := current_item.get_text(0) top_field_edited.emit(field_name, field_name, new_value) func _on_new_variable_button_pressed() -> void: var selected := variable_tree.get_selected() # TODO: UX impr. - if selected is part of a container, add to that container instead # (but if selected is a container, prioritize adding to that) if selected == null or not (selected.get_meta(&"value") is Array or selected.get_meta(&"value") is Dictionary): # top field var var_name := "new_variable%s" % variable_tree.get_root().get_child_count() var new_item := add_item(var_name, "") top_field_edited.emit(var_name, var_name, "") new_item.select(0) else: var container = selected.get_meta(&"value") if container is Dictionary: var var_name := "new_key%s" % (container as Dictionary).size() var new_item := add_item(var_name, "", selected, container) new_item.select(0) container[var_name] = "" else: var index := (container as Array).size() (container as Array).append("") var new_item := add_item(str(index), "", selected, container, true) new_item.select(0) refresh_item_value(selected) func enable_new_button() -> void: new_variable_button.disabled = false new_variable_button.tooltip_text = "" func disable_new_button() -> void: new_variable_button.disabled = true new_variable_button.tooltip_text = "There is no deck open, so you can not create a new variable.\nOpen a deck first."