diff options
-rw-r--r-- | game/addons/keychain/Keychain.gd | 174 | ||||
-rw-r--r-- | game/addons/keychain/ShortcutEdit.gd | 44 | ||||
-rw-r--r-- | game/addons/keychain/ShortcutEdit.tscn | 12 | ||||
-rw-r--r-- | game/addons/keychain/ShortcutSelectorDialog.gd | 101 | ||||
-rw-r--r-- | game/addons/keychain/ShortcutSelectorDialog.tscn | 49 |
5 files changed, 177 insertions, 203 deletions
diff --git a/game/addons/keychain/Keychain.gd b/game/addons/keychain/Keychain.gd index ff939f3..2288107 100644 --- a/game/addons/keychain/Keychain.gd +++ b/game/addons/keychain/Keychain.gd @@ -1,22 +1,29 @@ 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. +## [Array] of [ShortcutProfile]s. +var profiles: Array[ShortcutProfile] = [preload("profiles/default.tres")] +var selected_profile := profiles[0] ## The currently selected [ShortcutProfile]. +var profile_index := 0 ## The index of the currently selected [ShortcutProfile]. +## [Dictionary] of [String] and [InputAction]. +## 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") +## [Dictionary] of [String] and [InputGroup]. +## Syntax: "Group Name": InputGroup.new("Parent Group Name") var groups := {} -var ignore_actions := [] +var ignore_actions: Array[StringName] = [] ## [Array] of [StringName] input map actions to ignore. +## If [code]true[/code], ignore Godot's default "ui_" input map actions. var ignore_ui_actions := true -var changeable_types := [true, true, true, true] -var multiple_menu_accelerators := false +## A [PackedByteArray] of [bool]s with a fixed length of 4. Used for developers to allow or +## forbid setting certain types of InputEvents. The first element is for [InputEventKey]s, +## the second for [InputEventMouseButton]s, the third for [InputEventJoypadButton]s +## and the fourth for [InputEventJoypadMotion]s. +var changeable_types: PackedByteArray = [true, true, true, true] +## The file path of the [code]config_file[/code]. var config_path := "user://cache.ini" +## Used to store the settings to the filesystem. var config_file: ConfigFile @@ -25,79 +32,11 @@ class InputAction: var group := "" var global := true - func _init(_display_name := "",_group := "",_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 := "" @@ -115,13 +54,10 @@ func _ready() -> void: 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 + profile_dir.list_dir_begin() var file_name = profile_dir.get_next() while file_name != "": if !profile_dir.current_is_dir(): @@ -143,35 +79,9 @@ func _ready() -> void: 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(): @@ -184,29 +94,35 @@ func change_profile(index: int) -> void: action_add_event(action, event) -func action_add_event(action: String, event: InputEvent) -> void: +func action_add_event(action: StringName, 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: +func action_erase_event(action: StringName, 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: +func action_erase_events(action: StringName) -> 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 + + +func load_translation(locale: String) -> void: + var translation = load("res://addons/keychain/translations".path_join(locale + ".po")) + if is_instance_valid(translation) and translation is Translation: + TranslationServer.add_translation(translation) + + +## Converts a [param text] with snake case to a more readable format, by replacing +## underscores with spaces. If [param capitalize_first_letter] is [code]true[/code], +## the first letter of the text is capitalized. +## E.g, "snake_case" would be converted to "Snake case" if +## [param capitalize_first_letter] is [code]true[/code], else it would be converted to +## "snake case". +func humanize_snake_case(text: String, capitalize_first_letter := true) -> String: + text = text.replace("_", " ") + if capitalize_first_letter: + var first_letter := text.left(1) + first_letter = first_letter.capitalize() + text = text.right(-1) + text = text.insert(0, first_letter) + return text diff --git a/game/addons/keychain/ShortcutEdit.gd b/game/addons/keychain/ShortcutEdit.gd index fe4e69f..207ddec 100644 --- a/game/addons/keychain/ShortcutEdit.gd +++ b/game/addons/keychain/ShortcutEdit.gd @@ -2,7 +2,7 @@ extends Control enum { KEYBOARD, MOUSE, JOY_BUTTON, JOY_AXIS } -const MOUSE_BUTTON_NAMES := [ +const MOUSE_BUTTON_NAMES: PackedStringArray = [ "Left Button", "Right Button", "Middle Button", @@ -14,7 +14,7 @@ const MOUSE_BUTTON_NAMES := [ "X Button 2", ] -const JOY_BUTTON_NAMES := [ +const JOY_BUTTON_NAMES: PackedStringArray = [ "DualShock Cross, Xbox A, Nintendo B", "DualShock Circle, Xbox B, Nintendo A", "DualShock Square, Xbox X, Nintendo Y", @@ -40,7 +40,7 @@ const JOY_BUTTON_NAMES := [ "PS4/5 Touchpad", ] -const JOY_AXIS_NAMES := [ +const JOY_AXIS_NAMES: PackedStringArray = [ "(Left Stick Left)", "(Left Stick Right)", "(Left Stick Up)", @@ -107,7 +107,7 @@ func _ready() -> void: profile_option_button.select(Keychain.profile_index) _on_ProfileOptionButton_item_selected(Keychain.profile_index) - if OS.get_name() == "HTML5": + if OS.get_name() == "Web": $VBoxContainer/HBoxContainer/OpenProfileFolder.queue_free() @@ -151,8 +151,6 @@ func _construct_tree() -> void: func _fill_selector_options() -> void: - keyboard_shortcut_selector.entered_shortcut.visible = true - keyboard_shortcut_selector.option_button.visible = false mouse_shortcut_selector.input_type_l.text = "Mouse Button Index:" joy_key_shortcut_selector.input_type_l.text = "Joypad Button Index:" joy_axis_shortcut_selector.input_type_l.text = "Joypad Axis Index:" @@ -171,8 +169,8 @@ func _fill_selector_options() -> void: var joy_axis_option_button: OptionButton = joy_axis_shortcut_selector.option_button var i := 0.0 for option in JOY_AXIS_NAMES: - var sign_symbol = "+" if floor(i) != i else "-" - var text: String = tr("Axis") + " %s %s %s" % [floor(i), sign_symbol, tr(option)] + var sign_symbol := "+" if floori(i) != i else "-" + var text: String = tr("Axis") + " %s %s %s" % [floori(i), sign_symbol, tr(option)] joy_axis_option_button.add_item(text) i += 0.5 @@ -201,19 +199,10 @@ func get_action_name(action: String) -> String: display_name = Keychain.actions[action].display_name if display_name.is_empty(): - display_name = _humanize_snake_case(action) + display_name = Keychain.humanize_snake_case(action) return display_name -func _humanize_snake_case(text: String) -> String: - text = text.replace("_", " ") - var first_letter := text.left(1) - first_letter = first_letter.capitalize() - text = text.right(-1) - text = text.insert(0, first_letter) - return text - - func add_event_tree_item(event: InputEvent, action_tree_item: TreeItem) -> void: var event_class := event.get_class() match event_class: @@ -252,21 +241,10 @@ func add_event_tree_item(event: InputEvent, action_tree_item: TreeItem) -> void: func event_to_str(event: InputEvent) -> String: - var output := "" - if event is InputEventKey: - var scancode: int = 0 - if event.keycode != 0: - scancode = event.get_keycode_with_modifiers() - var physical_str := "" - if scancode == 0: - scancode = event.get_physical_keycode_with_modifiers() - physical_str = " " + tr("(Physical)") - output = OS.get_keycode_string(scancode) + physical_str - - elif event is InputEventMouseButton: - output = tr(MOUSE_BUTTON_NAMES[event.button_index - 1]) - - elif event is InputEventJoypadButton: + var output := event.as_text() + # event.as_text() could be used for these event types as well, but this gives more control + # to the developer as to what strings will be printed + if event is InputEventJoypadButton: var button_index: int = event.button_index output = tr("Button") if button_index >= JOY_BUTTON_NAMES.size(): diff --git a/game/addons/keychain/ShortcutEdit.tscn b/game/addons/keychain/ShortcutEdit.tscn index daeb0b8..57bfd46 100644 --- a/game/addons/keychain/ShortcutEdit.tscn +++ b/game/addons/keychain/ShortcutEdit.tscn @@ -7,7 +7,8 @@ [ext_resource type="Texture2D" uid="uid://bma7xj2rqqcr8" path="res://addons/keychain/assets/mouse.svg" id="5"] [ext_resource type="PackedScene" uid="uid://bfjcafe2kvx7n" path="res://addons/keychain/ShortcutSelectorDialog.tscn" id="6"] -[node name="ShortcutEdit" type="VBoxContainer"] +[node name="ShortcutEdit" type="Control"] +layout_mode = 3 anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 @@ -17,7 +18,12 @@ size_flags_vertical = 3 script = ExtResource("1") [node name="VBoxContainer" type="VBoxContainer" parent="."] -layout_mode = 2 +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 size_flags_vertical = 3 [node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"] @@ -94,8 +100,6 @@ offset_left = 8.0 offset_top = 8.0 offset_right = 192.0 offset_bottom = 51.0 -caret_blink = true -caret_blink_interval = 0.5 [node name="DeleteConfirmation" type="ConfirmationDialog" parent="."] size = Vector2i(427, 100) diff --git a/game/addons/keychain/ShortcutSelectorDialog.gd b/game/addons/keychain/ShortcutSelectorDialog.gd index 38cb555..fb28434 100644 --- a/game/addons/keychain/ShortcutSelectorDialog.gd +++ b/game/addons/keychain/ShortcutSelectorDialog.gd @@ -2,27 +2,38 @@ extends ConfirmationDialog enum InputTypes { KEYBOARD, MOUSE, JOY_BUTTON, JOY_AXIS } -@export var input_type: InputTypes = InputTypes.KEYBOARD +@export var input_type := InputTypes.KEYBOARD var listened_input: InputEvent -@onready var root: Node = get_parent() -@onready var input_type_l: Label = $VBoxContainer/InputTypeLabel -@onready var entered_shortcut: LineEdit = $VBoxContainer/EnteredShortcut -@onready var option_button: OptionButton = $VBoxContainer/OptionButton -@onready var already_exists: Label = $VBoxContainer/AlreadyExistsLabel +@onready var root := get_parent() +@onready var input_type_l := $VBoxContainer/InputTypeLabel as Label +@onready var entered_shortcut := $VBoxContainer/EnteredShortcut as LineEdit +@onready var option_button := $VBoxContainer/OptionButton as OptionButton +@onready var modifier_buttons := $VBoxContainer/ModifierButtons as HBoxContainer +@onready var alt_button := $VBoxContainer/ModifierButtons/Alt as CheckBox +@onready var shift_button := $VBoxContainer/ModifierButtons/Shift as CheckBox +@onready var control_button := $VBoxContainer/ModifierButtons/Control as CheckBox +@onready var meta_button := $VBoxContainer/ModifierButtons/Meta as CheckBox +@onready var command_control_button := $VBoxContainer/ModifierButtons/CommandOrControl as CheckBox +@onready var already_exists := $VBoxContainer/AlreadyExistsLabel as Label func _ready() -> void: set_process_input(false) if input_type == InputTypes.KEYBOARD: + entered_shortcut.visible = true + option_button.visible = false get_ok_button().focus_neighbor_top = entered_shortcut.get_path() get_cancel_button().focus_neighbor_top = entered_shortcut.get_path() entered_shortcut.focus_neighbor_bottom = get_ok_button().get_path() else: + if input_type != InputTypes.MOUSE: + modifier_buttons.visible = false get_ok_button().focus_neighbor_top = option_button.get_path() get_cancel_button().focus_neighbor_top = option_button.get_path() option_button.focus_neighbor_bottom = get_ok_button().get_path() + # get_close_button().focus_mode = Control.FOCUS_NONE @@ -31,7 +42,8 @@ func _input(event: InputEvent) -> void: return if event.pressed: listened_input = event - entered_shortcut.text = OS.get_keycode_string(event.get_keycode_with_modifiers()) + _set_modifier_buttons_state(listened_input) + entered_shortcut.text = event.as_text() _show_assigned_state(event) @@ -101,27 +113,13 @@ func _set_shortcut(action: StringName, old_event: InputEvent, new_event: InputEv tree_item.free() break - tree_item = _get_next_tree_item(tree_item) + tree_item = tree_item.get_next_in_tree() Keychain.action_add_event(action, new_event) Keychain.selected_profile.change_action(action) return true -# Based on https://github.com/godotengine/godot/blob/master/scene/gui/tree.cpp#L685 -func _get_next_tree_item(current: TreeItem) -> TreeItem: - if current.get_first_child(): - current = current.get_first_child() - elif current.get_next(): - current = current.get_next() - else: - while current and !current.get_next(): - current = current.get_parent() - if current: - current = current.get_next() - return current - - func _find_matching_event_in_map(action: StringName, event: InputEvent) -> Array: var group := "" if action in Keychain.actions: @@ -148,23 +146,26 @@ func _find_matching_event_in_map(action: StringName, event: InputEvent) -> Array func _on_ShortcutSelectorDialog_about_to_show() -> void: + var metadata = root.currently_editing_tree_item.get_metadata(0) if input_type == InputTypes.KEYBOARD: listened_input = null already_exists.text = "" entered_shortcut.text = "" + if metadata is InputEvent: + _set_modifier_buttons_state(metadata) await get_tree().process_frame entered_shortcut.grab_focus() else: - var metadata = root.currently_editing_tree_item.get_metadata(0) if metadata is InputEvent: # Editing an input event var index := 0 if metadata is InputEventMouseButton: index = metadata.button_index - 1 + _set_modifier_buttons_state(metadata) elif metadata is InputEventJoypadButton: index = metadata.button_index elif metadata is InputEventJoypadMotion: index = metadata.axis * 2 - index += round(metadata.axis_value) / 2.0 + 0.5 + index += signi(metadata.axis_value) / 2.0 + 0.5 option_button.select(index) _on_OptionButton_item_selected(index) @@ -181,6 +182,11 @@ func _on_OptionButton_item_selected(index: int) -> void: if input_type == InputTypes.MOUSE: listened_input = InputEventMouseButton.new() listened_input.button_index = index + 1 + listened_input.alt_pressed = alt_button.button_pressed + listened_input.shift_pressed = shift_button.button_pressed + listened_input.ctrl_pressed = control_button.button_pressed + listened_input.meta_pressed = meta_button.button_pressed + listened_input.command_or_control_autoremap = command_control_button.button_pressed elif input_type == InputTypes.JOY_BUTTON: listened_input = InputEventJoypadButton.new() listened_input.button_index = index @@ -197,3 +203,50 @@ func _on_EnteredShortcut_focus_entered() -> void: func _on_EnteredShortcut_focus_exited() -> void: set_process_input(false) + + +func _on_alt_toggled(button_pressed: bool) -> void: + if not is_instance_valid(listened_input): + return + listened_input.alt_pressed = button_pressed + entered_shortcut.text = listened_input.as_text() + + +func _set_modifier_buttons_state(event: InputEventWithModifiers) -> void: + alt_button.button_pressed = event.alt_pressed + shift_button.button_pressed = event.shift_pressed + control_button.button_pressed = event.ctrl_pressed + meta_button.button_pressed = event.meta_pressed + command_control_button.button_pressed = event.command_or_control_autoremap + + +func _on_shift_toggled(button_pressed: bool) -> void: + if not is_instance_valid(listened_input): + return + listened_input.shift_pressed = button_pressed + entered_shortcut.text = listened_input.as_text() + + +func _on_control_toggled(button_pressed: bool) -> void: + if not is_instance_valid(listened_input): + return + listened_input.ctrl_pressed = button_pressed + entered_shortcut.text = listened_input.as_text() + + +func _on_meta_toggled(button_pressed: bool) -> void: + if not is_instance_valid(listened_input): + return + listened_input.meta_pressed = button_pressed + entered_shortcut.text = listened_input.as_text() + + +func _on_command_or_control_toggled(button_pressed: bool) -> void: + control_button.button_pressed = false + meta_button.button_pressed = false + control_button.visible = not button_pressed + meta_button.visible = not button_pressed + if not is_instance_valid(listened_input): + return + listened_input.command_or_control_autoremap = button_pressed + entered_shortcut.text = listened_input.as_text() diff --git a/game/addons/keychain/ShortcutSelectorDialog.tscn b/game/addons/keychain/ShortcutSelectorDialog.tscn index d8f528f..65250e3 100644 --- a/game/addons/keychain/ShortcutSelectorDialog.tscn +++ b/game/addons/keychain/ShortcutSelectorDialog.tscn @@ -3,44 +3,67 @@ [ext_resource type="Script" path="res://addons/keychain/ShortcutSelectorDialog.gd" id="1"] [node name="ShortcutSelectorDialog" type="ConfirmationDialog"] +size = Vector2i(417, 169) script = ExtResource("1") [node name="VBoxContainer" type="VBoxContainer" parent="."] offset_left = 8.0 offset_top = 8.0 -offset_right = 341.0 -offset_bottom = 64.0 +offset_right = 409.0 +offset_bottom = 120.0 [node name="InputTypeLabel" type="Label" parent="VBoxContainer"] layout_mode = 2 -offset_right = 333.0 -offset_bottom = 14.0 text = "Press a key or a key combination to set the shortcut" [node name="EnteredShortcut" type="LineEdit" parent="VBoxContainer"] visible = false layout_mode = 2 -offset_top = 18.0 -offset_right = 333.0 -offset_bottom = 32.0 editable = false virtual_keyboard_enabled = false [node name="OptionButton" type="OptionButton" parent="VBoxContainer"] layout_mode = 2 -offset_top = 18.0 -offset_right = 333.0 -offset_bottom = 38.0 mouse_default_cursor_shape = 2 +[node name="ModifierButtons" type="HBoxContainer" parent="VBoxContainer"] +layout_mode = 2 + +[node name="Alt" type="CheckBox" parent="VBoxContainer/ModifierButtons"] +layout_mode = 2 +mouse_default_cursor_shape = 2 +text = "Alt" + +[node name="Shift" type="CheckBox" parent="VBoxContainer/ModifierButtons"] +layout_mode = 2 +mouse_default_cursor_shape = 2 +text = "Shift" + +[node name="Control" type="CheckBox" parent="VBoxContainer/ModifierButtons"] +layout_mode = 2 +mouse_default_cursor_shape = 2 +text = "Control" + +[node name="Meta" type="CheckBox" parent="VBoxContainer/ModifierButtons"] +layout_mode = 2 +mouse_default_cursor_shape = 2 +text = "Meta" + +[node name="CommandOrControl" type="CheckBox" parent="VBoxContainer/ModifierButtons"] +layout_mode = 2 +mouse_default_cursor_shape = 2 +text = "Command / Control (auto)" + [node name="AlreadyExistsLabel" type="Label" parent="VBoxContainer"] layout_mode = 2 -offset_top = 42.0 -offset_right = 333.0 -offset_bottom = 56.0 [connection signal="about_to_popup" from="." to="." method="_on_ShortcutSelectorDialog_about_to_show"] [connection signal="confirmed" from="." to="." method="_on_ShortcutSelectorDialog_confirmed"] [connection signal="focus_entered" from="VBoxContainer/EnteredShortcut" to="." method="_on_EnteredShortcut_focus_entered"] [connection signal="focus_exited" from="VBoxContainer/EnteredShortcut" to="." method="_on_EnteredShortcut_focus_exited"] [connection signal="item_selected" from="VBoxContainer/OptionButton" to="." method="_on_OptionButton_item_selected"] +[connection signal="toggled" from="VBoxContainer/ModifierButtons/Alt" to="." method="_on_alt_toggled"] +[connection signal="toggled" from="VBoxContainer/ModifierButtons/Shift" to="." method="_on_shift_toggled"] +[connection signal="toggled" from="VBoxContainer/ModifierButtons/Control" to="." method="_on_control_toggled"] +[connection signal="toggled" from="VBoxContainer/ModifierButtons/Meta" to="." method="_on_meta_toggled"] +[connection signal="toggled" from="VBoxContainer/ModifierButtons/CommandOrControl" to="." method="_on_command_or_control_toggled"] |