mirror of
https://codeberg.org/StreamGraph/StreamGraph.git
synced 2024-11-13 19:49:55 +01:00
f720efcc72
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>
513 lines
15 KiB
GDScript
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)
|