miggor-StreamGraph/graph_node_renderer/sidebar/sidebar.gd
Lera Elvoé f720efcc72 lib group storage rework (#162)
saving decks that use lib groups will no longer save the whole file path to that library (at the expense of the structure needing to be the same)

also some ui/ux improvements:
- more menus in sidebar remember their collapsed state between deck/node switches
- adding a lib group will name the group node appropriately
- save dialog properly remembers the most recent path when invoked via ctrl+s and the deck hasn't been saved before

Reviewed-on: https://codeberg.org/StreamGraph/StreamGraph/pulls/162
Co-authored-by: Lera Elvoé <yagich@poto.cafe>
Co-committed-by: Lera Elvoé <yagich@poto.cafe>
2024-05-26 14:13:42 +00:00

513 lines
15 KiB
GDScript

# (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:
if id.is_empty():
id = title
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_menu := Sidebar.create_menu("Library Group", "lib_group", true)
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 in search results."
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")
menu = Sidebar.create_menu("Group Inputs/Outputs", "group_io", true)
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))
if input:
# descriptors are very rarely used on output ports. when they are,
# the nodes are fairly special, so we're not going to allow them on groups.
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 is_output := ports[0].port_type == DeckNode.PortType.OUTPUT
var ports_menu := Sidebar.create_menu("Output Ports" if is_output else "Input Ports", "", false)
ports_menu.draw_background = true
for port in ports:
#var acc := AccordionMenu.new()
var id = "output_%s_menu" if is_output else "input_%s_menu"
var acc := Sidebar.create_menu("Port %s" % port.index_of_type, id % port.index_of_type, true)
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)
nodes.append(ports_menu)