rum 0.0.1-universal-darwin-10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (99) hide show
  1. data/CHANGELOG +3 -0
  2. data/README +30 -0
  3. data/Rakefile +134 -0
  4. data/bin/rum-client +124 -0
  5. data/doc/basic.rb +10 -0
  6. data/doc/doc.html +602 -0
  7. data/doc/example.rb +59 -0
  8. data/doc/reference.rb +415 -0
  9. data/doc/resources/bg.png +0 -0
  10. data/doc/resources/bottom.png +0 -0
  11. data/doc/resources/build.rb +235 -0
  12. data/doc/resources/doc.haml +167 -0
  13. data/doc/resources/emacs-auto-completion.png +0 -0
  14. data/doc/resources/flash.png +0 -0
  15. data/doc/resources/highlight.css +94 -0
  16. data/doc/resources/intro.rb +17 -0
  17. data/doc/resources/left.png +0 -0
  18. data/doc/resources/logo.png +0 -0
  19. data/doc/resources/screen.css +420 -0
  20. data/doc/resources/screenshot.png +0 -0
  21. data/doc/resources/top.png +0 -0
  22. data/ext/mac/keyboard_hook/English.lproj/InfoPlist.strings +0 -0
  23. data/ext/mac/keyboard_hook/Event.h +17 -0
  24. data/ext/mac/keyboard_hook/Event.m +18 -0
  25. data/ext/mac/keyboard_hook/EventTap.h +11 -0
  26. data/ext/mac/keyboard_hook/EventTap.m +77 -0
  27. data/ext/mac/keyboard_hook/Info.plist +26 -0
  28. data/ext/mac/keyboard_hook/KeyboardHook.xcodeproj/TemplateIcon.icns +0 -0
  29. data/ext/mac/keyboard_hook/KeyboardHook.xcodeproj/project.pbxproj +323 -0
  30. data/ext/mac/keyboard_hook/KeyboardHook_Prefix.pch +7 -0
  31. data/ext/mac/keyboard_hook/version.plist +16 -0
  32. data/ext/windows/keyboard_hook/extconf.rb +2 -0
  33. data/ext/windows/keyboard_hook/keyboard_hook.c +126 -0
  34. data/ext/windows/system/autohotkey_stuff.c +255 -0
  35. data/ext/windows/system/autohotkey_stuff.h +2 -0
  36. data/ext/windows/system/clipboard_watcher.c +58 -0
  37. data/ext/windows/system/clipboard_watcher.h +2 -0
  38. data/ext/windows/system/extconf.rb +3 -0
  39. data/ext/windows/system/input_box.c +239 -0
  40. data/ext/windows/system/input_box.h +4 -0
  41. data/ext/windows/system/system.c +273 -0
  42. data/lib/rum.rb +4 -0
  43. data/lib/rum/apps.rb +4 -0
  44. data/lib/rum/barrel.rb +157 -0
  45. data/lib/rum/barrel/emacs.rb +44 -0
  46. data/lib/rum/barrel/emacs_client.rb +74 -0
  47. data/lib/rum/core.rb +125 -0
  48. data/lib/rum/dsl.rb +109 -0
  49. data/lib/rum/gui.rb +93 -0
  50. data/lib/rum/help.rb +128 -0
  51. data/lib/rum/hotkey_core.rb +479 -0
  52. data/lib/rum/mac.rb +18 -0
  53. data/lib/rum/mac/app.rb +4 -0
  54. data/lib/rum/mac/apps.rb +19 -0
  55. data/lib/rum/mac/gui.rb +26 -0
  56. data/lib/rum/mac/gui/CocoaDialog.app/Contents/Info.plist +28 -0
  57. data/lib/rum/mac/gui/CocoaDialog.app/Contents/MacOS/CocoaDialog +0 -0
  58. data/lib/rum/mac/gui/CocoaDialog.app/Contents/Resources/Info.plist +28 -0
  59. data/lib/rum/mac/gui/CocoaDialog.app/Contents/Resources/InfoPlist.strings +0 -0
  60. data/lib/rum/mac/gui/CocoaDialog.app/Contents/Resources/Inputbox.nib/classes.nib +51 -0
  61. data/lib/rum/mac/gui/CocoaDialog.app/Contents/Resources/Inputbox.nib/info.nib +16 -0
  62. data/lib/rum/mac/gui/CocoaDialog.app/Contents/Resources/Inputbox.nib/keyedobjects.nib +0 -0
  63. data/lib/rum/mac/gui/CocoaDialog.app/Contents/Resources/MainMenu.nib/classes.nib +7 -0
  64. data/lib/rum/mac/gui/CocoaDialog.app/Contents/Resources/MainMenu.nib/info.nib +21 -0
  65. data/lib/rum/mac/gui/CocoaDialog.app/Contents/Resources/MainMenu.nib/info.nib.orig +21 -0
  66. data/lib/rum/mac/gui/CocoaDialog.app/Contents/Resources/MainMenu.nib/objects.nib +0 -0
  67. data/lib/rum/mac/gui/CocoaDialog.app/Contents/Resources/MainMenu.nib/objects.nib.orig +0 -0
  68. data/lib/rum/mac/gui/CocoaDialog.app/Contents/Resources/Msgbox.nib/classes.nib +27 -0
  69. data/lib/rum/mac/gui/CocoaDialog.app/Contents/Resources/Msgbox.nib/info.nib +16 -0
  70. data/lib/rum/mac/gui/CocoaDialog.app/Contents/Resources/Msgbox.nib/keyedobjects.nib +0 -0
  71. data/lib/rum/mac/gui/Growl.framework/Growl +0 -0
  72. data/lib/rum/mac/gui/Growl.framework/Versions/A/Growl +0 -0
  73. data/lib/rum/mac/gui/Growl.framework/Versions/A/Headers/Growl.h +6 -0
  74. data/lib/rum/mac/gui/Growl.framework/Versions/A/Headers/GrowlApplicationBridge-Carbon.h +780 -0
  75. data/lib/rum/mac/gui/Growl.framework/Versions/A/Headers/GrowlApplicationBridge.h +575 -0
  76. data/lib/rum/mac/gui/Growl.framework/Versions/A/Headers/GrowlDefines.h +348 -0
  77. data/lib/rum/mac/gui/Growl.framework/Versions/A/Resources/Info.plist +24 -0
  78. data/lib/rum/mac/gui/growl.rb +54 -0
  79. data/lib/rum/mac/irb/completion.rb +207 -0
  80. data/lib/rum/mac/keyboard_hook.rb +73 -0
  81. data/lib/rum/mac/keyboard_hook/KeyboardHook.framework/KeyboardHook +0 -0
  82. data/lib/rum/mac/keyboard_hook/KeyboardHook.framework/Versions/A/KeyboardHook +0 -0
  83. data/lib/rum/mac/keyboard_hook/KeyboardHook.framework/Versions/A/Resources/English.lproj/InfoPlist.strings +0 -0
  84. data/lib/rum/mac/keyboard_hook/KeyboardHook.framework/Versions/A/Resources/Info.plist +22 -0
  85. data/lib/rum/mac/layouts.rb +146 -0
  86. data/lib/rum/mac/system.rb +45 -0
  87. data/lib/rum/remote.rb +48 -0
  88. data/lib/rum/server.rb +92 -0
  89. data/lib/rum/windows.rb +23 -0
  90. data/lib/rum/windows/app.rb +72 -0
  91. data/lib/rum/windows/apps.rb +25 -0
  92. data/lib/rum/windows/gui.rb +116 -0
  93. data/lib/rum/windows/keyboard.rb +80 -0
  94. data/lib/rum/windows/keyboard_hook.rb +20 -0
  95. data/lib/rum/windows/layouts.rb +232 -0
  96. data/lib/rum/windows/system.rb +310 -0
  97. data/lib/rum/windows/system_foreign_functions.rb +129 -0
  98. data/rum.gemspec +14 -0
  99. metadata +166 -0
@@ -0,0 +1,93 @@
1
+ module Rum
2
+ module Gui
3
+ extend self
4
+
5
+ def self.use gui_module, *methods
6
+ if methods.empty?
7
+ include gui_module
8
+ extend self
9
+ gui_module.auto_setup if gui_module.respond_to? :auto_setup
10
+ else
11
+ methods.each do |method|
12
+ method = gui_module.method(method)
13
+ define_method(method.name) do |*args, &block|
14
+ method.call(*args, &block)
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ def message text, *args, &callback
21
+ sticky = !!args.delete(:sticky)
22
+ title = args.first
23
+ message_backend(text, title, sticky, callback)
24
+ end
25
+
26
+ def alert text, title=nil
27
+ Rum.switch_worker_thread
28
+ title ||= 'Rum'
29
+ alert_backend(text, title)
30
+ end
31
+
32
+ def read(text='', args=nil)
33
+ Rum.switch_worker_thread
34
+ if text.is_a? Hash
35
+ args = text
36
+ text = nil
37
+ end
38
+ if args
39
+ default = args[:default]
40
+ title = args[:title]
41
+ end
42
+ read_backend(text, (title or 'Rum'), default)
43
+ end
44
+
45
+ def choose prompt=nil, choices
46
+ Rum.switch_worker_thread
47
+ choose_backend(prompt, choices)
48
+ end
49
+
50
+ def browse url
51
+ url = 'http://' + url unless url =~ /^\w+:\/\//
52
+ browse_backend url
53
+ end
54
+
55
+ module CocoaDialog
56
+ private
57
+
58
+ def alert_backend prompt, title
59
+ result = dialog('ok-msgbox', '--text', prompt.to_s, '--title', title.to_s)
60
+ result.first == '1'
61
+ end
62
+
63
+ def message_backend text, title, sticky, callback
64
+ alert text, title
65
+ end
66
+
67
+ def read_backend text, title, default
68
+ text = ['--informative-text', text.to_s] if text
69
+ title = ['--title', title.to_s] if title
70
+ default = ['--text', default.to_s] if default
71
+ result = dialog('standard-inputbox', *text, *default, *title)
72
+ result.shift == '1' ? result.join(' ') : ''
73
+ end
74
+
75
+ def choose_backend prompt, choices
76
+ prompt = ['--text', prompt.to_s] if prompt
77
+ result = dialog('standard-dropdown', '--items', *choices.map(&:to_s), *prompt)
78
+ choices[result[1].to_i] if result.first == '1'
79
+ end
80
+
81
+ def dialog *args
82
+ IO.popen([@@binary, *args]) { |p| p.read }.split
83
+ end
84
+
85
+ def self.setup binary=nil
86
+ unless binary
87
+ raise ArgumentError, 'Please provide a path to the CocoaDialog binary.'
88
+ end
89
+ @@binary = binary
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,128 @@
1
+ module Rum
2
+ def self.reference
3
+ reference = File.join(File.dirname(__FILE__), '..', '..',
4
+ 'doc', 'reference.rb')
5
+ Gui.open_file reference
6
+ end
7
+
8
+ def self.read_key &proc
9
+ KeyReader.start(hotkey_processor, proc)
10
+ end
11
+
12
+ def self.show_hotkey
13
+ read_key { |hotkey| Gui.message "'#{hotkey}' not active" }
14
+ Action.hook = lambda do |action|
15
+ KeyReader.remove_hooks
16
+ action.show_definition or Gui.message "Definition location unkown."
17
+ # Must return true for the key that triggered the action to be
18
+ # retained by the processor.
19
+ true
20
+ end
21
+ end
22
+
23
+ def self.print_next_hotkey
24
+ read_key { |hotkey| Gui.message hotkey }
25
+ end
26
+
27
+ def self.snippet
28
+ Gui.message 'Enter hotkey!'
29
+ read_key do |hotkey|
30
+ snippet = "'#{hotkey}'.do { }"
31
+ Keyboard.type! snippet
32
+ Keyboard.type '(left)(left)'
33
+ end
34
+ end
35
+
36
+ module KeyReader
37
+ extend self
38
+ attr_accessor :pressed_modifiers
39
+
40
+ Hook = proc do
41
+ @pass_key = false
42
+ if @key.modifier?
43
+ if @down
44
+ KeyReader.pressed_modifiers << @key
45
+ elsif seen_key = KeyReader.pressed_modifiers.delete(@key)
46
+ if @key == @last_key
47
+ KeyReader.stop(@key, @pressed_modifiers)
48
+ elsif @pressed_modifiers.values.compact.empty?
49
+ KeyReader.stop
50
+ end
51
+ else
52
+ @pass_key = true
53
+ end
54
+ elsif @down and not @was_executed[@key]
55
+ KeyReader.stop(@key, @pressed_modifiers)
56
+ end
57
+ end
58
+
59
+ def start(hotkey_processor, proc)
60
+ @hotkey_processor = hotkey_processor
61
+ @proc = proc
62
+ @pressed_modifiers = []
63
+ add_hooks
64
+ end
65
+
66
+ def stop key=nil, modifiers=nil
67
+ if key
68
+ modifiers = modifiers.keys.select { |key| modifiers[key] }
69
+ key = Hotkey.new(key, modifiers)
70
+ end
71
+ remove_hooks
72
+ @proc.call(key)
73
+ end
74
+
75
+ def add_hooks
76
+ @hotkey_processor.add_hook Hook
77
+ Action.hook = lambda { |action| } # Ignore Actions.
78
+ end
79
+
80
+ def remove_hooks
81
+ @hotkey_processor.remove_hook Hook
82
+ Action.hook = nil
83
+ end
84
+ end
85
+
86
+ module WindowInfo
87
+ module_function
88
+
89
+ def start
90
+ return if @thread
91
+ register_stop_key
92
+ @stop = false
93
+ @thread = Thread.new do
94
+ new = old = nil
95
+ loop do
96
+ break if @stop
97
+ new = active_window
98
+ if new != old
99
+ Gui.message new.report
100
+ old = new
101
+ end
102
+ sleep 0.1
103
+ end
104
+ Clipboard.set new.report
105
+ Gui.message 'Window Info stopped.'
106
+ @thread = nil
107
+ end
108
+ end
109
+
110
+ StopKey = 'escape'
111
+
112
+ def register_stop_key
113
+ @old_action = StopKey.unregister
114
+ StopKey.do { WindowInfo.stop }
115
+ Gui.message "Press '#{StopKey}' to stop WindowInfo."
116
+ end
117
+
118
+ def stop
119
+ return unless @thread
120
+ @stop = true
121
+ if @old_action
122
+ @old_action.register
123
+ else
124
+ StopKey.unregister
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,479 @@
1
+ module Rum
2
+ class Key
3
+ attr_accessor :name, :id, :aliases, :modifier
4
+
5
+ def initialize name, aliases, id
6
+ @name = name
7
+ @id = id
8
+ @aliases = aliases
9
+ @modifier = false
10
+ end
11
+
12
+ def modifier?
13
+ @modifier
14
+ end
15
+
16
+ def inspect
17
+ "#<Key:#{name}>"
18
+ end
19
+ end
20
+
21
+ class Layout
22
+ attr_accessor :ids, :names, :aliases, :modifiers, :core_modifiers, \
23
+ :action_modifiers, :translations
24
+
25
+ def initialize
26
+ @ids = {}
27
+ @names = {}
28
+ @aliases = {}
29
+ @modifiers = []
30
+ @core_modifiers = {}
31
+ # Windows-specific: Modifiers that trigger actions when pressed and released.
32
+ @action_modifiers = {}
33
+ @translations = {}
34
+ end
35
+
36
+ def keys
37
+ @ids.values.uniq
38
+ end
39
+
40
+ def lookup_id id
41
+ @ids[id]
42
+ end
43
+
44
+ def [] attribute
45
+ @names[attribute] or @aliases[attribute] or @ids[attribute]
46
+ end
47
+
48
+ # Modifiers can have the names of other keys as aliases.
49
+ # This allows for abbreviating 'ctrl shift enter'.do to 'c s enter'.do
50
+ # Lookup the alias first to avoid matching normal keys.
51
+ def modifier_lookup attribute
52
+ if (key = @aliases[attribute] || @names[attribute]) and key.modifier?
53
+ key
54
+ end
55
+ end
56
+
57
+ def add *args, id
58
+ id += 2**9 if args.delete :extended # Windows-specific magic value.
59
+ # An explicit implementation might follow
60
+ name = args.shift
61
+ aliases = args
62
+ key = Key.new(name, aliases, id)
63
+
64
+ @ids[id] = key
65
+ @names[name] = key
66
+ aliases.each { |key_alias| @aliases[key_alias] = key }
67
+ end
68
+
69
+ def alias key, key_alias
70
+ key = self[key]
71
+ @aliases[key_alias] = key
72
+ key.aliases << key_alias
73
+ end
74
+
75
+ def rename from, to
76
+ key = self[from]
77
+ @names.delete from
78
+ key.name = to
79
+ @names[to] = key
80
+ end
81
+
82
+ def remap *from, to
83
+ to = self[to]
84
+ from.each { |key| @ids[self[key].id] = to }
85
+ end
86
+
87
+ def modifier key
88
+ key = self[key]
89
+ key.modifier = true
90
+ @modifiers << key unless @modifiers.include? key
91
+ key
92
+ end
93
+
94
+ def core_modifier key
95
+ @core_modifiers[modifier(key)] = true
96
+ end
97
+
98
+ def action_modifier key
99
+ @action_modifiers[modifier(key)] = true
100
+ end
101
+ end
102
+
103
+ module Layouts
104
+ def self.list
105
+ layouts = methods(nil) - [:default_layout, :core, :basic, :list]
106
+ layouts - Object.methods # Needed for MacRuby, methods(nil) doesn't work here.
107
+ end
108
+ end
109
+
110
+ class Hotkey
111
+ attr_accessor :key, :modifiers, :actions, :direction # todo: unveränderlich machen
112
+
113
+ def initialize key, modifiers=[], fuzzy=false, actions=[]
114
+ @key = key
115
+ @modifiers = modifiers
116
+ @fuzzy = fuzzy
117
+ @actions = actions
118
+ @direction = nil
119
+ end
120
+
121
+ def fuzzy?
122
+ @fuzzy
123
+ end
124
+
125
+ def execute repeated
126
+ @actions.each { |action| return true if action.execute(repeated) }
127
+ false
128
+ end
129
+
130
+ def to_s
131
+ (modifiers.dup << key).map(&:name).join ' '
132
+ end
133
+ end
134
+
135
+ class Action
136
+ attr_accessor :action, :condition, :hotkey, :location
137
+
138
+ @@hook = nil
139
+
140
+ def self.work_queue= queue
141
+ @@work_queue = queue
142
+ end
143
+
144
+ def self.hook= proc
145
+ @@hook = proc
146
+ end
147
+
148
+ def self.hook
149
+ @@hook
150
+ end
151
+
152
+ def initialize(action, condition, repeated, location)
153
+ @action = action
154
+ @condition = condition
155
+ @repeated = repeated
156
+ @location = location
157
+ end
158
+
159
+ def execute repeated
160
+ return false if not @action or repeated and not @repeated
161
+ if not @condition or @condition.call
162
+ if @@hook
163
+ @@hook.call(self)
164
+ else
165
+ @@work_queue.enq @action
166
+ true
167
+ end
168
+ end
169
+ end
170
+
171
+ def show_definition
172
+ @location.show if @location
173
+ end
174
+ end
175
+
176
+ # TODO: Needs refactoring.
177
+ class HotkeySet
178
+ attr_accessor :layout, :hotkeys, :modifiers, :down, :up, \
179
+ :up_fuzzy, :down_fuzzy, :modifier_hotkeys
180
+
181
+ def initialize layout
182
+ @layout = layout
183
+ # All hotkeys that consist entirely of modifiers
184
+ @modifier_hotkeys = {}
185
+ @modifiers = layout.modifiers.sort_by &:id
186
+ # All modifier combinations of all registered hotkeys
187
+ @modifier_combinations = Hash.new(0)
188
+ @up = {}
189
+ @down = {}
190
+ @up_fuzzy = {}
191
+ @down_fuzzy = {}
192
+ end
193
+
194
+ def add_hotkey(string, action, condition, repeated, location)
195
+ action = Action.new(action, condition, repeated, location)
196
+ action.hotkey = hotkey_from_string(string)
197
+ register(action)
198
+ end
199
+
200
+ def remove_hotkey string
201
+ hotkey = hotkey_from_string(string)
202
+ Rum.hotkey_set.unregister_conditionless_action(hotkey)
203
+ end
204
+
205
+ # Implement translations in terms of normal hotkeys.
206
+ # TODO: Extending HotkeyProcessor to natively support translations might be a
207
+ # simpler and more solid approach.
208
+ def add_translation(string, to, condition, location)
209
+ down_hotkey = hotkey_from_string(string)
210
+ up_hotkey = hotkey_from_string(string)
211
+ down_hotkey.direction = :down
212
+ up_hotkey.direction = :up
213
+ to_key = @layout[to]
214
+
215
+ catch_modifier_up = proc do
216
+ if down_hotkey.modifiers.include? @key
217
+ send_key_event(to_key, false)
218
+ remove_hook(catch_modifier_up)
219
+ end
220
+ end
221
+ down_action = Action.new( lambda do
222
+ send_key_event(to_key, true)
223
+ Rum.hotkey_processor.add_hook(catch_modifier_up)
224
+ end,
225
+ condition, true, location)
226
+ up_action = Action.new( lambda do
227
+ send_key_event(to_key, false)
228
+ Rum.hotkey_processor.remove_hook(catch_modifier_up)
229
+ end,
230
+ condition, true, location)
231
+ down_action.hotkey = down_hotkey
232
+ up_action.hotkey = up_hotkey
233
+ register(down_action)
234
+ register(up_action)
235
+ end
236
+
237
+ def hotkey_from_string str
238
+ *modifier_aliases, key_alias = str.split(' ')
239
+ fuzzy = !!modifier_aliases.delete('*')
240
+
241
+ # Lookup modifiers
242
+ modifiers = []
243
+ modifier_aliases.each do |modifier_alias|
244
+ if key = @layout.modifier_lookup(modifier_alias)
245
+ modifiers << key
246
+ else
247
+ raise "Invalid modifier: #{modifier_alias}"
248
+ end
249
+ end
250
+ modifiers = modifiers.sort_by &:id
251
+
252
+ # Lookup key
253
+ key = @layout[key_alias]
254
+ raise "#{key_alias} is no valid key." unless key
255
+
256
+ Hotkey.new(key, modifiers, fuzzy)
257
+ end
258
+
259
+ def register action
260
+ hotkey = register_hotkey(action.hotkey)
261
+ if action.condition
262
+ hotkey.actions.unshift action # put actions with conditions first
263
+ else
264
+ # Only one conditionless action per hotkey
265
+ unless last_action = hotkey.actions.last and last_action.condition
266
+ hotkey.actions.pop
267
+ end
268
+ hotkey.actions << action
269
+ end
270
+ action
271
+ end
272
+
273
+ def unregister action
274
+ if hotkey = action.hotkey
275
+ hotkey.actions.delete(action)
276
+ unregister_hotkey(hotkey) if hotkey.actions.empty?
277
+ action
278
+ end
279
+ end
280
+
281
+ def unregister_conditionless_action hotkey
282
+ if hotkey = lookup_hotkey(hotkey) and action = hotkey.actions.last
283
+ unregister action unless action.condition
284
+ end
285
+ end
286
+
287
+ def get_dict(hotkey)
288
+ if hotkey.fuzzy?
289
+ dict = if hotkey.direction == :up
290
+ @up_fuzzy
291
+ else
292
+ @down_fuzzy
293
+ end
294
+ dict = (dict[hotkey.key] ||= {})
295
+ dict_key = hotkey.modifiers
296
+ else
297
+ dict_key = key_signature(hotkey)
298
+ dict = if (dir = hotkey.direction and dir == :up) or \
299
+ (hotkey.key.modifier? and @modifier_combinations[dict_key] > 0)
300
+ @up
301
+ else
302
+ @down
303
+ end
304
+ end
305
+ [dict, dict_key]
306
+ end
307
+
308
+ def key_signature hotkey
309
+ hotkey.modifiers.dup << hotkey.key
310
+ end
311
+
312
+ def lookup_hotkey hotkey
313
+ dict, dict_key = get_dict(hotkey)
314
+ dict[dict_key]
315
+ end
316
+
317
+ def register_hotkey hotkey
318
+ dict, dict_key = get_dict(hotkey)
319
+ existing_hotkey = dict[dict_key] and return existing_hotkey
320
+
321
+ dict[dict_key] = hotkey
322
+ if hotkey.key.modifier? # Modifier hotkeys aren't allowed to be fuzzy.
323
+ @modifier_hotkeys[dict_key] = hotkey
324
+ else
325
+ register_modifier_combination(hotkey.modifiers)
326
+ end
327
+ hotkey
328
+ end
329
+
330
+ def unregister_hotkey hotkey
331
+ dict, dict_key = get_dict(hotkey)
332
+ if dict.delete(dict_key)
333
+ if hotkey.key.modifier?
334
+ @modifier_hotkeys.delete(hotkey)
335
+ else
336
+ unregister_modifier_combination(hotkey.modifiers)
337
+ end
338
+ end
339
+ end
340
+
341
+ def register_modifier_combination(modifiers)
342
+ maybe_reregister_mod_hotkey(modifiers, 1, 0)
343
+ end
344
+
345
+ def unregister_modifier_combination(modifiers)
346
+ maybe_reregister_mod_hotkey(modifiers, -1, 1)
347
+ end
348
+
349
+ # Example:
350
+ # 1. The modifier hotkey 'ctrl shift' is the only registered hotkey.
351
+ # 2. The hotkey 'ctrl shift a' gets registered.
352
+ # Now the 'ctrl shift' hotkey needs to be re-registered to avoid
353
+ # getting triggered while 'ctrl shift a' is pressed.
354
+ #
355
+ # Vice versa:
356
+ # When all conflicting modifier combinations have been
357
+ # unregistered, a modifier hotkey can be re-registered to trigger
358
+ # instantly.
359
+ #
360
+ # This function keeps track of active modifier_combinations when
361
+ # hotkeys ar added or removed. It re-registers corresponding
362
+ # modifier hotkeys accordingly.
363
+ def maybe_reregister_mod_hotkey(modifiers, change_count, threshold)
364
+ count = @modifier_combinations[modifiers]
365
+ if count == threshold and (mod_hotkey = @modifier_hotkeys[modifiers])
366
+ unregister_hotkey(mod_hotkey)
367
+ @modifier_combinations[modifiers] = count + change_count
368
+ register_hotkey(mod_hotkey)
369
+ else
370
+ @modifier_combinations[modifiers] = count + change_count
371
+ end
372
+ end
373
+
374
+ def lookup(down, signature)
375
+ (down ? @down : @up)[signature]
376
+ end
377
+
378
+ def fuzzy_lookup(down, key, pressed_modifiers)
379
+ if (hotkeys = (down ? @down_fuzzy : @up_fuzzy)[key])
380
+ hotkeys.each do |modifiers, hotkey|
381
+ return hotkey if modifiers.all? { |mod| pressed_modifiers[mod] }
382
+ end
383
+ nil
384
+ end
385
+ end
386
+ end
387
+
388
+ # TODO: Needs refactoring.
389
+ class HotkeyProcessor
390
+ attr_reader :layout, :pressed_modifiers, :hooks
391
+ def initialize hotkey_set
392
+ @hotkey_set = hotkey_set
393
+ @modifiers = @hotkey_set.modifiers
394
+ @layout = @hotkey_set.layout
395
+ @pressed_modifiers = {}
396
+ @was_executed = {}
397
+ @key_signature = nil
398
+ @hooks = []
399
+ @pass_key = true
400
+
401
+ @key = nil
402
+ @last_key = nil
403
+ @down = nil
404
+ @last_event_up = nil
405
+ end
406
+
407
+ def add_hook(hook=nil, &block)
408
+ hook ||= block
409
+ @hooks.unshift hook
410
+ hook
411
+ end
412
+
413
+ # Return removed hook.
414
+ def remove_hook(hook)
415
+ @hooks.delete hook
416
+ end
417
+
418
+ def key_signature key
419
+ @modifiers.select { |modifier| @pressed_modifiers[modifier] } << key
420
+ end
421
+
422
+ def execute(down, repeated)
423
+ (hotkey = @hotkey_set.lookup(down, @key_signature) \
424
+ and hotkey.execute(repeated)) or
425
+ (hotkey = @hotkey_set.fuzzy_lookup(down, @key, @pressed_modifiers) \
426
+ and hotkey.execute(repeated))
427
+ end
428
+
429
+ def process_event event
430
+ puts event
431
+ @key = @layout.lookup_id(event.id)
432
+ @down = event.down?
433
+ unless @key
434
+ puts "Unknown Key\n"
435
+ return true
436
+ end
437
+ modifier = @key.modifier?
438
+
439
+ # Skip generating unnecessary key_signatures
440
+ if modifier and @last_event_up or @key != @last_key
441
+ @key_signature = key_signature(@key)
442
+ end
443
+ @last_event_up = !@down
444
+
445
+ print "[#{@key_signature.map { |key| key.name.capitalize }.join(', ')}]"
446
+ print ' (again)' if @key == @last_key; puts
447
+
448
+ if @down
449
+ repeated = @was_executed[@key]
450
+ eat = @was_executed[@key] = true if execute(true, repeated)
451
+ if repeated
452
+ eat = true
453
+ elsif modifier # if repeated, the modifier has already been added to pressed_modifiers
454
+ @pressed_modifiers[@key] = true
455
+ end
456
+ else #up
457
+ @was_executed[@key] = true if execute(false, false)
458
+ if modifier
459
+ @pressed_modifiers[@key] = nil
460
+ if @layout.action_modifiers[@key] and \
461
+ (!@pass_key or @was_executed[@key])
462
+ inhibit_modifier_action
463
+ end
464
+ end
465
+ eat = @was_executed.delete(@key)
466
+ end
467
+
468
+ @pass_key = if modifier
469
+ @layout.core_modifiers[@key]
470
+ else
471
+ !eat
472
+ end
473
+ @hooks.dup.each { |hook| instance_eval &hook }
474
+ @last_key = @key
475
+ puts
476
+ @pass_key
477
+ end
478
+ end
479
+ end