aboutsummaryrefslogtreecommitdiff
path: root/game/addons/keychain/Keychain.gd
diff options
context:
space:
mode:
author George L. Albany <Megacake1234@gmail.com>2023-02-21 22:30:12 +0100
committer GitHub <noreply@github.com>2023-02-21 22:30:12 +0100
commitf1c23555878ee4b0e40c6af5f89f05b666012309 (patch)
tree978dfdfd1ac6940414af5e19128060419076de76 /game/addons/keychain/Keychain.gd
parentfb9e316a18139ea6b6ffe3b237796b42d7114738 (diff)
Add Keychain plugin for Controls tab (#15)
* Add modified Keychain plugin for future Controls tab See https://github.com/Orama-Interactive/Keychain/tree/4.x Added Events autoload singleton for global eventing namespace Added Events.Options for global options functionality * Add Controls tab via Keychain plugin Use Events.Options for save, load, and reset of settings Separate OptionMenu tabs into scene files Add locale saving and loading Refactor SettingNodes scripts for more generalized use Remove random prints Remove useless spinbox signal connection Make Resolution consistently use Vector2i * Implement Godot project overrides for resolution and window mode Overrides are necessary as Godot does not load resolution or window mode on startup, so an override is necessary to ensure this happens. Add null checks to SettingHSlider and SettingOptionButton * Fix incorrect resolution value in ResolutionSelector * Correct project settings override behavior in editor Godot normally tries to overwrite the project settings in the editor, a template feature tag must be used to prevent the editor from overwriting the project.godot settings. * Fix Orama-Interactive/Keychain#8
Diffstat (limited to 'game/addons/keychain/Keychain.gd')
-rw-r--r--game/addons/keychain/Keychain.gd212
1 files changed, 212 insertions, 0 deletions
diff --git a/game/addons/keychain/Keychain.gd b/game/addons/keychain/Keychain.gd
new file mode 100644
index 0000000..ff939f3
--- /dev/null
+++ b/game/addons/keychain/Keychain.gd
@@ -0,0 +1,212 @@
+extends Node
+
+const TRANSLATIONS_PATH := "res://addons/keychain/translations"
+const PROFILES_PATH := "user://shortcut_profiles"
+
+# Change these settings
+var profiles := [preload("profiles/default.tres")]
+var selected_profile: ShortcutProfile = profiles[0]
+var profile_index := 0
+# Syntax: "action_name": InputAction.new("Action Display Name", "Group", true)
+# Note that "action_name" must already exist in the Project's Input Map.
+var actions := {}
+# Syntax: "Group Name": InputGroup.new("Parent Group Name")
+var groups := {}
+var ignore_actions := []
+var ignore_ui_actions := true
+var changeable_types := [true, true, true, true]
+var multiple_menu_accelerators := false
+var config_path := "user://cache.ini"
+var config_file: ConfigFile
+
+
+class InputAction:
+ var display_name := ""
+ var group := ""
+ var global := true
+
+ func _init(_display_name := "",_group := "",_global := true):
+ display_name = _display_name
+ group = _group
+ global = _global
+
+ func update_node(_action: String) -> void:
+ pass
+
+ func handle_input(_event: InputEvent, _action: String) -> bool:
+ return false
+
+
+# This class is useful for the accelerators of PopupMenu items
+# It's possible for PopupMenu items to have multiple shortcuts by using
+# set_item_shortcut(), but we have no control over the accelerator text that appears.
+# Thus, we are stuck with using accelerators instead of shortcuts.
+# If Godot ever receives the ability to change the accelerator text of the items,
+# we could in theory remove this class.
+# If you don't care about PopupMenus in the same scene as ShortcutEdit
+# such as projects like Pixelorama where everything is in the same scene,
+# then you can ignore this class.
+class MenuInputAction:
+ extends InputAction
+ var node_path := ""
+ var node: PopupMenu
+ var menu_item_id := 0
+ var echo := false
+
+ func _init(
+ _display_name := "",
+ _group := "",
+ _global := true,
+ _node_path := "",
+ _menu_item_id := 0,
+ _echo := false
+ ) -> void:
+ super._init(_display_name, _group, _global)
+ node_path = _node_path
+ menu_item_id = _menu_item_id
+ echo = _echo
+
+ func get_node(root: Node) -> void:
+ var temp_node = root.get_node(node_path)
+ if temp_node is PopupMenu:
+ node = node
+ elif temp_node is MenuButton:
+ node = temp_node.get_popup()
+
+ func update_node(action: String) -> void:
+ if !node:
+ return
+ var first_key: InputEventKey = Keychain.action_get_first_key(action)
+ var accel := first_key.get_keycode_with_modifiers() if first_key else 0
+ node.set_item_accelerator(menu_item_id, accel)
+
+ func handle_input(event: InputEvent, action: String) -> bool:
+ if not node:
+ return false
+ if event.is_action_pressed(action, false, true):
+ if event is InputEventKey:
+ var acc: int = node.get_item_accelerator(menu_item_id)
+ # If the event is the same as the menu item's accelerator, skip
+ if acc == event.get_keycode_with_modifiers():
+ return true
+ node.emit_signal("id_pressed", menu_item_id)
+ return true
+ if event.is_action(action, true) and echo:
+ if event.is_echo():
+ node.emit_signal("id_pressed", menu_item_id)
+ return true
+
+ return false
+
+
+class InputGroup:
+ var parent_group := ""
+ var folded := true
+ var tree_item: TreeItem
+
+ func _init(_parent_group := "", _folded := true) -> void:
+ parent_group = _parent_group
+ folded = _folded
+
+
+func _ready() -> void:
+ if !config_file:
+ config_file = ConfigFile.new()
+ if !config_path.is_empty():
+ config_file.load(config_path)
+
+ set_process_input(multiple_menu_accelerators)
+
+ # Load shortcut profiles
+ DirAccess.make_dir_recursive_absolute(PROFILES_PATH)
+ var profile_dir := DirAccess.open(PROFILES_PATH)
+ profile_dir.make_dir(PROFILES_PATH)
+ profile_dir.list_dir_begin() # TODOGODOT4 fill missing arguments https://github.com/godotengine/godot/pull/40547
+ var file_name = profile_dir.get_next()
+ while file_name != "":
+ if !profile_dir.current_is_dir():
+ if file_name.get_extension() == "tres":
+ var file = load(PROFILES_PATH.path_join(file_name))
+ if file is ShortcutProfile:
+ profiles.append(file)
+ file_name = profile_dir.get_next()
+
+ # If there are no profiles besides the default, create one custom
+ if profiles.size() == 1:
+ var profile := ShortcutProfile.new()
+ profile.name = "Custom"
+ profile.resource_path = PROFILES_PATH.path_join("custom.tres")
+ var saved := profile.save()
+ if saved:
+ profiles.append(profile)
+
+ for profile in profiles:
+ profile.fill_bindings()
+
+ var l18n_dir := DirAccess.open(TRANSLATIONS_PATH)
+ l18n_dir.list_dir_begin() # TODOGODOT4 fill missing arguments https://github.com/godotengine/godot/pull/40547
+ file_name = l18n_dir.get_next()
+ while file_name != "":
+ if !l18n_dir.current_is_dir():
+ if file_name.get_extension() == "po":
+ var t: Translation = load(TRANSLATIONS_PATH.path_join(file_name))
+ TranslationServer.add_translation(t)
+ file_name = l18n_dir.get_next()
+
+ profile_index = config_file.get_value("shortcuts", "shortcuts_profile", 0)
+ change_profile(profile_index)
+
+ for action in actions:
+ var input_action: InputAction = actions[action]
+ if input_action is MenuInputAction:
+ input_action.get_node(get_tree().current_scene)
+
+
+func _input(event: InputEvent) -> void:
+ if event is InputEventMouseMotion:
+ return
+
+ for action in actions:
+ var input_action: InputAction = actions[action]
+ var done: bool = input_action.handle_input(event, action)
+ if done:
+ return
+
+
+func change_profile(index: int) -> void:
+ if index >= profiles.size():
+ index = profiles.size() - 1
+ profile_index = index
+ selected_profile = profiles[index]
+ for action in selected_profile.bindings:
+ action_erase_events(action)
+ for event in selected_profile.bindings[action]:
+ action_add_event(action, event)
+
+
+func action_add_event(action: String, event: InputEvent) -> void:
+ InputMap.action_add_event(action, event)
+ if action in actions:
+ actions[action].update_node(action)
+
+
+func action_erase_event(action: String, event: InputEvent) -> void:
+ InputMap.action_erase_event(action, event)
+ if action in actions:
+ actions[action].update_node(action)
+
+
+func action_erase_events(action: String) -> void:
+ InputMap.action_erase_events(action)
+ if action in actions:
+ actions[action].update_node(action)
+
+
+func action_get_first_key(action: String) -> InputEventKey:
+ var first_key: InputEventKey = null
+ var events := InputMap.action_get_events(action)
+ for event in events:
+ if event is InputEventKey:
+ first_key = event
+ break
+ return first_key