From b143c63dcc0fa165d415c3d8a6e212b4cb5516b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lera=20Elvo=C3=A9?= Date: Fri, 19 Jan 2024 09:52:51 +0000 Subject: [PATCH] add a bottom dock (#44) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - moves the console to the new bottom dock - adds a deck variables inspector to the bottom dock that allows adding, removing and editing variables of the currently open deck - bottom dock can be opened with N closes #33 Reviewed-on: https://codeberg.org/Eroax/StreamGraph/pulls/44 Co-authored-by: Lera ElvoƩ Co-committed-by: Lera ElvoƩ --- classes/deck/deck.gd | 17 ++ classes/deck/nodes/delay.gd | 2 +- classes/deck/nodes/set_deck_var.gd | 3 +- classes/types/deck_type.gd | 8 + graph_node_renderer/bottom_dock.gd | 17 ++ graph_node_renderer/bottom_dock.tscn | 19 ++ graph_node_renderer/deck_holder_renderer.gd | 42 ++- graph_node_renderer/deck_holder_renderer.tscn | 4 +- graph_node_renderer/logger_renderer.gd | 2 +- graph_node_renderer/logger_renderer.tscn | 36 +-- graph_node_renderer/tab_container_custom.gd | 4 + graph_node_renderer/textures/add_icon.svg | 1 + .../textures/add_icon.svg.import | 37 +++ graph_node_renderer/textures/remove-icon.svg | 1 + .../textures/remove-icon.svg.import | 37 +++ graph_node_renderer/variable_viewer.gd | 280 ++++++++++++++++++ graph_node_renderer/variable_viewer.tscn | 37 +++ project.godot | 2 +- .../Object/empty_template_with_license.gd | 5 + 19 files changed, 519 insertions(+), 35 deletions(-) create mode 100644 graph_node_renderer/bottom_dock.gd create mode 100644 graph_node_renderer/bottom_dock.tscn create mode 100644 graph_node_renderer/textures/add_icon.svg create mode 100644 graph_node_renderer/textures/add_icon.svg.import create mode 100644 graph_node_renderer/textures/remove-icon.svg create mode 100644 graph_node_renderer/textures/remove-icon.svg.import create mode 100644 graph_node_renderer/variable_viewer.gd create mode 100644 graph_node_renderer/variable_viewer.tscn create mode 100644 script_templates/Object/empty_template_with_license.gd diff --git a/classes/deck/deck.gd b/classes/deck/deck.gd index 94b9e02..963b9ba 100644 --- a/classes/deck/deck.gd +++ b/classes/deck/deck.gd @@ -44,6 +44,8 @@ signal node_added(node: DeckNode) signal node_removed(node: DeckNode) ## Emitted when nodes have been disconnected signal nodes_disconnected(from_node_id: String, to_node_id: String, from_output_port: int, to_input_port: int) +## Emitted when the [member variable_stack] has been modified. +signal variables_updated() #region group signals signal node_added_to_group(node: DeckNode, assign_id: String, assign_to_self: bool, deck: Deck) @@ -196,6 +198,21 @@ func remove_node(uuid: String, remove_connections: bool = false, force: bool = f node_removed_from_group.emit(uuid, remove_connections, self) +## Set a variable on this deck. +func set_variable(var_name: String, value: Variant) -> void: + variable_stack[var_name] = value + variables_updated.emit() + + +func update_variable(old_name: String, new_name: String, new_value: Variant) -> void: + variable_stack.erase(old_name) + variable_stack[new_name] = new_value + + +func remove_variable(name: String) -> void: + variable_stack.erase(name) + + ## Group the [param nodes_to_group] into a new deck and return it. ## Returns [code]null[/code] on failure.[br] ## Adds a group node to this deck, and adds group input and output nodes in the group. diff --git a/classes/deck/nodes/delay.gd b/classes/deck/nodes/delay.gd index 865c756..de4dbe1 100644 --- a/classes/deck/nodes/delay.gd +++ b/classes/deck/nodes/delay.gd @@ -34,7 +34,7 @@ func handle_delay(data): #print("Delay over") - send(0, data) + send.call_deferred(0, data) func _notification(what): diff --git a/classes/deck/nodes/set_deck_var.gd b/classes/deck/nodes/set_deck_var.gd index 40f383f..3bd5812 100644 --- a/classes/deck/nodes/set_deck_var.gd +++ b/classes/deck/nodes/set_deck_var.gd @@ -42,7 +42,8 @@ func _receive(to_input_port: int, _data: Variant, _extra_data: Array = []) -> vo var var_name: String = resolve_input_port_value(0) var var_value: Variant = resolve_input_port_value(1) - _belonging_to.variable_stack[var_name] = var_value + #_belonging_to.variable_stack[var_name] = var_value + _belonging_to.set_variable(var_name, var_value) send(0, var_value) diff --git a/classes/types/deck_type.gd b/classes/types/deck_type.gd index a26523e..adb91a7 100644 --- a/classes/types/deck_type.gd +++ b/classes/types/deck_type.gd @@ -29,6 +29,14 @@ const GODOT_TYPES_MAP := { Types.DICTIONARY: TYPE_DICTIONARY, } +const INVERSE_GODOT_TYPES_MAP := { + TYPE_BOOL: Types.BOOL, + TYPE_FLOAT: Types.NUMERIC, + TYPE_STRING: Types.STRING, + TYPE_ARRAY: Types.ARRAY, + TYPE_DICTIONARY: Types.DICTIONARY, +} + static func can_convert(from: Types, to: Types) -> bool: if from == to: diff --git a/graph_node_renderer/bottom_dock.gd b/graph_node_renderer/bottom_dock.gd new file mode 100644 index 0000000..3632bd5 --- /dev/null +++ b/graph_node_renderer/bottom_dock.gd @@ -0,0 +1,17 @@ +# (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 TabContainer +class_name BottomDock + +@export var tab_names := ["Console", "Variables"] +@onready var variable_viewer: VariableViewer = %VariableViewer + + +func _ready() -> void: + for i in get_child_count(): + set_tab_title(i, tab_names[i]) + + +func rebuild_variable_tree(data: Dictionary) -> void: + variable_viewer.rebuild_variable_tree(data) diff --git a/graph_node_renderer/bottom_dock.tscn b/graph_node_renderer/bottom_dock.tscn new file mode 100644 index 0000000..6834156 --- /dev/null +++ b/graph_node_renderer/bottom_dock.tscn @@ -0,0 +1,19 @@ +[gd_scene load_steps=4 format=3 uid="uid://dayri1ejk20bc"] + +[ext_resource type="PackedScene" uid="uid://duvh3r740w2p5" path="res://graph_node_renderer/logger_renderer.tscn" id="1_6yuco"] +[ext_resource type="Script" path="res://graph_node_renderer/bottom_dock.gd" id="1_mu1hf"] +[ext_resource type="PackedScene" uid="uid://ovf5nt5pt0oj" path="res://graph_node_renderer/variable_viewer.tscn" id="3_vlkh8"] + +[node name="BottomDock" type="TabContainer"] +offset_right = 122.0 +offset_bottom = 95.0 +script = ExtResource("1_mu1hf") + +[node name="LoggerRenderer" parent="." instance=ExtResource("1_6yuco")] +unique_name_in_owner = true +layout_mode = 2 + +[node name="VariableViewer" parent="." instance=ExtResource("3_vlkh8")] +unique_name_in_owner = true +visible = false +layout_mode = 2 diff --git a/graph_node_renderer/deck_holder_renderer.gd b/graph_node_renderer/deck_holder_renderer.gd index cfe36d8..b066270 100644 --- a/graph_node_renderer/deck_holder_renderer.gd +++ b/graph_node_renderer/deck_holder_renderer.gd @@ -65,13 +65,14 @@ var _deck_to_save: WeakRef @onready var obs_setup_dialog := $OBSWebsocketSetupDialog as OBSWebsocketSetupDialog @onready var twitch_setup_dialog := $Twitch_Setup_Dialog as TwitchSetupDialog -@onready var logger_renderer: LoggerRenderer = %LoggerRenderer +@onready var bottom_dock: BottomDock = %BottomDock func _ready() -> void: get_tree().auto_accept_quit = false tab_container.add_button_pressed.connect(add_empty_deck) tab_container.tab_changed.connect(_on_tab_container_tab_changed) + tab_container.tab_about_to_change.connect(_on_tab_container_tab_about_to_change) RendererPersistence.init_namespace(PERSISTENCE_NAMESPACE) var embed_subwindows: bool = RendererPersistence.get_or_create(PERSISTENCE_NAMESPACE, "config", "embed_subwindows", true) @@ -113,12 +114,31 @@ func _ready() -> void: file_popup_menu.set_item_shortcut(FileMenuId.SAVE, save_deck_shortcut) file_popup_menu.set_item_shortcut(FileMenuId.SAVE_AS, save_deck_as_shortcut) file_popup_menu.set_item_shortcut(FileMenuId.CLOSE, close_deck_shortcut) + + bottom_dock.variable_viewer.top_field_edited.connect( + func(old_name: String, new_name: String, new_value: Variant) -> void: + get_active_deck().update_variable(old_name, new_name, new_value) + ) + + bottom_dock.variable_viewer.top_field_removed.connect( + func(field_name: String) -> void: + get_active_deck().remove_variable(field_name) + ) + + +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) func _on_tab_container_tab_changed(tab: int) -> void: var is_group = tab_container.get_tab_metadata(tab, "group", false) file_popup_menu.set_item_disabled(FileMenuId.SAVE, is_group) file_popup_menu.set_item_disabled(FileMenuId.SAVE_AS, is_group) + 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)) ## Called when the File button in the [MenuBar] is pressed with the [param id] @@ -150,6 +170,7 @@ func add_empty_deck() -> void: inst.group_enter_requested.connect(_on_deck_renderer_group_enter_requested) inst.dirty_state_changed.connect(_on_deck_renderer_dirty_state_changed.bind(inst)) tab_container.set_current_tab(tab) + bottom_dock.variable_viewer.enable_new_button() ## Closes the current tab in [member tab_container] func close_current_tab() -> void: @@ -165,8 +186,10 @@ func close_tab(tab: int) -> void: if tab_container.get_tab_metadata(c_tab, "id") == group: tab_container.close_tab(c_tab) await get_tree().process_frame - + tab_container.close_tab(tab) + if tab_container.get_tab_count() == 0: + bottom_dock.variable_viewer.disable_new_button() ## Opens [member file_dialog] with the mode [member FileDialog.FILE_MODE_SAVE_FILE] ## as well as getting a weakref to the active [Deck] @@ -234,14 +257,21 @@ func open_deck_at_path(path: String) -> void: recent_path = path.get_base_dir() RendererPersistence.set_value(PERSISTENCE_NAMESPACE, "config", "recent_path", recent_path) tab_container.set_current_tab(tab) + bottom_dock.variable_viewer.enable_new_button() -## Gets the currently active [Deck] from [member tab_container] +## Returns the current deck in the [member tab_container]. func get_active_deck() -> Deck: + return get_deck_at_tab(tab_container.get_current_tab()) + + +## Returns the deck at [param tab] in the [member tab_container]. +func get_deck_at_tab(tab: int) -> Deck: if tab_container.is_empty(): return null + + return (tab_container.get_content(tab) as DeckRendererGraphEdit).deck - return (tab_container.get_content(tab_container.get_current_tab()) as DeckRendererGraphEdit).deck ## Saves the active [Deck] in [member tab_container] func save_active_deck() -> void: @@ -445,8 +475,8 @@ func _on_unsaved_changes_dialog_confirmed() -> void: func _unhandled_input(event: InputEvent) -> void: - if event.is_action_pressed("toggle_console"): - logger_renderer.visible = !logger_renderer.visible + if event.is_action_pressed("toggle_bottom_dock"): + bottom_dock.visible = !bottom_dock.visible accept_event() diff --git a/graph_node_renderer/deck_holder_renderer.tscn b/graph_node_renderer/deck_holder_renderer.tscn index d2b2864..ec9988b 100644 --- a/graph_node_renderer/deck_holder_renderer.tscn +++ b/graph_node_renderer/deck_holder_renderer.tscn @@ -3,8 +3,8 @@ [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://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"] -[ext_resource type="PackedScene" uid="uid://duvh3r740w2p5" path="res://graph_node_renderer/logger_renderer.tscn" id="4_pvexk"] [ext_resource type="Script" path="res://addons/no_twitch/twitch_connection.gd" id="5_3n36q"] [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://bq2lxmbnic4lc" path="res://graph_node_renderer/twitch_setup_dialog.tscn" id="7_7rhap"] @@ -151,7 +151,7 @@ item_1/id = 1 unique_name_in_owner = true layout_mode = 2 -[node name="LoggerRenderer" parent="MarginContainer/VSplitContainer" instance=ExtResource("4_pvexk")] +[node name="BottomDock" parent="MarginContainer/VSplitContainer" instance=ExtResource("4_gwnhy")] unique_name_in_owner = true visible = false layout_mode = 2 diff --git a/graph_node_renderer/logger_renderer.gd b/graph_node_renderer/logger_renderer.gd index d17c8ab..5b2477a 100644 --- a/graph_node_renderer/logger_renderer.gd +++ b/graph_node_renderer/logger_renderer.gd @@ -1,7 +1,7 @@ # (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 +extends VBoxContainer class_name LoggerRenderer @onready var output_label: RichTextLabel = %OutputLabel diff --git a/graph_node_renderer/logger_renderer.tscn b/graph_node_renderer/logger_renderer.tscn index e2c2c6e..a46a0a5 100644 --- a/graph_node_renderer/logger_renderer.tscn +++ b/graph_node_renderer/logger_renderer.tscn @@ -9,45 +9,35 @@ font_names = PackedStringArray("Monospace") font_names = PackedStringArray("Monospace") font_weight = 700 -[node name="LoggerRenderer" type="PanelContainer"] +[node name="LoggerRenderer" type="VBoxContainer"] anchors_preset = -1 -anchor_right = 0.42016 -anchor_bottom = 0.181704 -offset_right = -0.0240021 -offset_bottom = 0.255997 +anchor_right = 0.186632 +anchor_bottom = 0.169753 +grow_horizontal = 2 +grow_vertical = 2 script = ExtResource("1_82rlk") metadata/_edit_use_anchors_ = true -[node name="MarginContainer" type="MarginContainer" parent="."] -layout_mode = 2 -theme_override_constants/margin_left = 3 -theme_override_constants/margin_top = 3 -theme_override_constants/margin_right = 3 -theme_override_constants/margin_bottom = 3 - -[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"] -layout_mode = 2 - -[node name="Label" type="Label" parent="MarginContainer/VBoxContainer"] +[node name="Label" type="Label" parent="."] layout_mode = 2 text = "Debug Console" -[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer"] +[node name="HBoxContainer" type="HBoxContainer" parent="."] layout_mode = 2 size_flags_vertical = 3 -[node name="PanelContainer" type="PanelContainer" parent="MarginContainer/VBoxContainer/HBoxContainer"] +[node name="PanelContainer" type="PanelContainer" parent="HBoxContainer"] layout_mode = 2 size_flags_horizontal = 3 -[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/HBoxContainer/PanelContainer"] +[node name="MarginContainer" type="MarginContainer" parent="HBoxContainer/PanelContainer"] layout_mode = 2 theme_override_constants/margin_left = 3 theme_override_constants/margin_top = 3 theme_override_constants/margin_right = 3 theme_override_constants/margin_bottom = 3 -[node name="OutputLabel" type="RichTextLabel" parent="MarginContainer/VBoxContainer/HBoxContainer/PanelContainer/MarginContainer"] +[node name="OutputLabel" type="RichTextLabel" parent="HBoxContainer/PanelContainer/MarginContainer"] unique_name_in_owner = true layout_mode = 2 size_flags_horizontal = 3 @@ -63,17 +53,17 @@ selection_enabled = true deselect_on_focus_loss_enabled = false drag_and_drop_selection_enabled = false -[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/HBoxContainer"] +[node name="VBoxContainer" type="VBoxContainer" parent="HBoxContainer"] layout_mode = 2 alignment = 1 -[node name="CopyButton" type="Button" parent="MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer"] +[node name="CopyButton" type="Button" parent="HBoxContainer/VBoxContainer"] unique_name_in_owner = true visible = false layout_mode = 2 text = "Copy" -[node name="ClearButton" type="Button" parent="MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer"] +[node name="ClearButton" type="Button" parent="HBoxContainer/VBoxContainer"] unique_name_in_owner = true layout_mode = 2 text = "Clear" diff --git a/graph_node_renderer/tab_container_custom.gd b/graph_node_renderer/tab_container_custom.gd index 1a331c4..1fbfcf0 100644 --- a/graph_node_renderer/tab_container_custom.gd +++ b/graph_node_renderer/tab_container_custom.gd @@ -21,6 +21,8 @@ class_name TabContainerCustom signal add_button_pressed ## Emitted when the current tab in [member tab_bar] is changed. signal tab_changed(tab: int) +## Emitted just before the current tab in [member tab_bar] is changed.s +signal tab_about_to_change(previous_tab: int) ## Emitted when a tab in [member tab_bar] has been closed. ## See [signal TabBar.tab_close_requested] signal tab_closed(tab: int) @@ -41,6 +43,8 @@ func _ready() -> void: if _previous_active_tab == tab: return + tab_about_to_change.emit(_previous_active_tab) + if _previous_active_tab > -1: content_container.get_child(_previous_active_tab).visible = false content_container.get_child(tab).visible = true diff --git a/graph_node_renderer/textures/add_icon.svg b/graph_node_renderer/textures/add_icon.svg new file mode 100644 index 0000000..afad08a --- /dev/null +++ b/graph_node_renderer/textures/add_icon.svg @@ -0,0 +1 @@ + diff --git a/graph_node_renderer/textures/add_icon.svg.import b/graph_node_renderer/textures/add_icon.svg.import new file mode 100644 index 0000000..a14b73d --- /dev/null +++ b/graph_node_renderer/textures/add_icon.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://drxi5ks3mqbnk" +path="res://.godot/imported/add_icon.svg-1a070ac229fd6d9079fc5a70a6dc355a.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://graph_node_renderer/textures/add_icon.svg" +dest_files=["res://.godot/imported/add_icon.svg-1a070ac229fd6d9079fc5a70a6dc355a.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/graph_node_renderer/textures/remove-icon.svg b/graph_node_renderer/textures/remove-icon.svg new file mode 100644 index 0000000..eb8e244 --- /dev/null +++ b/graph_node_renderer/textures/remove-icon.svg @@ -0,0 +1 @@ + diff --git a/graph_node_renderer/textures/remove-icon.svg.import b/graph_node_renderer/textures/remove-icon.svg.import new file mode 100644 index 0000000..d791b97 --- /dev/null +++ b/graph_node_renderer/textures/remove-icon.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cdhco73u4faf8" +path="res://.godot/imported/remove-icon.svg-39dfaea1590f79f30a4d86a2f83a7c3d.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://graph_node_renderer/textures/remove-icon.svg" +dest_files=["res://.godot/imported/remove-icon.svg-39dfaea1590f79f30a4d86a2f83a7c3d.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/graph_node_renderer/variable_viewer.gd b/graph_node_renderer/variable_viewer.gd new file mode 100644 index 0000000..8b66337 --- /dev/null +++ b/graph_node_renderer/variable_viewer.gd @@ -0,0 +1,280 @@ +# (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 VBoxContainer +class_name VariableViewer + +const REMOVE_ICON = preload("res://graph_node_renderer/textures/remove-icon.svg") + +@onready var new_variable_button: Button = %NewVariableButton +@onready var variable_tree: Tree = %VariableTree +@onready var types_popup: PopupMenu = %TypesPopup + +var root: TreeItem +var _old_name: String + +signal top_field_edited(old_name: String, new_name: String, new_value: Variant) +signal top_field_removed(field_name: String) + + +func _ready() -> void: + variable_tree.set_column_title(0, "Name") + variable_tree.set_column_title(1, "Value") + variable_tree.set_column_title(2, "Type") + variable_tree.set_column_expand(1, true) + variable_tree.set_column_expand_ratio(0, 15) + variable_tree.set_column_expand_ratio(1, 70) + variable_tree.set_column_expand_ratio(2, 15) + root = variable_tree.create_item() + rebuild_variable_tree() + variable_tree.item_activated.connect( + func(): + var column := variable_tree.get_selected_column() + var item := variable_tree.get_selected() + + # do nothing if this is an array and user is trying to edit index + # (TODO: proper reordering) + if column == 0 && item.get_meta(&"indexed"): + return + + if column < 2: + _old_name = item.get_text(0) + + variable_tree.edit_selected(true) + else: + var pos := get_global_mouse_position() + var r := Rect2i(Vector2i(pos), Vector2i(0, 100)) + + for i in types_popup.get_item_count(): + types_popup.set_item_checked(i, false) + + types_popup.set_item_checked(item.get_metadata(2), true) + types_popup.popup(r) + ) + + variable_tree.item_edited.connect( + func(): + var item := variable_tree.get_selected() + var new_name := item.get_text(0) + var new_value + var type: DeckType.Types = item.get_metadata(2) + + match type: + DeckType.Types.STRING: + new_value = item.get_text(1) + DeckType.Types.BOOL: + new_value = item.is_checked(1) + DeckType.Types.NUMERIC: + new_value = item.get_range(1) + _: + new_value = item.get_meta(&"value") + + #item.set_meta(&"value", new_value) + + if !item.has_meta(&"container"): + top_field_edited.emit(_old_name, new_name, new_value) + else: + var container = item.get_meta(&"container") + + # array + if item.get_meta(&"indexed", false): + var index := item.get_index() + (container as Array)[index] = new_value + # dictionary + else: + var key := new_name + (container as Dictionary).erase(_old_name) + (container as Dictionary)[key] = new_value + + set_item_value(item, new_value) + + ) + + variable_tree.nothing_selected.connect(variable_tree.deselect_all) + + variable_tree.button_clicked.connect(_on_variable_tree_button_clicked) + + for i in DeckType.Types.size() - 1: + types_popup.add_radio_check_item(DeckType.type_str(i)) + + types_popup.id_pressed.connect(_on_types_popup_id_pressed) + + new_variable_button.pressed.connect(_on_new_variable_button_pressed) + + disable_new_button() + + +func rebuild_variable_tree(data: Dictionary = {}) -> void: + #variable_tree.clear() # godot will raw dog a nullptr later if we clear the whole tree + for i in root.get_children(): + i.free() + + for i in data: + add_item(i, data[i]) + + +func add_item(item_name: String, item_value: Variant, parent: TreeItem = root, container: Variant = null, indexed: bool = false) -> TreeItem: + # the container parameter is -1 instead of null because Object#get_meta() logs an error + # if the default parameter is null + var item := variable_tree.create_item(parent) + item.set_text(0, item_name) + + var type: DeckType.Types = DeckType.INVERSE_GODOT_TYPES_MAP[typeof(item_value)] + item.set_meta(&"container", container) + item.set_meta(&"indexed", indexed) + + set_item_value(item, item_value) + + item.set_text(2, DeckType.type_str(type)) + item.set_metadata(2, type) + if item_value is Dictionary: + for i in item_value: + add_item(i, item_value[i], item, item_value) + if item_value is Array: + for i in (item_value as Array).size(): + add_item(str(i), item_value[i], item, item_value, true) + #item.collapsed = true + item.add_button(2, REMOVE_ICON) + + return item + + +func set_item_value(item: TreeItem, value: Variant) -> void: + item.set_meta(&"value", value) + var type: DeckType.Types = DeckType.INVERSE_GODOT_TYPES_MAP[typeof(value)] + match type: + DeckType.Types.NUMERIC: + item.set_cell_mode(1, TreeItem.CELL_MODE_RANGE) + item.set_range_config(1, -9999, 9999, 0.0001) + item.set_range(1, value) + DeckType.Types.BOOL: + item.set_cell_mode(1, TreeItem.CELL_MODE_CHECK) + item.set_checked(1, value) + _: + item.set_text(1, str(value)) + + if item.has_meta(&"container"): + var container = item.get_meta(&"container") + if container is Array: + var index := item.get_index() + container[index] = value + else: + var key := item.get_text(0) + container[key] = value + + if item.get_parent() != root: + refresh_item_value(item.get_parent()) + + +func refresh_item_value(item: TreeItem) -> void: + var value = item.get_meta(&"value") + var type: DeckType.Types = DeckType.INVERSE_GODOT_TYPES_MAP[typeof(value)] + match type: + DeckType.Types.NUMERIC: + item.set_cell_mode(1, TreeItem.CELL_MODE_RANGE) + item.set_range_config(1, -9999, 9999, 0.0001) + item.set_range(1, value) + DeckType.Types.BOOL: + item.set_cell_mode(1, TreeItem.CELL_MODE_CHECK) + item.set_checked(1, value) + _: + item.set_cell_mode(1, TreeItem.CELL_MODE_STRING) + item.set_text(1, str(value)) + + +func _on_variable_tree_button_clicked(item: TreeItem, column: int, id: int, mouse_button_index: int) -> void: + if mouse_button_index != MOUSE_BUTTON_LEFT: + return + + # we only have a delete button for now, so assume it is what's clicked + + if !item.has_meta(&"container"): + var key := item.get_text(0) + top_field_removed.emit(key) + item.free() + else: + var container = item.get_meta(&"container") + var parent := item.get_parent() + # array + if item.get_meta(&"indexed", false): + var index := item.get_index() + (container as Array).remove_at(index) + item.free() + + # go through the array and reset the index strings + for i in (container as Array).size(): + parent.get_child(i).set_text(0, str(i)) + + # dictionary + else: + (container as Dictionary).erase(item.get_text(0)) + item.free() + + refresh_item_value(parent) + + +func _on_types_popup_id_pressed(id: int) -> void: + var current_item := variable_tree.get_selected() + + if current_item.get_metadata(2) == id: + return + + current_item.set_metadata(2, id) + + var new_value: Variant + match id as DeckType.Types: + DeckType.Types.BOOL, DeckType.Types.STRING, DeckType.Types.NUMERIC: + # simple types that can generally be converted between each other + var target_type: Variant.Type = DeckType.GODOT_TYPES_MAP[id] + new_value = type_convert(current_item.get_meta(&"value"), target_type) + DeckType.Types.ARRAY: + new_value = [] + for i in current_item.get_children(): + i.free() + DeckType.Types.DICTIONARY: + new_value = {} + for i in current_item.get_children(): + i.free() + + set_item_value(current_item, new_value) + + current_item.set_text(2, DeckType.type_str(id)) + + if !current_item.has_meta(&"container"): + var field_name := current_item.get_text(0) + top_field_edited.emit(field_name, field_name, new_value) + + +func _on_new_variable_button_pressed() -> void: + var selected := variable_tree.get_selected() + # TODO: UX impr. - if selected is part of a container, add to that container instead + # (but if selected is a container, prioritize adding to that) + if selected == null || !(selected.get_meta(&"value") is Array || selected.get_meta(&"value") is Dictionary): + # top field + var var_name := "new_variable%s" % variable_tree.get_root().get_child_count() + var new_item := add_item(var_name, "") + top_field_edited.emit(var_name, var_name, "") + new_item.select(0) + else: + var container = selected.get_meta(&"value") + if container is Dictionary: + var var_name := "new_key%s" % (container as Dictionary).size() + var new_item := add_item(var_name, "", selected, container) + new_item.select(0) + container[var_name] = "" + else: + var index := (container as Array).size() + (container as Array).append("") + var new_item := add_item(str(index), "", selected, container, true) + new_item.select(0) + refresh_item_value(selected) + + +func enable_new_button() -> void: + new_variable_button.disabled = false + new_variable_button.tooltip_text = "" + + +func disable_new_button() -> void: + new_variable_button.disabled = true + new_variable_button.tooltip_text = "There is no deck open, so you can not create a new variable.\nOpen a deck first." diff --git a/graph_node_renderer/variable_viewer.tscn b/graph_node_renderer/variable_viewer.tscn new file mode 100644 index 0000000..c51e402 --- /dev/null +++ b/graph_node_renderer/variable_viewer.tscn @@ -0,0 +1,37 @@ +[gd_scene load_steps=3 format=3 uid="uid://ovf5nt5pt0oj"] + +[ext_resource type="Script" path="res://graph_node_renderer/variable_viewer.gd" id="1_b5gmj"] +[ext_resource type="Texture2D" uid="uid://drxi5ks3mqbnk" path="res://graph_node_renderer/textures/add_icon.svg" id="2_15lro"] + +[node name="VariableViewer" type="VBoxContainer"] +anchors_preset = -1 +anchor_right = 0.467014 +anchor_bottom = 0.359568 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_b5gmj") +metadata/_edit_use_anchors_ = true + +[node name="Label" type="Label" parent="."] +layout_mode = 2 +text = "Deck Variables" + +[node name="VariableTree" type="Tree" parent="."] +unique_name_in_owner = true +layout_mode = 2 +size_flags_vertical = 3 +columns = 3 +column_titles_visible = true +allow_rmb_select = true +allow_search = false +hide_root = true + +[node name="TypesPopup" type="PopupMenu" parent="."] +unique_name_in_owner = true + +[node name="NewVariableButton" type="Button" parent="."] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 4 +text = "New" +icon = ExtResource("2_15lro") diff --git a/project.godot b/project.godot index 1b21f61..e784511 100644 --- a/project.godot +++ b/project.godot @@ -44,7 +44,7 @@ rename_node={ "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194333,"physical_keycode":0,"key_label":0,"unicode":0,"echo":false,"script":null) ] } -toggle_console={ +toggle_bottom_dock={ "deadzone": 0.5, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":78,"key_label":0,"unicode":110,"echo":false,"script":null) ] diff --git a/script_templates/Object/empty_template_with_license.gd b/script_templates/Object/empty_template_with_license.gd new file mode 100644 index 0000000..2d6dbe4 --- /dev/null +++ b/script_templates/Object/empty_template_with_license.gd @@ -0,0 +1,5 @@ +# 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) +extends _BASE_