From ed5a0f427b6617aaaf354c8d02ee2ab5036fff93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lera=20Elvo=C3=A9?= Date: Sun, 14 Apr 2024 12:40:26 +0000 Subject: [PATCH] add shortcuts (#145) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes #133 Reviewed-on: https://codeberg.org/StreamGraph/StreamGraph/pulls/145 Co-authored-by: Lera ElvoƩ Co-committed-by: Lera ElvoƩ --- graph_node_renderer/deck_holder_renderer.gd | 82 ++++++--- graph_node_renderer/deck_holder_renderer.tscn | 64 +------ .../deck_node_renderer_graph_node.tscn | 2 +- .../deck_renderer_graph_edit.gd | 35 +++- .../deck_renderer_graph_edit.tscn | 3 - graph_node_renderer/settings_dialog.gd | 14 +- graph_node_renderer/settings_dialog.tscn | 38 +++- .../shortcuts/renderer_shortcuts.gd | 173 ++++++++++++++++++ .../shortcuts/shortcuts_editor.gd | 136 ++++++++++++++ .../shortcuts/shortcuts_editor.tscn | 6 + graph_node_renderer/textures/revert-icon.svg | 1 + .../textures/revert-icon.svg.import | 37 ++++ project.godot | 20 -- 13 files changed, 501 insertions(+), 110 deletions(-) create mode 100644 graph_node_renderer/shortcuts/renderer_shortcuts.gd create mode 100644 graph_node_renderer/shortcuts/shortcuts_editor.gd create mode 100644 graph_node_renderer/shortcuts/shortcuts_editor.tscn create mode 100644 graph_node_renderer/textures/revert-icon.svg create mode 100644 graph_node_renderer/textures/revert-icon.svg.import diff --git a/graph_node_renderer/deck_holder_renderer.gd b/graph_node_renderer/deck_holder_renderer.gd index 6deab4e..51172eb 100644 --- a/graph_node_renderer/deck_holder_renderer.gd +++ b/graph_node_renderer/deck_holder_renderer.gd @@ -20,12 +20,6 @@ const PERSISTENCE_NAMESPACE := "default" ## Reference to the [FileDialog] used for File operations through the program. @onready var file_dialog: FileDialog = $FileDialog -@export var new_deck_shortcut: Shortcut -@export var open_deck_shortcut: Shortcut -@export var save_deck_shortcut: Shortcut -@export var save_deck_as_shortcut: Shortcut -@export var close_deck_shortcut: Shortcut - ## Enum for storing the Options in the "File" PopupMenu. enum FileMenuId { NEW, @@ -62,8 +56,12 @@ enum HelpMenuId { @onready var about_dialog: AcceptDialog = %AboutDialog enum EditMenuId { + COPY, + PASTE, + DUPLICATE, SETTINGS, } +@onready var edit_popup_menu: PopupMenu = %Edit @onready var settings_dialog: SettingsDialog = %SettingsDialog ## Weak Reference to the Deck that is currently going to be saved. @@ -105,6 +103,11 @@ func _ready() -> void: PERSISTENCE_NAMESPACE, "config", "recent_path", OS.get_system_dir(OS.SYSTEM_DIR_DOCUMENTS) ) + + RendererShortcuts.load_overrides() + + reset_popup_menu_shortcuts() + add_recents_to_menu() var rpc_port: int = RendererPersistence.get_or_create( @@ -153,12 +156,6 @@ func _ready() -> void: Connections.twitch.chat_received_rich.connect(Connections._twitch_chat_received) - file_popup_menu.set_item_shortcut(FileMenuId.NEW, new_deck_shortcut) - file_popup_menu.set_item_shortcut(FileMenuId.OPEN, open_deck_shortcut) - 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_renderer().dirty = true @@ -172,6 +169,30 @@ func _ready() -> void: ) +func reset_popup_menu_shortcuts() -> void: + file_popup_menu.set_item_shortcut(FileMenuId.NEW, RendererShortcuts.get_shortcut("new_deck")) + file_popup_menu.set_item_shortcut(FileMenuId.OPEN, RendererShortcuts.get_shortcut("open_deck")) + file_popup_menu.set_item_shortcut(FileMenuId.SAVE, RendererShortcuts.get_shortcut("save_deck")) + file_popup_menu.set_item_shortcut(FileMenuId.SAVE_AS, RendererShortcuts.get_shortcut("save_deck_as")) + file_popup_menu.set_item_shortcut(FileMenuId.CLOSE, RendererShortcuts.get_shortcut("close_deck")) + + edit_popup_menu.set_item_shortcut(EditMenuId.COPY, RendererShortcuts.get_shortcut("copy_nodes")) + edit_popup_menu.set_item_shortcut(EditMenuId.PASTE, RendererShortcuts.get_shortcut("paste_nodes")) + edit_popup_menu.set_item_shortcut(EditMenuId.DUPLICATE, RendererShortcuts.get_shortcut("duplicate_nodes")) + edit_popup_menu.set_item_shortcut(EditMenuId.SETTINGS, RendererShortcuts.get_shortcut("settings")) + + +func reset_recents_shortcuts() -> void: + for i in recent_files.size(): + var s := RendererShortcuts.get_shortcut("open_recent_deck_%s" % [i + 1]) + file_popup_menu.set_item_shortcut(file_popup_menu.get_item_count() - recent_files.size() + i, s) + + +func _notification(what: int) -> void: + if what == RendererShortcuts.NOTIFICATION_SHORTCUTS_UPDATED: + reset_popup_menu_shortcuts() + + 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): @@ -209,6 +230,29 @@ func _on_edit_id_pressed(id: int) -> void: match id: EditMenuId.SETTINGS: settings_dialog.popup_centered() + EditMenuId.COPY: + var r := get_active_deck_renderer() + r._on_copy_nodes_request() + EditMenuId.PASTE: + var r := get_active_deck_renderer() + r._on_paste_nodes_request() + EditMenuId.DUPLICATE: + var r := get_active_deck_renderer() + r._on_duplicate_nodes_request() + + +func _on_edit_about_to_popup() -> void: + # enable/disable the copy/paste buttons depending on if there's a deck renderer and if it has nodes selected + if tab_container.get_tab_count() == 0: + edit_popup_menu.set_item_disabled(EditMenuId.COPY, true) + edit_popup_menu.set_item_disabled(EditMenuId.PASTE, true) + edit_popup_menu.set_item_disabled(EditMenuId.DUPLICATE, true) + return + + edit_popup_menu.set_item_disabled(EditMenuId.PASTE, false) + var r := get_active_deck_renderer() + edit_popup_menu.set_item_disabled(EditMenuId.COPY, r.get_selected_nodes().size() == 0) + edit_popup_menu.set_item_disabled(EditMenuId.DUPLICATE, r.get_selected_nodes().size() == 0) ## Adds an empty [DeckRendererGraphEdit] with a corresponding [Deck] for it's data. @@ -225,6 +269,7 @@ func add_empty_deck() -> void: 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: tab_container.close_tab(tab_container.get_current_tab()) @@ -510,14 +555,8 @@ func add_recents_to_menu() -> void: file = base.path_join(filename) file_popup_menu.add_item(file) - var s := Shortcut.new() - var k := InputEventKey.new() - @warning_ignore("int_as_enum_without_cast") - k.keycode = KEY_1 + i - k.ctrl_pressed = true - s.events.append(k) - file_popup_menu.set_item_shortcut(file_popup_menu.get_item_count() - 1, s) + reset_recents_shortcuts() RendererPersistence.set_value(PERSISTENCE_NAMESPACE, "config", "recent_files", recent_files) @@ -552,8 +591,8 @@ func _on_unsaved_changes_dialog_confirmed() -> void: quit_completed.emit() -func _unhandled_input(event: InputEvent) -> void: - if event.is_action_pressed("toggle_bottom_dock"): +func _unhandled_key_input(event: InputEvent) -> void: + if RendererShortcuts.check_shortcut("toggle_bottom_dock", event): bottom_dock.visible = !bottom_dock.visible accept_event() @@ -564,3 +603,4 @@ func _on_help_id_pressed(id: int) -> void: about_dialog.show() HelpMenuId.DOCS: OS.shell_open("https://codeberg.org/Eroax/StreamGraph/wiki") + diff --git a/graph_node_renderer/deck_holder_renderer.tscn b/graph_node_renderer/deck_holder_renderer.tscn index cd9aa06..cf8d497 100644 --- a/graph_node_renderer/deck_holder_renderer.tscn +++ b/graph_node_renderer/deck_holder_renderer.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=27 format=3 uid="uid://duaah5x0jhkn6"] +[gd_scene load_steps=17 format=3 uid="uid://duaah5x0jhkn6"] [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"] @@ -17,52 +17,6 @@ [ext_resource type="PackedScene" uid="uid://brfrufvkjwcor" path="res://graph_node_renderer/rpc_setup_dialog.tscn" id="12_1xrfk"] [ext_resource type="PackedScene" uid="uid://dodqetbke5wji" path="res://graph_node_renderer/settings_dialog.tscn" id="16_rktri"] -[sub_resource type="InputEventKey" id="InputEventKey_giamc"] -device = -1 -ctrl_pressed = true -keycode = 78 -unicode = 110 - -[sub_resource type="Shortcut" id="Shortcut_30rq6"] -events = [SubResource("InputEventKey_giamc")] - -[sub_resource type="InputEventKey" id="InputEventKey_cyjf4"] -device = -1 -ctrl_pressed = true -keycode = 79 -unicode = 111 - -[sub_resource type="Shortcut" id="Shortcut_m48tj"] -events = [SubResource("InputEventKey_cyjf4")] - -[sub_resource type="InputEventKey" id="InputEventKey_jgr3p"] -device = -1 -ctrl_pressed = true -keycode = 83 -unicode = 115 - -[sub_resource type="Shortcut" id="Shortcut_xr6s4"] -events = [SubResource("InputEventKey_jgr3p")] - -[sub_resource type="InputEventKey" id="InputEventKey_762xj"] -device = -1 -shift_pressed = true -ctrl_pressed = true -keycode = 83 -unicode = 83 - -[sub_resource type="Shortcut" id="Shortcut_myxuq"] -events = [SubResource("InputEventKey_762xj")] - -[sub_resource type="InputEventKey" id="InputEventKey_exx3o"] -device = -1 -ctrl_pressed = true -keycode = 87 -unicode = 119 - -[sub_resource type="Shortcut" id="Shortcut_46v8y"] -events = [SubResource("InputEventKey_exx3o")] - [node name="DeckHolderRenderer" type="Control"] layout_mode = 3 anchors_preset = 15 @@ -75,11 +29,6 @@ script = ExtResource("1_67g2g") DECK_SCENE = ExtResource("3_uf16c") DEBUG_DECKS_LIST = ExtResource("4_ux0jt") DEBUG_NODES_LIST = ExtResource("5_pnfg8") -new_deck_shortcut = SubResource("Shortcut_30rq6") -open_deck_shortcut = SubResource("Shortcut_m48tj") -save_deck_shortcut = SubResource("Shortcut_xr6s4") -save_deck_as_shortcut = SubResource("Shortcut_myxuq") -close_deck_shortcut = SubResource("Shortcut_46v8y") [node name="MarginContainer" type="MarginContainer" parent="."] layout_mode = 1 @@ -128,9 +77,15 @@ item_7/separator = true [node name="Edit" type="PopupMenu" parent="MarginContainer/VSplitContainer/VBoxContainer/MenuBar"] unique_name_in_owner = true -item_count = 1 -item_0/text = "Settings..." +item_count = 4 +item_0/text = "Copy Node(s)" item_0/id = 0 +item_1/text = "Paste Node(s)" +item_1/id = 1 +item_2/text = "Duplicate Node(s)" +item_2/id = 2 +item_3/text = "Settings..." +item_3/id = 3 [node name="Connections" type="PopupMenu" parent="MarginContainer/VSplitContainer/VBoxContainer/MenuBar"] unique_name_in_owner = true @@ -205,6 +160,7 @@ unique_name_in_owner = true 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"] diff --git a/graph_node_renderer/deck_node_renderer_graph_node.tscn b/graph_node_renderer/deck_node_renderer_graph_node.tscn index df4d75c..9194da6 100644 --- a/graph_node_renderer/deck_node_renderer_graph_node.tscn +++ b/graph_node_renderer/deck_node_renderer_graph_node.tscn @@ -3,9 +3,9 @@ [ext_resource type="Script" path="res://graph_node_renderer/deck_node_renderer_graph_node.gd" id="1_pos0w"] [node name="DeckNodeRendererGraphNode" type="GraphNode"] -script = ExtResource("1_pos0w") custom_minimum_size = Vector2(200, 0) offset_right = 200.0 offset_bottom = 55.0 resizable = true title = "Deck Node" +script = ExtResource("1_pos0w") diff --git a/graph_node_renderer/deck_renderer_graph_edit.gd b/graph_node_renderer/deck_renderer_graph_edit.gd index b67e769..336fba6 100644 --- a/graph_node_renderer/deck_renderer_graph_edit.gd +++ b/graph_node_renderer/deck_renderer_graph_edit.gd @@ -210,7 +210,7 @@ func get_selected_nodes() -> Array: ## Executes functionality based off hotkey inputs. Specifically handles creating groups ## based off the action "group_nodes". func _gui_input(event: InputEvent) -> void: - if event.is_action_pressed("group_nodes") and get_selected_nodes().size() > 0: + if RendererShortcuts.check_shortcut("group_nodes", event) and get_selected_nodes().size() > 0: clear_connections() var nodes = get_selected_nodes().map( func(x: DeckNodeRendererGraphNode): @@ -218,7 +218,16 @@ func _gui_input(event: InputEvent) -> void: ) deck.group_nodes(nodes) refresh_connections() - get_viewport().set_input_as_handled() + accept_event() + + if RendererShortcuts.check_shortcut("add_node", event): + var p := get_viewport_rect().position + get_global_mouse_position() + p += Vector2(10, 10) + var r := Rect2i(p, search_popup_panel.size) + search_popup_panel.popup_on_parent(r) + add_node_menu.focus_search_bar() + popup_position = r.position + accept_event() if event.is_action_pressed("debug_make_unique") and get_selected_nodes().size() == 1: var node: DeckNode = get_selected_nodes().map( @@ -230,12 +239,26 @@ func _gui_input(event: InputEvent) -> void: node.make_unique() - if event.is_action_pressed("rename_node") and get_selected_nodes().size() == 1: + if RendererShortcuts.check_shortcut("rename_node", event) and get_selected_nodes().size() == 1: var node: DeckNodeRendererGraphNode = get_selected_nodes()[0] var pos := get_viewport_rect().position + get_global_mouse_position() rename_popup.popup_on_parent(Rect2i(pos, rename_popup_size)) rename_popup.le.size.x = rename_popup_size.x rename_popup.set_text(node.title) + accept_event() + + # copy/paste/duplicate/etc + if RendererShortcuts.check_shortcut("copy_nodes", event) and get_selected_nodes().size() > 0: + _on_copy_nodes_request() + accept_event() + + if RendererShortcuts.check_shortcut("duplicate_nodes", event) and get_selected_nodes().size() > 0: + _on_duplicate_nodes_request() + accept_event() + + if RendererShortcuts.check_shortcut("paste_nodes", event): + _on_paste_nodes_request() + accept_event() ## Handles entering groups with action "enter_group". Done here to bypass neighbor @@ -244,11 +267,11 @@ func _input(event: InputEvent) -> void: if not has_focus(): return - if event.is_action_pressed("enter_group") and get_selected_nodes().size() == 1: + if RendererShortcuts.check_shortcut("enter_group", event) and get_selected_nodes().size() == 1: if (get_selected_nodes()[0] as DeckNodeRendererGraphNode).node.node_type != "group_node": return group_enter_requested.emit((get_selected_nodes()[0] as DeckNodeRendererGraphNode).node.group_id) - get_viewport().set_input_as_handled() + accept_event() func _on_rename_popup_rename_confirmed(new_name: String) -> void: @@ -271,6 +294,7 @@ func _on_popup_request(p_popup_position: Vector2) -> void: add_node_menu.focus_search_bar() popup_position = p_popup_position + ## Connected to [signal AddNodeMenu.node_selected] and creates a [DeckNode] using ## [method NodeDB.instance_node]. Then placing it at the [member scroll_offset] + ## [member popup_position] / [member zoom] @@ -297,7 +321,6 @@ func _on_deck_nodes_disconnected(from_node_id: String, to_node_id: String, from_ return x.node._id == to_node_id )[0] - print(is_node_connected(from_node.name, from_output_port, to_node.name, to_input_port)) disconnect_node(from_node.name, from_output_port, to_node.name, to_input_port) diff --git a/graph_node_renderer/deck_renderer_graph_edit.tscn b/graph_node_renderer/deck_renderer_graph_edit.tscn index 61f3565..783476e 100644 --- a/graph_node_renderer/deck_renderer_graph_edit.tscn +++ b/graph_node_renderer/deck_renderer_graph_edit.tscn @@ -20,9 +20,6 @@ script = ExtResource("1_pojfs") NODE_SCENE = ExtResource("2_67ymi") ADD_NODE_MENU_SCENE = ExtResource("3_thbt5") -[connection signal="copy_nodes_request" from="." to="." method="_on_copy_nodes_request"] [connection signal="delete_nodes_request" from="." to="." method="_on_delete_nodes_request"] -[connection signal="duplicate_nodes_request" from="." to="." method="_on_duplicate_nodes_request"] -[connection signal="paste_nodes_request" from="." to="." method="_on_paste_nodes_request"] [connection signal="popup_request" from="." to="." method="_on_popup_request"] [connection signal="scroll_offset_changed" from="." to="." method="_on_scroll_offset_changed"] diff --git a/graph_node_renderer/settings_dialog.gd b/graph_node_renderer/settings_dialog.gd index 7fb88a4..cd4e2bc 100644 --- a/graph_node_renderer/settings_dialog.gd +++ b/graph_node_renderer/settings_dialog.gd @@ -6,7 +6,7 @@ class_name SettingsDialog @onready var category_tree: Tree = %CategoryTree @onready var category_content: Control = %CategoryContent -@onready var shortcuts_tree: Tree = %Shortcuts +@onready var shortcuts_editor: ShortcutsEditor = %ShortcutsEditor func _ready() -> void: @@ -21,5 +21,17 @@ func _ready() -> void: for i: Control in category_content.get_children(): i.visible = i.get_index() == item.get_index() ) + + canceled.connect(shortcuts_editor.ensure_no_focus) + confirmed.connect(shortcuts_editor.ensure_no_focus) category_tree.set_selected(cr.get_child(0), 0) + + +func _unhandled_key_input(event: InputEvent) -> void: + if not visible: + return + + if event.keycode == KEY_ESCAPE and event.is_pressed(): + canceled.emit() + hide() diff --git a/graph_node_renderer/settings_dialog.tscn b/graph_node_renderer/settings_dialog.tscn index a53ad3c..79e3cb3 100644 --- a/graph_node_renderer/settings_dialog.tscn +++ b/graph_node_renderer/settings_dialog.tscn @@ -1,6 +1,7 @@ -[gd_scene load_steps=2 format=3 uid="uid://dodqetbke5wji"] +[gd_scene load_steps=3 format=3 uid="uid://dodqetbke5wji"] [ext_resource type="Script" path="res://graph_node_renderer/settings_dialog.gd" id="1_lh25g"] +[ext_resource type="PackedScene" uid="uid://bvjxc2vyx35b1" path="res://graph_node_renderer/shortcuts/shortcuts_editor.tscn" id="2_5tyfb"] [node name="SettingsDialog" type="AcceptDialog"] title = "Settings" @@ -8,6 +9,7 @@ initial_position = 1 size = Vector2i(705, 370) min_size = Vector2i(500, 250) ok_button_text = "Close" +dialog_close_on_escape = false script = ExtResource("1_lh25g") [node name="HSplitContainer" type="HSplitContainer" parent="."] @@ -44,8 +46,7 @@ text = "Library Group Search Paths" layout_mode = 2 size_flags_vertical = 3 -[node name="Shortcuts" type="Tree" parent="HSplitContainer/CategoryContent"] -unique_name_in_owner = true +[node name="Shortcuts" type="VBoxContainer" parent="HSplitContainer/CategoryContent"] visible = false layout_mode = 1 anchors_preset = 15 @@ -53,4 +54,33 @@ anchor_right = 1.0 anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 -hide_root = true + +[node name="Label" type="Label" parent="HSplitContainer/CategoryContent/Shortcuts"] +layout_mode = 2 +text = "Shortcuts" + +[node name="PanelContainer" type="PanelContainer" parent="HSplitContainer/CategoryContent/Shortcuts"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="ScrollContainer" type="ScrollContainer" parent="HSplitContainer/CategoryContent/Shortcuts/PanelContainer"] +layout_mode = 2 +horizontal_scroll_mode = 0 + +[node name="MarginContainer" type="MarginContainer" parent="HSplitContainer/CategoryContent/Shortcuts/PanelContainer/ScrollContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +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="ShortcutsEditor" parent="HSplitContainer/CategoryContent/Shortcuts/PanelContainer/ScrollContainer/MarginContainer" instance=ExtResource("2_5tyfb")] +unique_name_in_owner = true +layout_mode = 2 + +[node name="Label2" type="Label" parent="HSplitContainer/CategoryContent/Shortcuts"] +layout_mode = 2 +text = "Click on a shortcut to change it. +Press Escape to cancel the change or Enter to confirm it." diff --git a/graph_node_renderer/shortcuts/renderer_shortcuts.gd b/graph_node_renderer/shortcuts/renderer_shortcuts.gd new file mode 100644 index 0000000..03dc3af --- /dev/null +++ b/graph_node_renderer/shortcuts/renderer_shortcuts.gd @@ -0,0 +1,173 @@ +# (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) +class_name RendererShortcuts +## @experimental +## A manager for shortcuts/hotkeys for the default renderer. +## +## Allows overriding shortcuts and provides an API for the renderer's various interfaces to check +## that a shortcut has been pressed. Also handles saving the overrides using [RendererPersistence].[br] +## Shortcuts are mapped to an [codee]action[/code] String, much like [InputMap], with the difference +## being that only one shortcut can be assigned to any one action. This may change in the future. + +## The map of overriden shortcuts. See [member defaults]. +static var map = {} + +const PERSISTENCE_NAMESPACE := "default" +const PERSISTENCE_CHANNEL := "shortcuts" +const PERSISTENCE_KEY := "overrides" + +## Notification that is emitted when the overrides have finished loading from [RendererPersistence]. +const NOTIFICATION_SHORTCUTS_LOADED := 99999 +## Notification that is emitted when any shortcut has been updated (i.e., overridden or reset). +const NOTIFICATION_SHORTCUTS_UPDATED := 99998 + +## Map of default shortcuts.[br] +## The structure is:[br] +##[codeblock] +##{ +## action: Shortcut|String +##} +##[/codeblock] +## where [code]action[/code] is any String.[br] +## If [code]action[/code] starts with [code]"_sep"[/code], the value must also be a String, +## which should be interpreted as a category separator with the value being the category name.[br] +## Shortcuts can be created more easily with [method create_shortcut_from_dict]. +static var defaults = { + "_sep_file": "file", + "new_deck": create_shortcut_from_dict({"ctrl": true, "key": "n"}), + "open_deck": create_shortcut_from_dict({"ctrl": true, "key": "o"}), + "save_deck": create_shortcut_from_dict({"ctrl": true, "key": "s"}), + "save_deck_as": create_shortcut_from_dict({"ctrl": true, "shift": true, "key": "s"}), + "close_deck": create_shortcut_from_dict({"ctrl": true, "key": "w"}), + + "open_recent_deck_1": create_shortcut_from_dict({"ctrl": true, "key": "1"}), + "open_recent_deck_2": create_shortcut_from_dict({"ctrl": true, "key": "2"}), + "open_recent_deck_3": create_shortcut_from_dict({"ctrl": true, "key": "3"}), + "open_recent_deck_4": create_shortcut_from_dict({"ctrl": true, "key": "4"}), + "open_recent_deck_5": create_shortcut_from_dict({"ctrl": true, "key": "5"}), + + "_sep_deck": "deck", + "add_node": create_shortcut_from_dict({"shift": true, "key": "a"}), + "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"}), + + "_sep_nodes": "nodes", + "copy_nodes": create_shortcut_from_dict({"ctrl": true, "key": "c"}), + #"cut_nodes": create_shortcut_from_dict({"ctrl": true, "key": "x"}), + "paste_nodes": create_shortcut_from_dict({"ctrl": true, "key": "v"}), + "duplicate_nodes": create_shortcut_from_dict({"ctrl": true, "key": "d"}), + + "_sep_misc": "misc", + "settings": create_shortcut_from_dict({"ctrl": true, "key": "comma"}), + "toggle_bottom_dock": create_shortcut_from_dict({"key": "n"}), +} + + +## Loads shortcut overrides from [RendererPersistence]. +static func load_overrides() -> void: + var loaded_map: Dictionary = RendererPersistence.get_or_create(PERSISTENCE_NAMESPACE, PERSISTENCE_CHANNEL, PERSISTENCE_KEY, {}) + map_from_dict(loaded_map) + (Engine.get_main_loop() as SceneTree).get_root().propagate_notification(NOTIFICATION_SHORTCUTS_LOADED) + + +## Returns whether a shortcut [param action] has been pressed with the given [param event]. +## To be called from [method Node._input] and similar. +static func check_shortcut(action: String, event: InputEvent) -> bool: + var over := (map.get(action) as Shortcut) + if over: + return over.matches_event(event) and event.pressed and not event.is_echo() + + return (defaults.get(action) as Shortcut).matches_event(event) and event.pressed and not event.is_echo() + + +## Returns the [Shortcut] associated with the [param action]. +static func get_shortcut(action: String) -> Shortcut: + var over := map.get(action, null) as Shortcut + if over: + return over + + return defaults.get(action, null) as Shortcut + + +## Returns the full shortcut map, which is a combination of default and overridden [code]action[/code]s. +static func get_full_map() -> Dictionary: + var defaults_copy := defaults.duplicate() + defaults_copy.merge(map, true) + return defaults_copy + + +## Returns the default [Shortcut] for the given [param action]. +static func get_default(action: String) -> Shortcut: + return defaults.get(action) + + +## Overrides an [param action], setting its shortcut to the equivalent of the [param event]. +static func add_override(action: String, event: InputEvent) -> Shortcut: + var e := { + "key": OS.get_keycode_string(event.keycode).to_lower(), + "ctrl": event.ctrl_pressed, + "alt": event.alt_pressed, + "shift": event.shift_pressed, + } + + map[action] = create_shortcut_from_dict(e) + commit_overrides() + (Engine.get_main_loop() as SceneTree).get_root().propagate_notification(NOTIFICATION_SHORTCUTS_UPDATED) + return map[action] + + +## Returns [code]true[/code] if the [param action] has been overridden. +static func has_override(action: String) -> bool: + return map.has(action) + + +## Removes the override for [param action], resetting the shortcut to default. +static func remove_override(action: String) -> void: + map.erase(action) + commit_overrides() + (Engine.get_main_loop() as SceneTree).get_root().propagate_notification(NOTIFICATION_SHORTCUTS_UPDATED) + + +## Returns a [Shortcut] from the given [Dictionary].[br] +## The dictionary must contain at least one key, [code]key[/code], which is a Unicode +## String of the character that must be pressed to activate the shortcut.[br] +## The dictionary can also optionally contain the keys [code]ctrl[/code], [code]alt[/code], and [code]shift[/code] +## with their values being [bool] to indicate if those modifiers should be held along with the [code]key[/code]. +static func create_shortcut_from_dict(d: Dictionary) -> Shortcut: + var s := Shortcut.new() + + var input := InputEventKey.new() + input.ctrl_pressed = d.get("ctrl", false) + input.alt_pressed = d.get("alt", false) + input.shift_pressed = d.get("shift", false) + input.keycode = OS.find_keycode_from_string(d.key.to_upper()) + + s.events.append(input) + return s + + +## Serializes the overrides [member map] to a [Dictionary]. +static func map_to_dict() -> Dictionary: + var res := {} + for action: String in map: + res[action] = { + "ctrl": (map[action] as Shortcut).events[0].ctrl_pressed, + "alt": (map[action] as Shortcut).events[0].alt_pressed, + "shift": (map[action] as Shortcut).events[0].shift_pressed, + "key": OS.get_keycode_string((map[action] as Shortcut).events[0].keycode), + } + return res + + +## Loads a dictionary such as one created by [method map_to_dict] into [member map]. +static func map_from_dict(d: Dictionary) -> void: + for action: String in d: + map[action] = create_shortcut_from_dict(d[action]) + + +## Saves the overrides [member map] to [RendererPersistence]. +static func commit_overrides() -> void: + RendererPersistence.set_value(PERSISTENCE_NAMESPACE, PERSISTENCE_CHANNEL, PERSISTENCE_KEY, map_to_dict()) + RendererPersistence.commit(PERSISTENCE_NAMESPACE, PERSISTENCE_CHANNEL) diff --git a/graph_node_renderer/shortcuts/shortcuts_editor.gd b/graph_node_renderer/shortcuts/shortcuts_editor.gd new file mode 100644 index 0000000..32d4e45 --- /dev/null +++ b/graph_node_renderer/shortcuts/shortcuts_editor.gd @@ -0,0 +1,136 @@ +# (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 ShortcutsEditor +## An editor for [RendererShortcuts]. + +const REVERT_ICON = preload("res://graph_node_renderer/textures/revert-icon.svg") + + +func _notification(what: int) -> void: + if what == RendererShortcuts.NOTIFICATION_SHORTCUTS_LOADED: + var g := ButtonGroup.new() + var map := RendererShortcuts.get_full_map() + for action: String in map: + if action.begins_with("_sep"): + # draw a separator. + var l := Label.new() + l.text = map[action].capitalize() + + var hb := HBoxContainer.new() + var sep := HSeparator.new() + sep.size_flags_horizontal = Control.SIZE_EXPAND_FILL + hb.add_child(sep) + hb.add_child(l) + hb.add_child(sep.duplicate(0)) + + add_child.call_deferred(hb) + continue + + var c := ShortcutDisplay.new(action, action.capitalize(), g) + add_child.call_deferred(c) + + +## Unfocuses all [ShorcutsEditor.ShortcutDisplay] children. +func ensure_no_focus() -> void: + for c in get_children(): + if c is ShortcutDisplay: + c.unfocus_button() + + +## A container for displaying an editable shorcut. +class ShortcutDisplay extends HBoxContainer: + ## The action this container represents and edits. + var action: String + var _label: Label + var _button: ShortcutButton + var _reset_button: Button + var _cc: CenterContainer + + + func _init(p_action: String, p_label: String, p_group: ButtonGroup) -> void: + action = p_action + + _label = Label.new() + _label.size_flags_horizontal = Control.SIZE_EXPAND_FILL + _label.text = p_label + + _button = ShortcutButton.new(RendererShortcuts.get_shortcut(p_action).events[0]) + _button.button_group = p_group + _button.text = RendererShortcuts.get_shortcut(p_action).get_as_text() + + _button.confirmed.connect( + func(raw_event: InputEvent): + var override := RendererShortcuts.add_override(action, raw_event) + _button.set_event(override.events[0]) + _reset_button.disabled = false + ) + + _reset_button = Button.new() + _reset_button.icon = REVERT_ICON + _reset_button.tooltip_text = "Reset (default: %s)" % RendererShortcuts.get_default(action).get_as_text() + + _reset_button.pressed.connect( + func(): + _button.set_event(RendererShortcuts.get_default(action).events[0]) + _reset_button.disabled = true + RendererShortcuts.remove_override(action) + ) + _reset_button.disabled = not RendererShortcuts.has_override(action) + + _cc = CenterContainer.new() + _cc.size_flags_horizontal = Control.SIZE_EXPAND_FILL + _button.size_flags_horizontal = Control.SIZE_SHRINK_CENTER + + add_child(_label) + _cc.add_child(_button) + add_child(_cc) + add_child(_reset_button) + + + ## Removes focus of the container's button, in cases where focus is + ## about to be lost to prevent remapping an action on accident. + func unfocus_button() -> void: + _button.set_event(_button._event) + + +## A button that displays a shortcut and allows editing (remapping) it. +class ShortcutButton extends Button: + var _event: InputEvent + var _prev_event: InputEvent + + ## Emitted when Enter is pressed on the button. + signal confirmed(raw_event: InputEvent) + + + func _init(p_event: InputEvent) -> void: + toggle_mode = true + set_event(p_event) + + + ## Sets the event this button is storing and updates its text. + func set_event(p_event: InputEvent) -> void: + button_pressed = false + _event = p_event + text = _event.as_text() + + + func _unhandled_key_input(event: InputEvent) -> void: + if not button_pressed: + return + + accept_event() + if event.keycode == KEY_ESCAPE: + set_event(_event) + return + + if event.keycode == KEY_ENTER: + button_pressed = false + confirmed.emit(_prev_event) + return + + if event.is_pressed(): + text = "%s..." % event.as_text() + _prev_event = event + diff --git a/graph_node_renderer/shortcuts/shortcuts_editor.tscn b/graph_node_renderer/shortcuts/shortcuts_editor.tscn new file mode 100644 index 0000000..75377a6 --- /dev/null +++ b/graph_node_renderer/shortcuts/shortcuts_editor.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://bvjxc2vyx35b1"] + +[ext_resource type="Script" path="res://graph_node_renderer/shortcuts/shortcuts_editor.gd" id="1_hoas8"] + +[node name="ShortcutsEditor" type="VBoxContainer"] +script = ExtResource("1_hoas8") diff --git a/graph_node_renderer/textures/revert-icon.svg b/graph_node_renderer/textures/revert-icon.svg new file mode 100644 index 0000000..9887d0d --- /dev/null +++ b/graph_node_renderer/textures/revert-icon.svg @@ -0,0 +1 @@ + diff --git a/graph_node_renderer/textures/revert-icon.svg.import b/graph_node_renderer/textures/revert-icon.svg.import new file mode 100644 index 0000000..768ab9d --- /dev/null +++ b/graph_node_renderer/textures/revert-icon.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bgsdcu3sdl8qt" +path="res://.godot/imported/revert-icon.svg-5ba4d0c812ba7bad2eceb424fb6b2d29.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://graph_node_renderer/textures/revert-icon.svg" +dest_files=["res://.godot/imported/revert-icon.svg-5ba4d0c812ba7bad2eceb424fb6b2d29.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/project.godot b/project.godot index c72947f..b6b6095 100644 --- a/project.godot +++ b/project.godot @@ -25,26 +25,6 @@ enabled=PackedStringArray("res://addons/no-obs-ws/plugin.cfg", "res://addons/no_ [input] -group_nodes={ -"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":true,"meta_pressed":false,"pressed":false,"keycode":71,"physical_keycode":0,"key_label":0,"unicode":103,"echo":false,"script":null) -] -} -enter_group={ -"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":4194306,"key_label":0,"unicode":0,"echo":false,"script":null) -] -} -rename_node={ -"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":4194333,"physical_keycode":0,"key_label":0,"unicode":0,"echo":false,"script":null) -] -} -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) -] -} debug_make_unique={ "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":4194335,"key_label":0,"unicode":0,"echo":false,"script":null)