add a sidebar (#146)

Reviewed-on: https://codeberg.org/StreamGraph/StreamGraph/pulls/146
Co-authored-by: Lera Elvoé <yagich@poto.cafe>
Co-committed-by: Lera Elvoé <yagich@poto.cafe>
This commit is contained in:
Lera Elvoé 2024-04-16 15:16:38 +00:00 committed by yagich
parent ed5a0f427b
commit 1ce3cd0367
18 changed files with 733 additions and 28 deletions

View file

@ -2,9 +2,59 @@
# (c) 2023-present Yagich
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
class_name Util
## A class with commonly used methods.
static var _batches: Dictionary # Dictionary[StringName, BatchConnection]
## Connects the [param p_func] to [param p_signal] if that connection doesn't already exist.
static func safe_connect(p_signal: Signal, p_func: Callable) -> void:
if not p_signal.is_connected(p_func):
if not p_signal.is_connected(p_func) and not p_signal.is_null() and p_func.is_valid():
p_signal.connect(p_func)
## Disconnects the [param p_func] from [param p_signal] if that connection exists.
static func safe_disconnect(p_signal: Signal, p_func: Callable) -> void:
if p_signal.is_connected(p_func) and not p_signal.is_null() and p_func.is_valid():
p_signal.disconnect(p_func)
## Returns a new [Util.BatchConnection] object.
static func batch_begin() -> BatchConnection:
return BatchConnection.new()
## Adds the [param batch] object to storage, to be retrieved later with [param key].
static func push_batch(batch: BatchConnection, key: StringName) -> void:
_batches[key] = batch
## Disconnects the signals in a batch connection stored at [param key]. See [method push_batch].
static func pop_batch(key: StringName) -> void:
var b: BatchConnection = _batches.get(key, null)
if not b:
return
b._pop()
_batches.erase(key)
## An object representing multiple connections.
##
## Useful when there's a need to connect multiple signals in one operation, to be disconnected later.
class BatchConnection:
var _connections := {} # Dictionary[Signal, Array[Callable]]
## Add a new connection from [param p_signal] to [param p_func]. Uses [method Util.safe_connect].
func add(p_signal: Signal, p_func: Callable) -> void:
var arr: Array = _connections.get(p_signal, [])
arr.append(p_func)
_connections[p_signal] = arr
Util.safe_connect(p_signal, p_func)
func _pop() -> void:
for sig: Signal in _connections:
for f in _connections[sig]:
Util.safe_disconnect(sig, f)

View file

@ -74,6 +74,8 @@ var _deck_to_save: WeakRef
@onready var rpc_setup_dialog := $RPCSetupDialog as RPCSetupDialog
@onready var bottom_dock: BottomDock = %BottomDock
@onready var sidebar_split: HSplitContainer = %SidebarSplit
@onready var sidebar: Sidebar = %Sidebar as Sidebar
signal quit_completed()
signal rpc_start_requested(port: int)
@ -167,6 +169,16 @@ func _ready() -> void:
get_active_deck_renderer().dirty = true
get_active_deck().remove_variable(field_name)
)
sidebar_split.dragger_visibility = SplitContainer.DRAGGER_HIDDEN_COLLAPSED
sidebar.go_to_node_requested.connect(
func(node_id: String) -> void:
# we can reasonably assume its the current deck
var deck := get_active_deck()
var node := deck.get_node(node_id)
var deck_renderer := get_active_deck_renderer()
deck_renderer.focus_node(deck_renderer.get_node_renderer(node))
)
func reset_popup_menu_shortcuts() -> void:
@ -195,8 +207,13 @@ func _notification(what: int) -> void:
func _on_tab_container_tab_about_to_change(previous_tab: int) -> void:
var deck := get_deck_at_tab(previous_tab)
if deck.variables_updated.is_connected(bottom_dock.rebuild_variable_tree):
deck.variables_updated.disconnect(bottom_dock.rebuild_variable_tree)
Util.safe_disconnect(deck.variables_updated, bottom_dock.rebuild_variable_tree)
var deck_renderer := get_deck_renderer_at_tab(previous_tab)
Util.safe_disconnect(deck_renderer.node_selected, set_sidebar_node)
Util.safe_disconnect(deck_renderer.node_deselected, set_sidebar_node)
Util.pop_batch(&"sidebar_signals")
func _on_tab_container_tab_changed(tab: int) -> void:
@ -206,6 +223,30 @@ func _on_tab_container_tab_changed(tab: int) -> void:
bottom_dock.rebuild_variable_tree(get_active_deck().variable_stack)
var deck := get_active_deck()
deck.variables_updated.connect(bottom_dock.rebuild_variable_tree.bind(deck.variable_stack))
sidebar.set_edited_deck(deck.id)
get_active_deck_renderer().node_selected.connect(set_sidebar_node)
get_active_deck_renderer().node_deselected.connect(set_sidebar_node)
sidebar.refresh_node_list()
var batch := Util.batch_begin()
batch.add(deck.node_added, refresh_sidebar_node_list)
batch.add(deck.node_removed, refresh_sidebar_node_list)
Util.push_batch(batch, &"sidebar_signals")
func set_sidebar_node(_node: Node) -> void:
var count = get_active_deck_renderer().get_selected_nodes().size()
if count != 1:
sidebar.set_edited_node()
return
var dnode := (get_active_deck_renderer().get_selected_nodes()[0] as DeckNodeRendererGraphNode).node
sidebar.set_edited_node(dnode._id)
func refresh_sidebar_node_list(_unused1 = null, _unused2 = null, _unused3 = null) -> void:
sidebar.refresh_node_list()
## Called when the File button in the [MenuBar] is pressed with the [param id]
@ -595,6 +636,13 @@ func _unhandled_key_input(event: InputEvent) -> void:
if RendererShortcuts.check_shortcut("toggle_bottom_dock", event):
bottom_dock.visible = !bottom_dock.visible
accept_event()
if RendererShortcuts.check_shortcut("toggle_sidebar", event):
sidebar.visible = !sidebar.visible
if sidebar.visible:
sidebar_split.dragger_visibility = SplitContainer.DRAGGER_VISIBLE
else:
sidebar_split.dragger_visibility = SplitContainer.DRAGGER_HIDDEN_COLLAPSED
func _on_help_id_pressed(id: int) -> void:

View file

@ -2,7 +2,6 @@
[ext_resource type="Script" path="res://graph_node_renderer/deck_holder_renderer.gd" id="1_67g2g"]
[ext_resource type="PackedScene" uid="uid://b84f2ngtcm5b8" path="res://graph_node_renderer/tab_container_custom.tscn" id="1_s3ug2"]
[ext_resource type="Theme" uid="uid://dqqdqscid2iem" path="res://graph_node_renderer/default_theme.tres" id="1_tgul2"]
[ext_resource type="PackedScene" uid="uid://b18qpb48df14l" path="res://graph_node_renderer/deck_renderer_graph_edit.tscn" id="3_uf16c"]
[ext_resource type="PackedScene" uid="uid://dayri1ejk20bc" path="res://graph_node_renderer/bottom_dock.tscn" id="4_gwnhy"]
[ext_resource type="Script" path="res://addons/no-obs-ws/NoOBSWS.gd" id="4_nu72u"]
@ -10,6 +9,7 @@
[ext_resource type="Script" path="res://addons/no_twitch/twitch_connection.gd" id="5_3n36q"]
[ext_resource type="PackedScene" uid="uid://cddfyvpf5nqq7" path="res://graph_node_renderer/debug_nodes_list.tscn" id="5_pnfg8"]
[ext_resource type="PackedScene" uid="uid://eioso6jb42jy" path="res://graph_node_renderer/obs_websocket_setup_dialog.tscn" id="5_uo2gj"]
[ext_resource type="PackedScene" uid="uid://b56hjad0ih0gu" path="res://graph_node_renderer/sidebar/sidebar.tscn" id="7_0lv5b"]
[ext_resource type="PackedScene" uid="uid://bq2lxmbnic4lc" path="res://graph_node_renderer/twitch_setup_dialog.tscn" id="7_7rhap"]
[ext_resource type="PackedScene" uid="uid://cuwou2aa7qfc2" path="res://graph_node_renderer/unsaved_changes_dialog_single_deck.tscn" id="8_qf6ve"]
[ext_resource type="PackedScene" uid="uid://cvvkj138fg8jg" path="res://graph_node_renderer/unsaved_changes_dialog.tscn" id="9_4n0q6"]
@ -24,7 +24,6 @@ anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme = ExtResource("1_tgul2")
script = ExtResource("1_67g2g")
DECK_SCENE = ExtResource("3_uf16c")
DEBUG_DECKS_LIST = ExtResource("4_ux0jt")
@ -42,17 +41,24 @@ theme_override_constants/margin_top = 2
theme_override_constants/margin_right = 2
theme_override_constants/margin_bottom = 2
[node name="VSplitContainer" type="VSplitContainer" parent="MarginContainer"]
[node name="SidebarSplit" type="HSplitContainer" parent="MarginContainer"]
unique_name_in_owner = true
layout_mode = 2
theme_override_constants/separation = 1
split_offset = -270
[node name="BottomSplit" type="VSplitContainer" parent="MarginContainer/SidebarSplit"]
layout_mode = 2
size_flags_horizontal = 3
split_offset = 460
[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VSplitContainer"]
[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/SidebarSplit/BottomSplit"]
layout_mode = 2
[node name="MenuBar" type="MenuBar" parent="MarginContainer/VSplitContainer/VBoxContainer"]
[node name="MenuBar" type="MenuBar" parent="MarginContainer/SidebarSplit/BottomSplit/VBoxContainer"]
layout_mode = 2
[node name="File" type="PopupMenu" parent="MarginContainer/VSplitContainer/VBoxContainer/MenuBar"]
[node name="File" type="PopupMenu" parent="MarginContainer/SidebarSplit/BottomSplit/VBoxContainer/MenuBar"]
unique_name_in_owner = true
item_count = 8
item_0/text = "New Deck"
@ -75,7 +81,7 @@ item_7/text = "Recent Decks"
item_7/id = 7
item_7/separator = true
[node name="Edit" type="PopupMenu" parent="MarginContainer/VSplitContainer/VBoxContainer/MenuBar"]
[node name="Edit" type="PopupMenu" parent="MarginContainer/SidebarSplit/BottomSplit/VBoxContainer/MenuBar"]
unique_name_in_owner = true
item_count = 4
item_0/text = "Copy Node(s)"
@ -87,7 +93,7 @@ item_2/id = 2
item_3/text = "Settings..."
item_3/id = 3
[node name="Connections" type="PopupMenu" parent="MarginContainer/VSplitContainer/VBoxContainer/MenuBar"]
[node name="Connections" type="PopupMenu" parent="MarginContainer/SidebarSplit/BottomSplit/VBoxContainer/MenuBar"]
unique_name_in_owner = true
item_count = 3
item_0/text = "OBS..."
@ -97,7 +103,7 @@ item_1/id = 1
item_2/text = "RPC Server..."
item_2/id = 2
[node name="Debug" type="PopupMenu" parent="MarginContainer/VSplitContainer/VBoxContainer/MenuBar"]
[node name="Debug" type="PopupMenu" parent="MarginContainer/SidebarSplit/BottomSplit/VBoxContainer/MenuBar"]
unique_name_in_owner = true
item_count = 3
item_0/text = "Decks..."
@ -109,7 +115,7 @@ item_2/checkable = 1
item_2/checked = true
item_2/id = 2
[node name="Help" type="PopupMenu" parent="MarginContainer/VSplitContainer/VBoxContainer/MenuBar"]
[node name="Help" type="PopupMenu" parent="MarginContainer/SidebarSplit/BottomSplit/VBoxContainer/MenuBar"]
unique_name_in_owner = true
item_count = 2
item_0/text = "Online Documentation"
@ -117,11 +123,16 @@ item_0/id = 0
item_1/text = "About..."
item_1/id = 1
[node name="TabContainerCustom" parent="MarginContainer/VSplitContainer/VBoxContainer" instance=ExtResource("1_s3ug2")]
[node name="TabContainerCustom" parent="MarginContainer/SidebarSplit/BottomSplit/VBoxContainer" instance=ExtResource("1_s3ug2")]
unique_name_in_owner = true
layout_mode = 2
[node name="BottomDock" parent="MarginContainer/VSplitContainer" instance=ExtResource("4_gwnhy")]
[node name="BottomDock" parent="MarginContainer/SidebarSplit/BottomSplit" instance=ExtResource("4_gwnhy")]
unique_name_in_owner = true
visible = false
layout_mode = 2
[node name="Sidebar" parent="MarginContainer/SidebarSplit" instance=ExtResource("7_0lv5b")]
unique_name_in_owner = true
visible = false
layout_mode = 2
@ -159,12 +170,12 @@ unique_name_in_owner = true
[node name="SettingsDialog" parent="." instance=ExtResource("16_rktri")]
unique_name_in_owner = true
[connection signal="id_pressed" from="MarginContainer/VSplitContainer/VBoxContainer/MenuBar/File" to="." method="_on_file_id_pressed"]
[connection signal="about_to_popup" from="MarginContainer/VSplitContainer/VBoxContainer/MenuBar/Edit" to="." method="_on_edit_about_to_popup"]
[connection signal="id_pressed" from="MarginContainer/VSplitContainer/VBoxContainer/MenuBar/Edit" to="." method="_on_edit_id_pressed"]
[connection signal="id_pressed" from="MarginContainer/VSplitContainer/VBoxContainer/MenuBar/Connections" to="." method="_on_connections_id_pressed"]
[connection signal="id_pressed" from="MarginContainer/VSplitContainer/VBoxContainer/MenuBar/Debug" to="." method="_on_debug_id_pressed"]
[connection signal="id_pressed" from="MarginContainer/VSplitContainer/VBoxContainer/MenuBar/Help" to="." method="_on_help_id_pressed"]
[connection signal="id_pressed" from="MarginContainer/SidebarSplit/BottomSplit/VBoxContainer/MenuBar/File" to="." method="_on_file_id_pressed"]
[connection signal="about_to_popup" from="MarginContainer/SidebarSplit/BottomSplit/VBoxContainer/MenuBar/Edit" to="." method="_on_edit_about_to_popup"]
[connection signal="id_pressed" from="MarginContainer/SidebarSplit/BottomSplit/VBoxContainer/MenuBar/Edit" to="." method="_on_edit_id_pressed"]
[connection signal="id_pressed" from="MarginContainer/SidebarSplit/BottomSplit/VBoxContainer/MenuBar/Connections" to="." method="_on_connections_id_pressed"]
[connection signal="id_pressed" from="MarginContainer/SidebarSplit/BottomSplit/VBoxContainer/MenuBar/Debug" to="." method="_on_debug_id_pressed"]
[connection signal="id_pressed" from="MarginContainer/SidebarSplit/BottomSplit/VBoxContainer/MenuBar/Help" to="." method="_on_help_id_pressed"]
[connection signal="connect_button_pressed" from="OBSWebsocketSetupDialog" to="." method="_on_obs_websocket_setup_dialog_connect_button_pressed"]
[connection signal="confirmed" from="UnsavedChangesDialogSingleDeck" to="." method="_on_unsaved_changes_dialog_single_deck_confirmed"]
[connection signal="custom_action" from="UnsavedChangesDialogSingleDeck" to="." method="_on_unsaved_changes_dialog_single_deck_custom_action"]

View file

@ -69,6 +69,7 @@ func _ready() -> void:
connection_request.connect(attempt_connection)
disconnection_request.connect(attempt_disconnect)
## Receives [signal GraphEdit.connection_request] and attempts to create a
## connection between the two [DeckNode]s involved, utilizes [NodeDB] for accessing them.
func attempt_connection(from_node_name: StringName, from_port: int, to_node_name: StringName, to_port: int) -> void:
@ -94,6 +95,7 @@ func _is_node_hover_valid(from_node: StringName, from_port: int, to_node: String
return deck.is_valid_connection(from_node_renderer.node._id, to_node_renderer.node._id, from_port, to_port)
## Receives [signal GraphEdit.disconnection_request] and attempts to disconnect the two [DeckNode]s
## involved, utilizes [NodeDB] for accessing them.
func attempt_disconnect(from_node_name: StringName, from_port: int, to_node_name: StringName, to_port: int) -> void:
@ -113,6 +115,7 @@ func attempt_disconnect(from_node_name: StringName, from_port: int, to_node_name
)
dirty = true
## Returns the associated [DeckNodeRendererGraphNode] for the supplied [DeckNode].
## Or [code]null[/code] if none is found.
func get_node_renderer(node: DeckNode) -> DeckNodeRendererGraphNode:
@ -122,6 +125,29 @@ func get_node_renderer(node: DeckNode) -> DeckNodeRendererGraphNode:
return null
func focus_node(node: DeckNodeRendererGraphNode) -> void:
set_selected(node)
var t := create_tween()
t.tween_property(self, ^"scroll_offset", (node.position_offset + node.size / 2.0) * zoom - size / 2.0, 0.3).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
func focus_selection() -> void:
var nodes := get_selected_nodes()
if nodes.size() == 0:
return
var midpoint := Vector2()
for i in nodes:
midpoint += i.position_offset + i.size / 2.0
midpoint *= zoom
midpoint /= nodes.size()
midpoint -= size / 2.0
var t := create_tween()
t.tween_property(self, ^"scroll_offset", midpoint, 0.3).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
## Updates [member Deck]s meta property "offset" whenever [member GraphEdit.scroll_offset]
func _on_scroll_offset_changed(offset: Vector2) -> void:
deck.set_meta("offset", offset)
@ -176,6 +202,7 @@ func refresh_connections() -> void:
to_node_port
)
## Connected to [signal Deck.node_added], used to instance the required
## [DeckNodeRendererGraphNode] and set it's [member DeckNodeRenderGraphNode.position_offset]
func _on_deck_node_added(node: DeckNode) -> void:
@ -199,6 +226,7 @@ func _on_deck_node_removed(node: DeckNode) -> void:
break
dirty = true
## Utility function that gets all [DeckNodeRenderGraphNode]s that are selected
## See [member GraphNode.selected]
func get_selected_nodes() -> Array:
@ -207,6 +235,7 @@ func get_selected_nodes() -> Array:
return x.selected
)
## Executes functionality based off hotkey inputs. Specifically handles creating groups
## based off the action "group_nodes".
func _gui_input(event: InputEvent) -> void:
@ -259,6 +288,9 @@ func _gui_input(event: InputEvent) -> void:
if RendererShortcuts.check_shortcut("paste_nodes", event):
_on_paste_nodes_request()
accept_event()
if RendererShortcuts.check_shortcut("focus_nodes", event):
focus_selection()
## Handles entering groups with action "enter_group". Done here to bypass neighbor
@ -276,7 +308,7 @@ func _input(event: InputEvent) -> void:
func _on_rename_popup_rename_confirmed(new_name: String) -> void:
var node: DeckNodeRendererGraphNode = get_selected_nodes()[0]
node.title = new_name
#node.title = new_name
node.node.rename(new_name)
dirty = true

View file

@ -1,4 +1,45 @@
[gd_resource type="Theme" format=3 uid="uid://dqqdqscid2iem"]
[gd_resource type="Theme" load_steps=5 format=3 uid="uid://dqqdqscid2iem"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_dt61u"]
content_margin_left = 4.0
content_margin_top = 4.0
content_margin_right = 4.0
content_margin_bottom = 4.0
bg_color = Color(0.384314, 0.384314, 0.384314, 0.835294)
corner_radius_top_left = 4
corner_radius_top_right = 4
corner_radius_bottom_right = 4
corner_radius_bottom_left = 4
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_fmm5o"]
content_margin_left = 4.0
content_margin_top = 4.0
content_margin_right = 4.0
content_margin_bottom = 4.0
bg_color = Color(0.247059, 0.247059, 0.247059, 0.835294)
corner_radius_top_left = 4
corner_radius_top_right = 4
corner_radius_bottom_right = 4
corner_radius_bottom_left = 4
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_5q5t0"]
content_margin_left = 4.0
content_margin_top = 4.0
content_margin_right = 4.0
content_margin_bottom = 4.0
bg_color = Color(0.247059, 0.247059, 0.247059, 0.835294)
corner_radius_top_left = 4
corner_radius_top_right = 4
corner_radius_bottom_right = 4
corner_radius_bottom_left = 4
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_q56mu"]
bg_color = Color(0.623529, 0.564706, 0.909804, 0.0588235)
[resource]
default_font_size = 14
AccordionButton/base_type = &"Button"
AccordionButton/styles/hover = SubResource("StyleBoxFlat_dt61u")
AccordionButton/styles/normal = SubResource("StyleBoxFlat_fmm5o")
AccordionButton/styles/pressed = SubResource("StyleBoxFlat_5q5t0")
AccordionMenu/styles/background = SubResource("StyleBoxFlat_q56mu")

View file

@ -1,4 +1,3 @@
# meta-description: An empty template with StreamGraph license header.
# (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)

View file

@ -14,3 +14,10 @@ func _setup(port: Port, _node: DeckNode) -> void:
check_box.text = port.label
port.value_callback = check_box.is_pressed
check_box.toggled.connect(port.set_value)
func set_value(new_value: Variant) -> void:
if check_box.has_focus():
return
else:
check_box.button_pressed = bool(new_value)

View file

@ -14,3 +14,10 @@ func _setup(port: Port, _node: DeckNode) -> void:
code_edit.text_changed.connect(port.set_value.bind(code_edit.get_text))
code_edit.custom_minimum_size = Vector2(200, 100)
code_edit.size_flags_vertical = SIZE_EXPAND_FILL
func set_value(new_value: Variant) -> void:
if code_edit.has_focus():
return
code_edit.text = new_value

View file

@ -1,4 +1,3 @@
# meta-description: An empty template with StreamGraph license header.
# (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)
@ -7,6 +6,8 @@ class_name DescriptorContainer
@onready var type_icon: TextureRect = %TypeIcon
var descriptor: Array
var ignore_signal: bool = false
const TYPE_ICONS := {
DeckType.Types.BOOL: preload("res://graph_node_renderer/textures/type_bool.svg"),
@ -18,20 +19,31 @@ const TYPE_ICONS := {
@warning_ignore("unused_parameter")
func set_up_from_port(port: Port, node: DeckNode) -> void:
func set_up_from_port(port: Port, node: DeckNode, in_node: bool = true) -> void:
descriptor = port.descriptor.split(":")
Util.safe_connect(port.value_updated,
func(new_value: Variant):
if ignore_signal:
return
set_value(new_value)
)
_setup(port, node)
if port.port_type == DeckNode.PortType.VIRTUAL or port.type == DeckType.Types.ANY:
if (port.port_type == DeckNode.PortType.VIRTUAL or port.type == DeckType.Types.ANY) or not in_node:
type_icon.visible = false
return
type_icon.modulate = DeckNodeRendererGraphNode.TYPE_COLORS[port.type]
type_icon.tooltip_text = "Type: %s" % DeckType.type_str(port.type).to_lower()
type_icon.texture = TYPE_ICONS[port.type]
if port.port_type == DeckNode.PortType.OUTPUT:
if port.port_type == DeckNode.PortType.OUTPUT and in_node:
move_child(type_icon, -1)
@warning_ignore("unused_parameter")
func _setup(port: Port, node: DeckNode) -> void:
pass
@warning_ignore("unused_parameter")
func set_value(new_value: Variant) -> void:
pass

View file

@ -12,3 +12,10 @@ func _setup(port: Port, _node: DeckNode) -> void:
line_edit.placeholder_text = port.label
port.value_callback = line_edit.get_text
line_edit.text_changed.connect(port.set_value)
func set_value(new_value: Variant) -> void:
if line_edit.has_focus():
return
line_edit.text = new_value

View file

@ -27,3 +27,13 @@ func _setup(port: Port, _node: DeckNode) -> void:
if box.get_item_text(i) == port.value:
box.select(i)
break
func set_value(new_value: Variant) -> void:
if box.has_focus():
return
for i in box.item_count:
if box.get_item_text(i) == new_value:
box.select(i)
break

View file

@ -26,3 +26,10 @@ func _setup(port: Port, _node: DeckNode) -> void:
spin_box.step = float(descriptor[4])
port.value_callback = spin_box.get_value
spin_box.value_changed.connect(port.set_value)
func set_value(new_value: Variant) -> void:
if spin_box.has_focus():
return
spin_box.set_value_no_signal(new_value)

View file

@ -52,6 +52,7 @@ static var defaults = {
"group_nodes": create_shortcut_from_dict({"ctrl": true, "key": "g"}),
"enter_group": create_shortcut_from_dict({"key": "tab"}),
"rename_node": create_shortcut_from_dict({"key": "f2"}),
"focus_nodes": create_shortcut_from_dict({"key": "f"}),
"_sep_nodes": "nodes",
"copy_nodes": create_shortcut_from_dict({"ctrl": true, "key": "c"}),
@ -62,6 +63,7 @@ static var defaults = {
"_sep_misc": "misc",
"settings": create_shortcut_from_dict({"ctrl": true, "key": "comma"}),
"toggle_bottom_dock": create_shortcut_from_dict({"key": "n"}),
"toggle_sidebar": create_shortcut_from_dict({"key": "t"}),
}

View file

@ -0,0 +1,152 @@
# (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)
@tool
extends VBoxContainer
class_name AccordionMenu
## A collapsible menu container.
##
## Provides a container that arranges its children vertically with a button to collapse
## and uncollapse its children. The name of the node controls the collapse button's text.
## Controls whether children are visible.
@export var collapsed: bool = false:
set = set_collapsed
## If [code]true[/code], the menu will draw a background under its children,
## which can be helpful for telling nested elements apart.[br]
## The stylebox for drawing this background can be defined in the [Theme],
## which is called [code]background[/code].
@export var draw_background: bool = false:
set(v):
draw_background = v
queue_redraw()
## If [code]true[/code], the menu will draw lines pointing to child [Control]s,
## similar to [Tree].
@export var draw_tree: bool = true:
set(v):
draw_tree = v
queue_redraw()
var _collapse_button: CollapseButton
const COLLAPSE_ICON = preload("res://graph_node_renderer/textures/collapse-icon.svg")
const COLLAPSE_ICON_COLLAPSED = preload("res://graph_node_renderer/textures/collapse-icon-collapsed.svg")
const BASE_MARGIN := 12
var _indent_level := 1
func _enter_tree() -> void:
if get_child_count(true) > 0 and get_child(0, true) is CollapseButton:
get_child(0, true).queue_free()
_collapse_button = CollapseButton.new(collapsed, name)
_collapse_button.toggled.connect(set_collapsed)
renamed.connect(
func():
_collapse_button.text = name
)
add_child(_collapse_button, false, Node.INTERNAL_MODE_FRONT)
func _exit_tree() -> void:
_collapse_button.queue_free()
func set_collapsed(v: bool) -> void:
collapsed = v
if _collapse_button:
_collapse_button.button_pressed = collapsed
if collapsed:
await collapse()
else:
await uncollapse()
queue_redraw()
## Collapse the menu, making the children invisible.
func collapse() -> void:
if _collapse_button:
_collapse_button.icon = COLLAPSE_ICON_COLLAPSED
for child in get_children(false):
child.visible = false
if is_inside_tree():
await get_tree().process_frame
update_minimum_size()
## Uncollapse the menu, making the children visible.
func uncollapse() -> void:
if _collapse_button:
_collapse_button.icon = COLLAPSE_ICON
for child in get_children(false):
child.visible = true
if is_inside_tree():
await get_tree().process_frame
update_minimum_size()
func _notification(what: int) -> void:
if what == NOTIFICATION_PRE_SORT_CHILDREN:
for i in get_children(false):
i.visible = not collapsed
if what == NOTIFICATION_SORT_CHILDREN:
for child: Control in get_children(false):
var base_rect := child.get_rect()
base_rect.size.x -= BASE_MARGIN * _indent_level
base_rect.position.x += BASE_MARGIN * _indent_level
fit_child_in_rect(child, base_rect)
func _draw() -> void:
if draw_tree and not collapsed and get_child_count() > 0:
# draw a tree-like line.
var line_color := Color(1.0, 1.0, 1.0, 0.35)
# first, draw a single vertical line
var line_start_x := _collapse_button.position.x + BASE_MARGIN / 2.0
var tree_line_start := Vector2(line_start_x, _collapse_button.position.y + _collapse_button.size.y)
var tree_line_end := tree_line_start
#tree_line_end.y = get_rect().size.y
tree_line_end.y = get_combined_minimum_size().y
tree_line_end.y -= get_child(-1, false).size.y / 2.0
if get_child(-1, false) is AccordionMenu:
var other: AccordionMenu = get_child(-1, false) as AccordionMenu
tree_line_end.y -= other.size.y / 2
tree_line_end.y += other._collapse_button.size.y / 2.0
draw_line(tree_line_start, tree_line_end, line_color)
# draw lines going out to each child
for child in get_children(false):
# special case for if the child is also an accordion:
# draw the line pointing to the header
if child is AccordionMenu:
var start := Vector2(line_start_x, child.position.y + child._collapse_button.size.y / 2)
var end := start + Vector2(BASE_MARGIN / 2.0, 0.0)
draw_line(start, end, line_color)
else:
var start := Vector2(line_start_x, child.position.y + child.size.y / 2)
var end := start + Vector2(BASE_MARGIN / 2.0, 0.0)
draw_line(start, end, line_color)
if draw_background:
var sb := get_theme_stylebox(&"background", &"AccordionMenu")
var button_height := _collapse_button.get_rect().size.y
var rect := Rect2(_collapse_button.position, size)
rect.size.y -= button_height
rect.position.y += button_height
draw_style_box(sb, rect)
class CollapseButton extends Button:
func _init(p_collapsed: bool = false, p_text: String = "") -> void:
text = p_text
icon = COLLAPSE_ICON_COLLAPSED if p_collapsed else COLLAPSE_ICON
toggle_mode = true
button_pressed = p_collapsed
alignment = HORIZONTAL_ALIGNMENT_LEFT
theme_type_variation = &"AccordionButton"

View file

@ -0,0 +1,268 @@
# (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
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()
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_inspector = null
return
await get_tree().process_frame
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).node_removed.connect(
func(node: DeckNode):
if node._id == edited_node_id:
set_edited_node()
)
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]
func add_inspector(text: String, control: Control = null) -> void:
nodes.append(Inspector.new(text, control))
func _init(id: String) -> void:
ref = weakref(DeckHolder.get_deck(id))
var deck: Deck = ref.get_ref() as Deck
if deck.is_group:
add_inspector("This deck is a group.")
class NodeInspector:
var ref: WeakRef
var nodes: Array[Control]
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 create_label(text: String) -> Label:
var l := Label.new()
l.text = text
l.size_flags_horizontal = Control.SIZE_EXPAND_FILL
return l
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 := 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)
var name_field := LineEdit.new()
name_field.placeholder_text = "Node name"
name_field.text = node.name
name_field.text_changed.connect(node.rename)
node.renamed.connect(
func(new_name: String) -> void:
if name_field.has_focus():
return
name_field.text = new_name
)
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_name.call_deferred("Output Ports" if is_output else "Input Ports")
for port in ports:
var acc := AccordionMenu.new()
acc.draw_background = true
acc.name = "Port %s" % port.index_of_type
var label_label := create_label("Name: %s" % port.label)
acc.add_child(label_label)
var type_label := 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 := 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 := 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)

View file

@ -0,0 +1,44 @@
[gd_scene load_steps=3 format=3 uid="uid://b56hjad0ih0gu"]
[ext_resource type="Script" path="res://graph_node_renderer/sidebar/sidebar.gd" id="1_bcym7"]
[ext_resource type="Script" path="res://graph_node_renderer/sidebar/accordion_menu.gd" id="1_q1gqb"]
[node name="Sidebar" type="PanelContainer"]
offset_right = 439.0
offset_bottom = 266.0
script = ExtResource("1_bcym7")
[node name="MarginContainer" type="MarginContainer" parent="."]
layout_mode = 2
theme_override_constants/margin_left = 4
theme_override_constants/margin_top = 4
theme_override_constants/margin_right = 4
theme_override_constants/margin_bottom = 4
[node name="ScrollContainer" type="ScrollContainer" parent="MarginContainer"]
layout_mode = 2
horizontal_scroll_mode = 0
[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/ScrollContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Label" type="Label" parent="MarginContainer/ScrollContainer/VBoxContainer"]
layout_mode = 2
text = "Inspector"
[node name="Deck" type="VBoxContainer" parent="MarginContainer/ScrollContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
script = ExtResource("1_q1gqb")
[node name="Node" type="VBoxContainer" parent="MarginContainer/ScrollContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
script = ExtResource("1_q1gqb")
[node name="Node List" type="VBoxContainer" parent="MarginContainer/ScrollContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
script = ExtResource("1_q1gqb")

View file

@ -0,0 +1,4 @@
[gd_resource type="SystemFont" format=3 uid="uid://borqsrevyoygv"]
[resource]
font_names = PackedStringArray("Monospace")

View file

@ -23,6 +23,10 @@ config/icon="res://dist/logo-flattened.svg"
enabled=PackedStringArray("res://addons/no-obs-ws/plugin.cfg", "res://addons/no_twitch/plugin.cfg")
[gui]
theme/custom="res://graph_node_renderer/default_theme.tres"
[input]
debug_make_unique={