aboutsummaryrefslogtreecommitdiff
path: root/game/addons/keychain/ShortcutSelectorDialog.gd
blob: 38cb5550cf16e3c498f65553bc4e964bdd24337b (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
extends ConfirmationDialog

enum InputTypes { KEYBOARD, MOUSE, JOY_BUTTON, JOY_AXIS }

@export var input_type: InputTypes = 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


func _ready() -> void:
   set_process_input(false)
   if input_type == InputTypes.KEYBOARD:
      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:
      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


func _input(event: InputEvent) -> void:
   if not event is InputEventKey:
      return
   if event.pressed:
      listened_input = event
      entered_shortcut.text = OS.get_keycode_string(event.get_keycode_with_modifiers())
      _show_assigned_state(event)


func _show_assigned_state(event: InputEvent) -> void:
   var metadata = root.currently_editing_tree_item.get_metadata(0)
   var action := ""
   if metadata is InputEvent:  # Editing an input event
      action = root.currently_editing_tree_item.get_parent().get_metadata(0)
   elif metadata is StringName:  # Adding a new input event to an action
      action = metadata

   var matching_pair: Array = _find_matching_event_in_map(action, event)
   if matching_pair:
      already_exists.text = tr("Already assigned to: %s") % root.get_action_name(matching_pair[0])
   else:
      already_exists.text = ""


func _on_ShortcutSelectorDialog_confirmed() -> void:
   if listened_input == null:
      return
   _apply_shortcut_change(listened_input)


func _apply_shortcut_change(input_event: InputEvent) -> void:
   var metadata = root.currently_editing_tree_item.get_metadata(0)
   if metadata is InputEvent:  # Editing an input event
      var parent_metadata = root.currently_editing_tree_item.get_parent().get_metadata(0)
      var changed: bool = _set_shortcut(parent_metadata, metadata, input_event)
      if !changed:
         return
      root.currently_editing_tree_item.set_metadata(0, input_event)
      root.currently_editing_tree_item.set_text(0, root.event_to_str(input_event))
   elif metadata is StringName:  # Adding a new input event to an action
      var changed: bool = _set_shortcut(metadata, null, input_event)
      if !changed:
         return
      root.add_event_tree_item(input_event, root.currently_editing_tree_item)


func _set_shortcut(action: StringName, old_event: InputEvent, new_event: InputEvent) -> bool:
   if InputMap.action_has_event(action, new_event):  # If the current action already has that event
      return false
   if old_event:
      Keychain.action_erase_event(action, old_event)

   # Loop through other actions to see if the event exists there, to re-assign it
   var matching_pair := _find_matching_event_in_map(action, new_event)

   if matching_pair:
      var group := ""
      if action in Keychain.actions:
         group = Keychain.actions[action].group

      var action_to_replace: StringName = matching_pair[0]
      var input_to_replace: InputEvent = matching_pair[1]
      Keychain.action_erase_event(action_to_replace, input_to_replace)
      Keychain.selected_profile.change_action(action_to_replace)
      var tree_item: TreeItem = root.tree.get_root()
      var prev_tree_item: TreeItem
      while tree_item != null:  # Loop through Tree's TreeItems...
         var metadata = tree_item.get_metadata(0)
         if metadata is InputEvent:
            if input_to_replace.is_match(metadata):
               var map_action: StringName = tree_item.get_parent().get_metadata(0)
               if map_action == action_to_replace:
                  tree_item.free()
                  break

         tree_item = _get_next_tree_item(tree_item)

   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:
      group = Keychain.actions[action].group

   for map_action in InputMap.get_actions():
      if map_action in Keychain.ignore_actions:
         continue
      if Keychain.ignore_ui_actions and (map_action as String).begins_with("ui_"):
         continue
      for map_event in InputMap.action_get_events(map_action):
         if !event.is_match(map_event):
            continue

         if map_action in Keychain.actions:
            # If it's local, check if it's the same group, otherwise ignore
            if !Keychain.actions[action].global or !Keychain.actions[map_action].global:
               if Keychain.actions[map_action].group != group:
                  continue

         return [map_action, map_event]

   return []


func _on_ShortcutSelectorDialog_about_to_show() -> void:
   if input_type == InputTypes.KEYBOARD:
      listened_input = null
      already_exists.text = ""
      entered_shortcut.text = ""
      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
         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
         option_button.select(index)
         _on_OptionButton_item_selected(index)

      elif metadata is StringName:  # Adding a new input event to an action
         option_button.select(0)
         _on_OptionButton_item_selected(0)


func _on_ShortcutSelectorDialog_popup_hide() -> void:
   set_process_input(false)


func _on_OptionButton_item_selected(index: int) -> void:
   if input_type == InputTypes.MOUSE:
      listened_input = InputEventMouseButton.new()
      listened_input.button_index = index + 1
   elif input_type == InputTypes.JOY_BUTTON:
      listened_input = InputEventJoypadButton.new()
      listened_input.button_index = index
   elif input_type == InputTypes.JOY_AXIS:
      listened_input = InputEventJoypadMotion.new()
      listened_input.axis = index / 2
      listened_input.axis_value = -1.0 if index % 2 == 0 else 1.0
   _show_assigned_state(listened_input)


func _on_EnteredShortcut_focus_entered() -> void:
   set_process_input(true)


func _on_EnteredShortcut_focus_exited() -> void:
   set_process_input(false)