# (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 PanelContainer class_name Sidebar @onready var deck_menu: AccordionMenu = %Deck @onready var node_menu: AccordionMenu = %Node @onready var node_list_menu: AccordionMenu = %"Node List" const SYSTEM_CODE_FONT = preload("res://graph_node_renderer/system_code_font.tres") var edited_deck_id: String var edited_node_id: String var deck_inspector: DeckInspector var node_inspector: NodeInspector static var collapsed_menus: Dictionary # Dictionary[String -> id, bool -> collapsed] signal go_to_node_requested(node_id: String) func _ready() -> void: set_edited_deck() func set_edited_deck(id: String = "") -> void: if edited_deck_id == id and not id.is_empty(): return edited_deck_id = id set_edited_node("") for i in deck_menu.get_children(): i.queue_free() if id.is_empty(): var label := Label.new() label.autowrap_mode = TextServer.AUTOWRAP_WORD label.text = "There is no open Deck. Open or create one to edit its properties." deck_menu.add_child(label) deck_inspector = null return deck_inspector = DeckInspector.new(id) for i in deck_inspector.nodes: deck_menu.add_child(i) func refresh_node_list(_unused = null) -> void: for i in node_list_menu.get_children(): i.queue_free() for node_id: String in DeckHolder.get_deck(edited_deck_id).nodes: var node := DeckHolder.get_deck(edited_deck_id).get_node(node_id) var b := Button.new() b.flat = true b.text = "\"%s\"" % node.name b.pressed.connect( func(): go_to_node_requested.emit(node._id) ) b.size_flags_horizontal = Control.SIZE_EXPAND_FILL b.tooltip_text = "Click to focus this node in the graph" b.alignment = HORIZONTAL_ALIGNMENT_LEFT b.clip_text = true var cb := Button.new() cb.text = "Copy ID" cb.pressed.connect( func(): DisplayServer.clipboard_set(node._id) ) #Util.safe_connect(node.renamed, #func(new_name: String): # TODO: bad. if the node (deck node or button, whichever) # gets removed the captures get invalidated. # maybe dont use lambda here #b.text = "\"%s\"" % node.name #) #Util.safe_connect(node.renamed, _set_button_text.bind(b)) # this is probably bad too, but its the only one that worked so far. Util.safe_connect(node.renamed, refresh_node_list) var hb := HBoxContainer.new() hb.add_child(b) hb.add_child(cb) node_list_menu.add_child(hb) #func _set_button_text(new_text: String, button: Button) -> void: # this was also bad. "Cannot convert argument 2 from Object to Object" 🙃 #button.text = "\"%s\"" % new_text func set_edited_node(id: String = "") -> void: if edited_node_id == id and not id.is_empty(): return edited_node_id = id for i in node_menu.get_children(): #i.queue_free() i.free() if id.is_empty(): var label := Label.new() label.autowrap_mode = TextServer.AUTOWRAP_WORD label.text = "There is no Node selected (or multiple are selected). Select a single Node to edit its properties." node_menu.add_child(label) node_menu.draw_tree = false node_inspector = null return #await get_tree().process_frame node_menu.draw_tree = true node_inspector = NodeInspector.new(edited_deck_id, id) node_inspector.go_to_node_requested.connect( func(): go_to_node_requested.emit(edited_node_id) ) for i in node_inspector.nodes: node_menu.add_child(i) DeckHolder.get_deck(edited_deck_id).get_node(id).about_to_free.connect( func(): set_edited_node() ) #DeckHolder.get_deck(edited_deck_id).node_removed.connect( #func(node: DeckNode): #if node._id == edited_node_id: #set_edited_node() #) static func create_menu(title: String, id: String, default_collapsed: bool = false) -> AccordionMenu: var res := AccordionMenu.new() res.set_title(title) res.collapsed = collapsed_menus.get(id, default_collapsed) res.menu_collapsed.connect( func(p_is_visible: bool) -> void: collapsed_menus[id] = p_is_visible ) return res class Inspector extends HBoxContainer: func _init(text: String, editor: Control = null) -> void: var _label = Label.new() _label.text = text _label.size_flags_horizontal = Control.SIZE_EXPAND_FILL add_child(_label) if editor: editor.size_flags_horizontal = Control.SIZE_EXPAND_FILL add_child(editor) class DeckInspector: var ref: WeakRef var nodes: Array[Control] var group_descriptors_inspector: GroupDescriptorsInspector func add_inspector(text: String, control: Control = null) -> void: nodes.append(Inspector.new(text, control)) static func create_label(text: String) -> Label: var l := Label.new() l.text = text l.size_flags_horizontal = Control.SIZE_EXPAND_FILL return l func create_hb_label(text: String, control: Control) -> HBoxContainer: var hb := HBoxContainer.new() control.size_flags_horizontal = Control.SIZE_EXPAND_FILL hb.add_child(DeckInspector.create_label(text)) hb.add_child(control) return hb func _init(id: String) -> void: ref = weakref(DeckHolder.get_deck(id)) var deck: Deck = ref.get_ref() as Deck var lib_menu := AccordionMenu.new() lib_menu.set_title("Library Group") var lib_group_text: String if deck.is_library: lib_group_text = "This deck is open as a library group. You may not edit it here.\nTo edit it, you have to open the file it's in." elif not deck.is_group: lib_group_text = "You may save this deck as a Library Group to reuse it in future decks.\nYou may edit how it will appear." else: lib_group_text = "This deck is a group." var l := DeckInspector.create_label(lib_group_text) l.autowrap_mode = TextServer.AUTOWRAP_WORD l.custom_minimum_size.x = 40 lib_menu.add_child(l) lib_menu.set_ignore_child_lines(0) var name_field := LineEdit.new() name_field.placeholder_text = "Name" name_field.text = deck.lib_name name_field.text_changed.connect( func(new_text: String): deck.lib_name = new_text ) name_field.editable = not deck.is_group lib_menu.add_child(create_hb_label("Library name:", name_field)) var desc_field := TextEdit.new() desc_field.wrap_mode = TextEdit.LINE_WRAPPING_BOUNDARY desc_field.placeholder_text = "Description" desc_field.text = deck.lib_description desc_field.text_changed.connect( func(): deck.lib_description = desc_field.text ) desc_field.editable = not deck.is_group desc_field.custom_minimum_size.y = 100 desc_field.custom_minimum_size.x = 100 lib_menu.add_child(create_hb_label("Library description:", desc_field)) group_descriptors_inspector = GroupDescriptorsInspector.new(deck) nodes.append(lib_menu) nodes.append(group_descriptors_inspector.menu) class GroupDescriptorsInspector: var menu: AccordionMenu var inputs_menu: AccordionMenu var outputs_menu: AccordionMenu var deck: Deck func _init(p_deck: Deck) -> void: deck = p_deck menu = AccordionMenu.new() menu.set_title("Group Inputs/Outputs") inputs_menu = Sidebar.create_menu("Inputs", "group_inputs", true) outputs_menu = Sidebar.create_menu("Outputs", "group_outputs", true) deck.node_added.connect(_on_deck_node_added) create_inputs() create_outputs() menu.add_child(inputs_menu) menu.add_child(outputs_menu) func _on_deck_node_added(node: DeckNode) -> void: if node.node_type == "group_input": Util.safe_connect(node.ports_updated, refresh_inputs.bind(node)) create_inputs(node) if node.node_type == "group_output": Util.safe_connect(node.ports_updated, refresh_outputs.bind(node)) create_outputs(node) func refresh_inputs(node: DeckNode) -> void: # oh boy var root := menu.get_tree().get_root() var fo := root.gui_get_focus_owner() if inputs_menu.is_ancestor_of(fo): return create_inputs(node) func refresh_outputs(node: DeckNode) -> void: # oh boy var root := menu.get_tree().get_root() var fo := root.gui_get_focus_owner() if outputs_menu.is_ancestor_of(fo): return create_outputs(node) func create_inputs(node: DeckNode = null) -> void: create_io(true, node) func create_outputs(node: DeckNode = null) -> void: create_io(false, node) func create_io(input: bool = true, node: DeckNode = null) -> void: var io_menu := inputs_menu if input else outputs_menu for i in io_menu.get_children(false): i.queue_free() if node == null: if input and deck.get_node(deck.group_input_node) == null: var l := DeckInspector.create_label("No input node") io_menu.add_child(l) return if not input and deck.get_node(deck.group_output_node) == null: var l := DeckInspector.create_label("No output node") io_menu.add_child(l) return if input: node = deck.get_node(deck.group_input_node) else: node = deck.get_node(deck.group_output_node) var refresh_func := refresh_inputs if input else refresh_outputs Util.safe_connect(node.ports_updated, refresh_func.bind(node)) var ports := node.get_output_ports() if input else node.get_input_ports() var get_port := func(index: int) -> Deck.GroupPort: if input: return deck.group_descriptors.get_input_port(index) else: return deck.group_descriptors.get_output_port(index) for i in ports.size(): var port_override: Deck.GroupPort = get_port.call(i) var menu_title := "Input %s" if input else "Output %s" var menu_id := "group_input_%s" if input else "group_output_%s" var _menu := Sidebar.create_menu(menu_title % i, menu_id % i, true) #input_menu.set_title("Input %s" % i) var port_label_field := LineEdit.new() #port_label_field.placeholder_text = ports[i].label port_label_field.placeholder_text = menu_title % i port_label_field.text = port_override.label port_label_field.text_changed.connect( func(new_text: String) -> void: port_override.label = new_text ) _menu.add_child(Inspector.new("Label:", port_label_field)) var type_combo := OptionButton.new() for type in DeckType.Types.size(): type_combo.add_item(DeckType.type_str(type).capitalize()) type_combo.selected = port_override.type type_combo.item_selected.connect( func(idx: int) -> void: port_override.type = idx as DeckType.Types ) _menu.add_child(Inspector.new("Type:", type_combo)) var usage_combo := OptionButton.new() for usage in Port.UsageType.size(): usage_combo.add_item(Port.UsageType.keys()[usage].capitalize()) usage_combo.selected = port_override.usage_type usage_combo.item_selected.connect( func(idx: int) -> void: port_override.usage_type = idx as Port.UsageType ) _menu.add_child(Inspector.new("Usage:", usage_combo)) var descriptor_field := LineEdit.new() descriptor_field.placeholder_text = "Descriptor (advanced)" descriptor_field.tooltip_text = "Advanced use only.\nSeparate arguments with colon (:).\nPress Enter to confirm." descriptor_field.text = port_override.descriptor descriptor_field.text_submitted.connect( func(new_text: String) -> void: port_override.descriptor = new_text ) _menu.add_child(Inspector.new("Descriptor:", descriptor_field)) io_menu.add_child(_menu) class NodeInspector: var ref: WeakRef var nodes: Array[Control] var _name_field: LineEdit signal go_to_node_requested() var DESCRIPTOR_SCENES := { "button": load("res://graph_node_renderer/descriptors/button_descriptor.tscn"), "field": load("res://graph_node_renderer/descriptors/field_descriptor.tscn"), "singlechoice": load("res://graph_node_renderer/descriptors/single_choice_descriptor.tscn"), "codeblock": load("res://graph_node_renderer/descriptors/codeblock_descriptor.tscn"), "checkbox": load("res://graph_node_renderer/descriptors/check_box_descriptor.tscn"), "spinbox": load("res://graph_node_renderer/descriptors/spin_box_descriptor.tscn"), "label": load("res://graph_node_renderer/descriptors/label_descriptor.tscn"), } func add_inspector(text: String, control: Control = null) -> void: nodes.append(Inspector.new(text, control)) func _name_field_rename(new_name: String) -> void: if _name_field.has_focus(): return _name_field.text = new_name func _init(deck_id: String, id: String) -> void: ref = weakref(DeckHolder.get_deck(deck_id).get_node(id)) var node: DeckNode = ref.get_ref() as DeckNode var type_label := DeckInspector.create_label(node.node_type) var type_label_settings := LabelSettings.new() type_label_settings.font = SYSTEM_CODE_FONT type_label_settings.font_size = 14 type_label.label_settings = type_label_settings var copy_button := Button.new() copy_button.text = "Copy" copy_button.pressed.connect( func(): DisplayServer.clipboard_set(node.node_type) ) var hb := HBoxContainer.new() hb.add_child(type_label) hb.add_child(copy_button) add_inspector("Node Type:", hb) _name_field = LineEdit.new() _name_field.placeholder_text = "Node name" _name_field.text = node.name _name_field.text_changed.connect(node.rename) _name_field.editable = not node._belonging_to.is_library node.renamed.connect(_name_field_rename) add_inspector("Node name:", _name_field) var focus_button := Button.new() focus_button.text = "Focus node" focus_button.pressed.connect( func(): go_to_node_requested.emit() ) focus_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL nodes.append(focus_button) add_port_menu(node.get_input_ports(), node) add_port_menu(node.get_output_ports(), node) func add_port_menu(ports: Array[Port], node: DeckNode) -> void: if ports.is_empty(): return var ports_menu := AccordionMenu.new() ports_menu.draw_background = true var is_output := ports[0].port_type == DeckNode.PortType.OUTPUT ports_menu.set_title("Output Ports" if is_output else "Input Ports") for port in ports: var acc := AccordionMenu.new() acc.draw_background = true acc.set_title("Port %s" % port.index_of_type) var label_label := DeckInspector.create_label("Name: %s" % port.label) acc.add_child(label_label) var type_label := DeckInspector.create_label("Type: %s" % DeckType.type_str(port.type).to_lower()) acc.add_child(type_label) var usage_is_both := port.usage_type == Port.UsageType.BOTH var usage_text = "Both (Value Request or Trigger)" if usage_is_both else Port.UsageType.keys()[port.usage_type].capitalize() var usage_label := DeckInspector.create_label("Usage: %s" % usage_text) acc.add_child(usage_label) var descriptor_split := port.descriptor.split(":") if DESCRIPTOR_SCENES.has(descriptor_split[0]): var port_hb := HBoxContainer.new() var value_label := DeckInspector.create_label("Value:") port_hb.add_child(value_label) var desc: DescriptorContainer = DESCRIPTOR_SCENES[descriptor_split[0]].instantiate() desc.ready.connect( func(): desc.set_up_from_port(port, node, false) ) port_hb.add_child(desc) acc.add_child(port_hb) ports_menu.add_child(acc) ports_menu.collapsed = true nodes.append(ports_menu)