# (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 [code]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"}), "focus_nodes": create_shortcut_from_dict({"key": "f"}), "_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"}), "toggle_sidebar": create_shortcut_from_dict({"key": "t"}), } ## 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 static func get_full_map_string() -> Dictionary: var full_map := get_full_map() var res := {} for key: String in full_map: if key.begins_with("_"): continue res[key] = (full_map[key] as Shortcut).get_as_text() return res ## 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) ## Replaces shortcut names with their keybinds in [param s].[br] ## Example usage: [code]interpolate_string("Press {new_deck} to open a new deck.")[/code] static func interpolate_string(s: String) -> String: return s.format(get_full_map_string())