From 1ce3cd0367ab1b84529cdcc2b978d9393e400a6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lera=20Elvo=C3=A9?= Date: Tue, 16 Apr 2024 15:16:38 +0000 Subject: [PATCH] add a sidebar (#146) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewed-on: https://codeberg.org/StreamGraph/StreamGraph/pulls/146 Co-authored-by: Lera Elvoé Co-committed-by: Lera Elvoé --- classes/util.gd | 52 +++- graph_node_renderer/deck_holder_renderer.gd | 52 +++- graph_node_renderer/deck_holder_renderer.tscn | 47 +-- .../deck_renderer_graph_edit.gd | 34 ++- graph_node_renderer/default_theme.tres | 43 ++- .../descriptors/button_descriptor.gd | 1 - .../descriptors/check_box_descriptor.gd | 7 + .../descriptors/codeblock_descriptor.gd | 7 + .../descriptors/descriptor_container.gd | 20 +- .../descriptors/field_descriptor.gd | 7 + .../descriptors/single_choice_descriptor.gd | 10 + .../descriptors/spin_box_descriptor.gd | 7 + .../shortcuts/renderer_shortcuts.gd | 2 + graph_node_renderer/sidebar/accordion_menu.gd | 152 ++++++++++ graph_node_renderer/sidebar/sidebar.gd | 268 ++++++++++++++++++ graph_node_renderer/sidebar/sidebar.tscn | 44 +++ graph_node_renderer/system_code_font.tres | 4 + project.godot | 4 + 18 files changed, 733 insertions(+), 28 deletions(-) create mode 100644 graph_node_renderer/sidebar/accordion_menu.gd create mode 100644 graph_node_renderer/sidebar/sidebar.gd create mode 100644 graph_node_renderer/sidebar/sidebar.tscn create mode 100644 graph_node_renderer/system_code_font.tres diff --git a/classes/util.gd b/classes/util.gd index ce24d45..fb44b45 100644 --- a/classes/util.gd +++ b/classes/util.gd @@ -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) diff --git a/graph_node_renderer/deck_holder_renderer.gd b/graph_node_renderer/deck_holder_renderer.gd index 51172eb..32190be 100644 --- a/graph_node_renderer/deck_holder_renderer.gd +++ b/graph_node_renderer/deck_holder_renderer.gd @@ -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: diff --git a/graph_node_renderer/deck_holder_renderer.tscn b/graph_node_renderer/deck_holder_renderer.tscn index cf8d497..dd77f21 100644 --- a/graph_node_renderer/deck_holder_renderer.tscn +++ b/graph_node_renderer/deck_holder_renderer.tscn @@ -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"] diff --git a/graph_node_renderer/deck_renderer_graph_edit.gd b/graph_node_renderer/deck_renderer_graph_edit.gd index 336fba6..1e47196 100644 --- a/graph_node_renderer/deck_renderer_graph_edit.gd +++ b/graph_node_renderer/deck_renderer_graph_edit.gd @@ -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 diff --git a/graph_node_renderer/default_theme.tres b/graph_node_renderer/default_theme.tres index 3425c99..55509cb 100644 --- a/graph_node_renderer/default_theme.tres +++ b/graph_node_renderer/default_theme.tres @@ -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") diff --git a/graph_node_renderer/descriptors/button_descriptor.gd b/graph_node_renderer/descriptors/button_descriptor.gd index dead1bd..c67d7d2 100644 --- a/graph_node_renderer/descriptors/button_descriptor.gd +++ b/graph_node_renderer/descriptors/button_descriptor.gd @@ -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) diff --git a/graph_node_renderer/descriptors/check_box_descriptor.gd b/graph_node_renderer/descriptors/check_box_descriptor.gd index 0e64e76..ae20862 100644 --- a/graph_node_renderer/descriptors/check_box_descriptor.gd +++ b/graph_node_renderer/descriptors/check_box_descriptor.gd @@ -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) diff --git a/graph_node_renderer/descriptors/codeblock_descriptor.gd b/graph_node_renderer/descriptors/codeblock_descriptor.gd index 7d81747..4983432 100644 --- a/graph_node_renderer/descriptors/codeblock_descriptor.gd +++ b/graph_node_renderer/descriptors/codeblock_descriptor.gd @@ -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 diff --git a/graph_node_renderer/descriptors/descriptor_container.gd b/graph_node_renderer/descriptors/descriptor_container.gd index 60a2910..8bba55d 100644 --- a/graph_node_renderer/descriptors/descriptor_container.gd +++ b/graph_node_renderer/descriptors/descriptor_container.gd @@ -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 diff --git a/graph_node_renderer/descriptors/field_descriptor.gd b/graph_node_renderer/descriptors/field_descriptor.gd index 44ad79a..9b078ab 100644 --- a/graph_node_renderer/descriptors/field_descriptor.gd +++ b/graph_node_renderer/descriptors/field_descriptor.gd @@ -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 diff --git a/graph_node_renderer/descriptors/single_choice_descriptor.gd b/graph_node_renderer/descriptors/single_choice_descriptor.gd index 7dc4c71..82a0e36 100644 --- a/graph_node_renderer/descriptors/single_choice_descriptor.gd +++ b/graph_node_renderer/descriptors/single_choice_descriptor.gd @@ -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 diff --git a/graph_node_renderer/descriptors/spin_box_descriptor.gd b/graph_node_renderer/descriptors/spin_box_descriptor.gd index f11f91a..0c748be 100644 --- a/graph_node_renderer/descriptors/spin_box_descriptor.gd +++ b/graph_node_renderer/descriptors/spin_box_descriptor.gd @@ -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) diff --git a/graph_node_renderer/shortcuts/renderer_shortcuts.gd b/graph_node_renderer/shortcuts/renderer_shortcuts.gd index 03dc3af..da605b9 100644 --- a/graph_node_renderer/shortcuts/renderer_shortcuts.gd +++ b/graph_node_renderer/shortcuts/renderer_shortcuts.gd @@ -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"}), } diff --git a/graph_node_renderer/sidebar/accordion_menu.gd b/graph_node_renderer/sidebar/accordion_menu.gd new file mode 100644 index 0000000..10f94c7 --- /dev/null +++ b/graph_node_renderer/sidebar/accordion_menu.gd @@ -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" diff --git a/graph_node_renderer/sidebar/sidebar.gd b/graph_node_renderer/sidebar/sidebar.gd new file mode 100644 index 0000000..7c95382 --- /dev/null +++ b/graph_node_renderer/sidebar/sidebar.gd @@ -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) diff --git a/graph_node_renderer/sidebar/sidebar.tscn b/graph_node_renderer/sidebar/sidebar.tscn new file mode 100644 index 0000000..7d2ca47 --- /dev/null +++ b/graph_node_renderer/sidebar/sidebar.tscn @@ -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") diff --git a/graph_node_renderer/system_code_font.tres b/graph_node_renderer/system_code_font.tres new file mode 100644 index 0000000..663c97d --- /dev/null +++ b/graph_node_renderer/system_code_font.tres @@ -0,0 +1,4 @@ +[gd_resource type="SystemFont" format=3 uid="uid://borqsrevyoygv"] + +[resource] +font_names = PackedStringArray("Monospace") diff --git a/project.godot b/project.godot index b6b6095..d994b58 100644 --- a/project.godot +++ b/project.godot @@ -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={