aboutsummaryrefslogtreecommitdiff
path: root/game/addons/keychain/Keychain.gd
blob: ff939f332a61339f72f2decd7d2a1290a8cc1a4d (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
200
201
202
203
204
205
206
207
208
209
210
211
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