AXElements 0.6.0beta2 → 0.7.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (107) hide show
  1. data/.yardopts +1 -2
  2. data/README.markdown +152 -88
  3. data/Rakefile +8 -103
  4. data/docs/Debugging.markdown +9 -2
  5. data/docs/KeyboardEvents.markdown +114 -49
  6. data/docs/Setting.markdown +1 -0
  7. data/docs/images/next_version.png +0 -0
  8. data/ext/accessibility/key_coder/extconf.rb +22 -0
  9. data/ext/accessibility/key_coder/key_coder.c +113 -0
  10. data/lib/AXElements.rb +2 -0
  11. data/lib/accessibility/core.rb +897 -0
  12. data/lib/accessibility/debug.rb +168 -0
  13. data/lib/accessibility/dsl.rb +697 -0
  14. data/lib/accessibility/enumerators.rb +104 -0
  15. data/lib/accessibility/errors.rb +32 -0
  16. data/lib/accessibility/factory.rb +153 -0
  17. data/lib/accessibility/graph.rb +150 -0
  18. data/lib/{ax_elements/inspector.rb → accessibility/pp_inspector.rb} +39 -28
  19. data/lib/accessibility/qualifier.rb +158 -0
  20. data/lib/accessibility/string.rb +494 -0
  21. data/lib/accessibility/translator.rb +178 -0
  22. data/lib/accessibility/version.rb +7 -0
  23. data/lib/accessibility.rb +79 -0
  24. data/lib/ax/application.rb +234 -0
  25. data/lib/{ax_elements/elements → ax}/button.rb +2 -0
  26. data/lib/ax/element.rb +518 -0
  27. data/lib/{ax_elements/elements → ax}/radio_button.rb +2 -0
  28. data/lib/ax/row.rb +37 -0
  29. data/lib/{ax_elements/elements → ax}/static_text.rb +2 -0
  30. data/lib/ax/systemwide.rb +86 -0
  31. data/lib/ax_elements/awesome_print.rb +25 -0
  32. data/lib/ax_elements/exception_workaround.rb +8 -0
  33. data/lib/ax_elements/nsarray_compat.rb +64 -0
  34. data/lib/ax_elements/vendor/inflection_data.rb +65 -0
  35. data/lib/ax_elements/vendor/inflections.rb +172 -0
  36. data/lib/ax_elements/vendor/inflector.rb +306 -0
  37. data/lib/ax_elements.rb +14 -25
  38. data/lib/minitest/ax_elements.rb +112 -12
  39. data/lib/mouse.rb +72 -46
  40. data/lib/rspec/expectations/ax_elements.rb +133 -6
  41. data/rakelib/doc.rake +13 -0
  42. data/rakelib/ext.rake +61 -0
  43. data/rakelib/gem.rake +28 -0
  44. data/rakelib/test.rake +53 -0
  45. data/test/helper.rb +11 -97
  46. data/test/integration/accessibility/test_core.rb +18 -0
  47. data/test/integration/accessibility/test_debug.rb +44 -0
  48. data/test/integration/accessibility/test_dsl.rb +225 -0
  49. data/test/integration/accessibility/test_enumerators.rb +122 -0
  50. data/test/integration/accessibility/test_errors.rb +38 -0
  51. data/test/integration/accessibility/test_notifications.rb +22 -0
  52. data/test/integration/accessibility/test_qualifier.rb +148 -0
  53. data/test/integration/ax/test_application.rb +56 -0
  54. data/test/integration/ax/test_element.rb +46 -0
  55. data/test/integration/ax/test_row.rb +23 -0
  56. data/test/integration/ax_elements/test_nsarray_compat.rb +43 -0
  57. data/test/integration/minitest/test_ax_elements.rb +98 -0
  58. data/test/integration/rspec/expectations/test_ax_elements.rb +58 -0
  59. data/test/integration/test_mouse.rb +35 -0
  60. data/test/sanity/accessibility/test_core.rb +553 -0
  61. data/test/sanity/accessibility/test_debug.rb +63 -0
  62. data/test/sanity/accessibility/test_dsl.rb +75 -0
  63. data/test/sanity/accessibility/test_errors.rb +10 -0
  64. data/test/sanity/accessibility/test_factory.rb +88 -0
  65. data/test/sanity/accessibility/test_pp_inspector.rb +110 -0
  66. data/test/sanity/accessibility/test_qualifier.rb +13 -0
  67. data/test/sanity/accessibility/test_string.rb +238 -0
  68. data/test/sanity/accessibility/test_translator.rb +145 -0
  69. data/test/sanity/ax/test_application.rb +90 -0
  70. data/test/sanity/ax/test_element.rb +80 -0
  71. data/test/sanity/ax/test_systemwide.rb +66 -0
  72. data/test/sanity/ax_elements/test_nsarray_compat.rb +16 -0
  73. data/test/sanity/ax_elements/test_nsobject_inspect.rb +11 -0
  74. data/test/sanity/minitest/test_ax_elements.rb +15 -0
  75. data/test/sanity/rspec/expectations/test_ax_elements.rb +12 -0
  76. data/test/sanity/test_ax_elements.rb +10 -0
  77. data/test/sanity/test_mouse.rb +19 -0
  78. metadata +111 -93
  79. data/LICENSE.txt +0 -25
  80. data/ext/key_coder/extconf.rb +0 -6
  81. data/ext/key_coder/key_coder.m +0 -77
  82. data/lib/ax_elements/accessibility/enumerators.rb +0 -104
  83. data/lib/ax_elements/accessibility/graph.rb +0 -118
  84. data/lib/ax_elements/accessibility/language.rb +0 -347
  85. data/lib/ax_elements/accessibility/qualifier.rb +0 -73
  86. data/lib/ax_elements/accessibility.rb +0 -166
  87. data/lib/ax_elements/core.rb +0 -541
  88. data/lib/ax_elements/element.rb +0 -593
  89. data/lib/ax_elements/elements/application.rb +0 -88
  90. data/lib/ax_elements/elements/row.rb +0 -30
  91. data/lib/ax_elements/elements/systemwide.rb +0 -46
  92. data/lib/ax_elements/macruby_extensions.rb +0 -255
  93. data/lib/ax_elements/notification.rb +0 -37
  94. data/lib/ax_elements/version.rb +0 -9
  95. data/test/elements/test_application.rb +0 -72
  96. data/test/elements/test_row.rb +0 -27
  97. data/test/elements/test_systemwide.rb +0 -38
  98. data/test/test_accessibility.rb +0 -127
  99. data/test/test_blankness.rb +0 -26
  100. data/test/test_core.rb +0 -448
  101. data/test/test_element.rb +0 -939
  102. data/test/test_enumerators.rb +0 -81
  103. data/test/test_inspector.rb +0 -130
  104. data/test/test_language.rb +0 -157
  105. data/test/test_macruby_extensions.rb +0 -303
  106. data/test/test_mouse.rb +0 -5
  107. data/test/test_search_semantics.rb +0 -143
@@ -0,0 +1,158 @@
1
+ require 'accessibility/translator'
2
+
3
+ ##
4
+ # Used in searches to answer whether or not a given element meets the
5
+ # expected criteria.
6
+ class Accessibility::Qualifier
7
+
8
+ ##
9
+ # Initialize a qualifier with the kind of object that you want to
10
+ # qualify and a dictionary of filter criteria. You can optionally
11
+ # pass a block if your qualification criteria is too complicated
12
+ # for key/value pairs and the blocks return value will be used to
13
+ # determine if an element qualifies.
14
+ #
15
+ # @example
16
+ #
17
+ # Accessibility::Qualifier.new(:standard_window, title: 'Test')
18
+ # Accessibility::Qualifier.new(:buttons, {})
19
+ # Accessibility::Qualifier.new(:Table, { row: { title: /Price/ } })
20
+ # Accessibility::Qualifier.new(:element) do |element|
21
+ # element.children.size > 5 && NSContainsRect(element.bounds, rect)
22
+ # end
23
+ #
24
+ # @param [#to_s] klass
25
+ # @param [Hash]
26
+ # @yield Optional block that can qualify an element
27
+ def initialize klass, criteria
28
+ @klass = TRANSLATOR.classify(klass)
29
+ @criteria = criteria
30
+ @block = Proc.new if block_given?
31
+ compile criteria
32
+ end
33
+
34
+ ##
35
+ # Whether or not a candidate object matches the criteria given
36
+ # at initialization.
37
+ #
38
+ # @param [AX::Element]
39
+ def qualifies? element
40
+ return false unless the_right_type? element
41
+ return false unless meets_criteria? element
42
+ return true
43
+ end
44
+
45
+ # @return [String]
46
+ def describe
47
+ "#{@klass}#{@criteria.ax_pp}#{@block ? '[✔]' : ''}"
48
+ end
49
+
50
+
51
+ private
52
+
53
+ ##
54
+ # @private
55
+ #
56
+ # Local reference to the {Accessibility::Translator}.
57
+ #
58
+ # @return [Accessibility::Translator]
59
+ TRANSLATOR = Accessibility::Translator.instance
60
+
61
+ ##
62
+ # Take a hash of search filters and generate an optimized search
63
+ #
64
+ # @param [Hash]
65
+ def compile criteria
66
+ @filters = criteria.map do |key, value|
67
+ if value.kind_of? Hash
68
+ [:children, [:subsearch, key, value]]
69
+ elsif key.kind_of? Array
70
+ filter = value.kind_of?(Regexp) ?
71
+ :parameterized_match : :parameterized_equality
72
+ [key.first, [filter, *key, value]]
73
+ else
74
+ filter = value.kind_of?(Regexp) ?
75
+ :match : :equality
76
+ [key, [filter, key, value]]
77
+ end
78
+ end
79
+ @filters << [:role, [:block_check]] if @block
80
+ end
81
+
82
+ ##
83
+ # Checks if a candidate object is of the correct class, respecting
84
+ # that that the class being searched for may not be defined yet.
85
+ #
86
+ # @param [AX::Element]
87
+ def the_right_type? element
88
+ unless @const
89
+ if AX.const_defined? @klass
90
+ @const = AX.const_get @klass
91
+ else
92
+ return false
93
+ end
94
+ end
95
+ return element.kind_of? @const
96
+ end
97
+
98
+ ##
99
+ # Determines if the element meets all the criteria of the filters,
100
+ # spawning sub-searches if necessary.
101
+ #
102
+ # @param [AX::Element]
103
+ def meets_criteria? element
104
+ @filters.all? do |filter|
105
+ if element.respond_to? filter.first
106
+ self.send *filter.last, element
107
+ end
108
+ end
109
+ end
110
+
111
+ def subsearch klass, criteria, element
112
+ !element.search(klass, criteria).blank?
113
+ end
114
+
115
+ def match attr, regexp, element
116
+ element.attribute(attr).match regexp
117
+ end
118
+
119
+ def equality attr, value, element
120
+ element.attribute(attr) == value
121
+ end
122
+
123
+ def parameterized_match attr, param, regexp, element
124
+ element.attribute(attr, for_parameter: param).match regexp
125
+ end
126
+
127
+ def parameterized_equality attr, param, value, element
128
+ element.attribute(attr, for_parameter: param) == value
129
+ end
130
+
131
+ def block_check element
132
+ @block.call element
133
+ end
134
+
135
+ end
136
+
137
+
138
+ ##
139
+ # Extensions to `NSDictionary`.
140
+ class NSDictionary
141
+ ##
142
+ # Format the hash for AXElements pretty printing.
143
+ #
144
+ # @return [String]
145
+ def ax_pp
146
+ return '' if empty?
147
+
148
+ list = map { |k, v|
149
+ case v
150
+ when Hash
151
+ "#{k}#{v.ax_pp}"
152
+ else
153
+ "#{k}: #{v.inspect}"
154
+ end
155
+ }
156
+ "(#{list.join(', ')})"
157
+ end
158
+ end
@@ -0,0 +1,494 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require 'accessibility/version'
4
+ require 'accessibility/key_coder'
5
+
6
+ ##
7
+ # Parses strings of human readable text into a series of events meant to
8
+ # be processed by {Accessibility::Core#post} or {KeyCoder.post_event}.
9
+ #
10
+ # Supports most, if not all, latin keyboard layouts, maybe some
11
+ # international layouts as well. Japanese layouts can be made to work with
12
+ # use of `String#transform`.
13
+ #
14
+ # @example
15
+ #
16
+ # app = AXUIElementCreateApplication(3456)
17
+ # include Accessibility::String
18
+ # app.post keyboard_events_for "Hello, world!\n"
19
+ #
20
+ module Accessibility::String
21
+
22
+ ##
23
+ # Generate keyboard events for the given string. Strings should be in a
24
+ # human readable with a few exceptions. Command key (e.g. control, option,
25
+ # command) should be written in string as they appear in
26
+ # {Accessibility::String::EventGenerator::CUSTOM}.
27
+ #
28
+ # For more details on event generation, read the
29
+ # {file:docs/KeyboardEvents.markdown Keyboard Events} documentation.
30
+ #
31
+ # @param [String]
32
+ # @return [Array<Array(Fixnum,Boolean)>]
33
+ def keyboard_events_for string
34
+ EventGenerator.new(Lexer.new(string).lex).generate
35
+ end
36
+
37
+ ##
38
+ # Tokenizer for strings. This class will take a string and break
39
+ # it up into chunks for the event generator. The structure generated
40
+ # here is an array that contains strings and recursively other arrays
41
+ # of strings and arrays of strings.
42
+ #
43
+ # @example
44
+ #
45
+ # Lexer.new("Hai").lex # => ['H','a','i']
46
+ # Lexer.new("\\CAPSLOCK").lex # => [["\\CAPSLOCK"]]
47
+ # Lexer.new("\\COMMAND+a").lex # => [["\\COMMAND", ['a']]]
48
+ # Lexer.new("One\nTwo").lex # => ['O','n','e',"\n",'T','w','o']
49
+ #
50
+ class Lexer
51
+
52
+ ##
53
+ # Once a string is lexed, this contains the tokenized structure.
54
+ #
55
+ # @return [Array<String,Array<String,...>]
56
+ attr_accessor :tokens
57
+
58
+ # @param [#to_s]
59
+ def initialize string
60
+ @chars = string.to_s
61
+ @tokens = []
62
+ end
63
+
64
+ ##
65
+ # Tokenize the string that the lexer was initialized with and
66
+ # return the sequence of tokens that were lexed.
67
+ #
68
+ # @return [Array<String,Array<String,...>]
69
+ def lex
70
+ length = @chars.length
71
+ @index = 0
72
+ while @index < length
73
+ @tokens << if custom?
74
+ lex_custom
75
+ else
76
+ lex_char
77
+ end
78
+ @index += 1
79
+ end
80
+ @tokens
81
+ end
82
+
83
+
84
+ private
85
+
86
+ ##
87
+ # Is it a real custom escape? Kind of a lie, there is one
88
+ # case it does not handle--they get handled in the generator,
89
+ # but maybe they should be handled here?
90
+ # - An upper case letter or symbol following `"\\"` that is
91
+ # not mapped
92
+ def custom?
93
+ @chars[@index] == CUSTOM_ESCAPE &&
94
+ (next_char = @chars[@index+1]) &&
95
+ next_char == next_char.upcase &&
96
+ next_char != SPACE
97
+ end
98
+
99
+ # @return [Array]
100
+ def lex_custom
101
+ start = @index
102
+ loop do
103
+ char = @chars[@index]
104
+ if char == PLUS
105
+ if @chars[@index-1] == CUSTOM_ESCAPE # \\+ case
106
+ @index += 1
107
+ return custom_subseq start
108
+ else
109
+ tokens = custom_subseq start
110
+ @index += 1
111
+ return tokens << lex_custom
112
+ end
113
+ elsif char == SPACE
114
+ return custom_subseq start
115
+ elsif char == nil
116
+ raise ArgumentError, "Bad escape sequence" if start == @index
117
+ return custom_subseq start
118
+ else
119
+ @index += 1
120
+ end
121
+ end
122
+ end
123
+
124
+ # @return [Array]
125
+ def custom_subseq start
126
+ [@chars[start...@index]]
127
+ end
128
+
129
+ # @return [String]
130
+ def lex_char
131
+ @chars[@index]
132
+ end
133
+
134
+ # @private
135
+ SPACE = " "
136
+ # @private
137
+ PLUS = "+"
138
+ # @private
139
+ CUSTOM_ESCAPE = "\\"
140
+ end
141
+
142
+
143
+ ##
144
+ # Generate a sequence of keyboard events given a sequence of tokens.
145
+ # The token format is defined by the {Lexer} class output; it is best
146
+ # to use that class to generate the tokens.
147
+ #
148
+ # @example
149
+ #
150
+ # # Upper case 'A'
151
+ # EventGenerator.new(["A"]).generate # => [[56,true],[70,true],[70,false],[56,false]]
152
+ #
153
+ # # Press the caps lock button, turn it on
154
+ # EventGenerator.new([["\\CAPS"]]).generate # => [[0x39,true],[0x39,false]]
155
+ #
156
+ # # Hotkey, press and hold command key and then 'a', then release both
157
+ # EventGenerator.new([["\\CMD",["a"]]]).generate # => [[55,true],[70,true],[70,false],[55,false]]
158
+ #
159
+ # # Press the return/enter key
160
+ # EventGenerator.new(["\n"]).generate # => [[10,true],[10,false]]
161
+ #
162
+ class EventGenerator
163
+
164
+ ##
165
+ # Regenerate the portion of the key mapping that is set dynamically
166
+ # based on keyboard layout (e.g. US, Dvorak, etc.).
167
+ #
168
+ # This method should be called whenever the keyboard layout changes.
169
+ # This can be called automatically by registering for a notification
170
+ # in a run looped environment.
171
+ def self.regenerate_dynamic_mapping
172
+ # KeyCoder is declared in the Objective-C extension
173
+ MAPPING.merge! KeyCoder.dynamic_mapping
174
+ # Also add an alias to the mapping
175
+ MAPPING["\n"] = MAPPING["\r"]
176
+ end
177
+
178
+ ##
179
+ # Dynamic mapping of characters to keycodes. The map is generated at
180
+ # startup time in order to support multiple keyboard layouts.
181
+ #
182
+ # @return [Hash{String=>Fixnum}]
183
+ MAPPING = {}
184
+
185
+ # Initialize the table
186
+ regenerate_dynamic_mapping
187
+
188
+ ##
189
+ # @note These mappings are all static and come from `/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/Events.h`
190
+ #
191
+ # Map of custom escape sequences to their hardcoded keycode value.
192
+ #
193
+ # @return [Hash{String=>Fixnum}]
194
+ CUSTOM = {
195
+ "\\FUNCTION" => 0x3F, # Standard Control Keys
196
+ "\\FN" => 0x3F,
197
+ "\\CONTROL" => 0x3B,
198
+ "\\CTRL" => 0x3B,
199
+ "\\OPTION" => 0x3A,
200
+ "\\OPT" => 0x3A,
201
+ "\\ALT" => 0x3A,
202
+ "\\COMMAND" => 0x37,
203
+ "\\CMD" => 0x37,
204
+ "\\LSHIFT" => 0x38,
205
+ "\\SHIFT" => 0x38,
206
+ "\\CAPSLOCK" => 0x39,
207
+ "\\CAPS" => 0x39,
208
+ "\\ROPTION" => 0x3D,
209
+ "\\ROPT" => 0x3D,
210
+ "\\RALT" => 0x3D,
211
+ "\\RCONTROL" => 0x3E,
212
+ "\\RCTRL" => 0x3E,
213
+ "\\RSHIFT" => 0x3C,
214
+ "\\ESCAPE" => 0x35, # Top Row Keys
215
+ "\\ESC" => 0x35,
216
+ "\\VOLUMEUP" => 0x48,
217
+ "\\VOLUP" => 0x48,
218
+ "\\VOLUMEDOWN" => 0x49,
219
+ "\\VOLDOWN" => 0x49,
220
+ "\\MUTE" => 0x4A,
221
+ "\\F1" => 0x7A,
222
+ "\\F2" => 0x78,
223
+ "\\F3" => 0x63,
224
+ "\\F4" => 0x76,
225
+ "\\F5" => 0x60,
226
+ "\\F6" => 0x61,
227
+ "\\F7" => 0x62,
228
+ "\\F8" => 0x64,
229
+ "\\F9" => 0x65,
230
+ "\\F10" => 0x6D,
231
+ "\\F11" => 0x67,
232
+ "\\F12" => 0x6F,
233
+ "\\F13" => 0x69,
234
+ "\\F14" => 0x6B,
235
+ "\\F15" => 0x71,
236
+ "\\F16" => 0x6A,
237
+ "\\F17" => 0x40,
238
+ "\\F18" => 0x4F,
239
+ "\\F19" => 0x50,
240
+ "\\F20" => 0x5A,
241
+ "\\HELP" => 0x72, # Island Keys
242
+ "\\HOME" => 0x73,
243
+ "\\END" => 0x77,
244
+ "\\PAGEUP" => 0x74,
245
+ "\\PAGEDOWN" => 0x79,
246
+ "\\DELETE" => 0x75,
247
+ "\\LEFT" => 0x7B, # Arrow Keys
248
+ "\\<-" => 0x7B,
249
+ "\\RIGHT" => 0x7C,
250
+ "\\->" => 0x7C,
251
+ "\\DOWN" => 0x7D,
252
+ "\\UP" => 0x7E,
253
+ "\\0" => 0x52, # Keypad Keys
254
+ "\\1" => 0x53,
255
+ "\\2" => 0x54,
256
+ "\\3" => 0x55,
257
+ "\\4" => 0x56,
258
+ "\\5" => 0x57,
259
+ "\\6" => 0x58,
260
+ "\\7" => 0x59,
261
+ "\\8" => 0x5B,
262
+ "\\9" => 0x5C,
263
+ "\\DECIMAL" => 0x41,
264
+ "\\." => 0x41,
265
+ "\\PLUS" => 0x45,
266
+ "\\+" => 0x45,
267
+ "\\MULTIPLY" => 0x43,
268
+ "\\*" => 0x43,
269
+ "\\MINUS" => 0x4E,
270
+ "\\-" => 0x4E,
271
+ "\\DIVIDE" => 0x4B,
272
+ "\\/" => 0x4B,
273
+ "\\EQUALS" => 0x51,
274
+ "\\=" => 0x51,
275
+ "\\ENTER" => 0x4C,
276
+ "\\CLEAR" => 0x47,
277
+ }
278
+
279
+ ##
280
+ # Mapping of shifted (characters written when holding shift) characters
281
+ # to keycodes.
282
+ #
283
+ # @return [Hash{String=>Fixnum}]
284
+ SHIFTED = {
285
+ '~' => '`',
286
+ '!' => '1',
287
+ '@' => '2',
288
+ '#' => '3',
289
+ '$' => '4',
290
+ '%' => '5',
291
+ '^' => '6',
292
+ '&' => '7',
293
+ '*' => '8',
294
+ '(' => '9',
295
+ ')' => '0',
296
+ '{' => '[',
297
+ '}' => ']',
298
+ '?' => '/',
299
+ '+' => '=',
300
+ '|' => "\\",
301
+ ':' => ';',
302
+ '_' => '-',
303
+ '"' => "'",
304
+ '<' => ',',
305
+ '>' => '.',
306
+ 'A' => 'a',
307
+ 'B' => 'b',
308
+ 'C' => 'c',
309
+ 'D' => 'd',
310
+ 'E' => 'e',
311
+ 'F' => 'f',
312
+ 'G' => 'g',
313
+ 'H' => 'h',
314
+ 'I' => 'i',
315
+ 'J' => 'j',
316
+ 'K' => 'k',
317
+ 'L' => 'l',
318
+ 'M' => 'm',
319
+ 'N' => 'n',
320
+ 'O' => 'o',
321
+ 'P' => 'p',
322
+ 'Q' => 'q',
323
+ 'R' => 'r',
324
+ 'S' => 's',
325
+ 'T' => 't',
326
+ 'U' => 'u',
327
+ 'V' => 'v',
328
+ 'W' => 'w',
329
+ 'X' => 'x',
330
+ 'Y' => 'y',
331
+ 'Z' => 'z',
332
+ }
333
+
334
+ ##
335
+ # Mapping of optioned (characters written when holding option/alt)
336
+ # characters to keycodes.
337
+ #
338
+ # @return [Hash{String=>Fixnum}]
339
+ OPTIONED = {
340
+ '¡' => '1',
341
+ '™' => '2',
342
+ '£' => '3',
343
+ '¢' => '4',
344
+ '∞' => '5',
345
+ '§' => '6',
346
+ '¶' => '7',
347
+ '•' => '8',
348
+ 'ª' => '9',
349
+ 'º' => '0',
350
+ '“' => '[',
351
+ '‘' => ']',
352
+ 'æ' => "'",
353
+ '≤' => ',',
354
+ '≥' => '.',
355
+ 'π' => 'p',
356
+ '¥' => 'y',
357
+ 'ƒ' => 'f',
358
+ '©' => 'g',
359
+ '®' => 'r',
360
+ '¬' => 'l',
361
+ '÷' => '/',
362
+ '≠' => '=',
363
+ '«' => "\\",
364
+ 'å' => 'a',
365
+ 'ø' => 'o',
366
+ '´' => 'e',
367
+ '¨' => 'u',
368
+ 'ˆ' => 'i',
369
+ '∂' => 'd',
370
+ '˙' => 'h',
371
+ '†' => 't',
372
+ '˜' => 'n',
373
+ 'ß' => 's',
374
+ '–' => '-',
375
+ '…' => ';',
376
+ 'œ' => 'q',
377
+ '∆' => 'j',
378
+ '˚' => 'k',
379
+ '≈' => 'x',
380
+ '∫' => 'b',
381
+ 'µ' => 'm',
382
+ '∑' => 'w',
383
+ '√' => 'v',
384
+ 'Ω' => 'z',
385
+ }
386
+
387
+
388
+ ##
389
+ # Once {#generate} is called, this contains the sequence of
390
+ # events.
391
+ #
392
+ # @return [Array<Array(Fixnum,Boolean)>]
393
+ attr_reader :events
394
+
395
+ # @param [Array<String,Array<String,Array...>>]
396
+ def initialize tokens
397
+ @tokens = tokens
398
+ # *3 since the output array will be at least *2 the
399
+ # number of tokens passed in, but will often be larger
400
+ # due to shifted/optioned characters and custom escapes;
401
+ # though a better number could be derived from
402
+ # analyzing common input...
403
+ @events = Array.new tokens.size*3
404
+ end
405
+
406
+ ##
407
+ # Generate the events for the tokens the event generator
408
+ # was initialized with. Returns the generated events, though
409
+ # you can also use {#events} to get the events later.
410
+ #
411
+ # @return [Array<Array(Fixnum,Boolean)>]
412
+ def generate
413
+ @index = 0
414
+ gen_all @tokens
415
+ @events.compact!
416
+ @events
417
+ end
418
+
419
+
420
+ private
421
+
422
+ def add event
423
+ @events[@index] = event
424
+ @index += 1
425
+ end
426
+ def previous_token; @events[@index-1] end
427
+ def rewind_index; @index -= 1 end
428
+
429
+ def gen_all tokens
430
+ tokens.each do |token|
431
+ if token.kind_of? Array
432
+ gen_nested token.first, token[1..-1]
433
+ else
434
+ gen_single token
435
+ end
436
+ end
437
+ end
438
+
439
+ def gen_nested head, tail
440
+ ((code = CUSTOM[head]) && gen_dynamic(code, tail)) ||
441
+ ((code = MAPPING[head]) && gen_dynamic(code, tail)) ||
442
+ ((code = SHIFTED[head]) && gen_shifted(code, tail)) ||
443
+ ((code = OPTIONED[head]) && gen_optioned(code, tail)) ||
444
+ gen_all(head.split(EMPTY_STRING)) # handling a special case :(
445
+ end
446
+
447
+ def gen_single token
448
+ ((code = MAPPING[token]) && gen_dynamic(code, nil)) ||
449
+ ((code = SHIFTED[token]) && gen_shifted(code, nil)) ||
450
+ ((code = OPTIONED[token]) && gen_optioned(code, nil)) ||
451
+ raise(ArgumentError, "#{token.inspect} has no mapping, bail!")
452
+ end
453
+
454
+ def gen_shifted code, tail
455
+ previous_token == SHIFT_UP ? rewind_index : add(SHIFT_DOWN)
456
+ gen_dynamic MAPPING[code], tail
457
+ add SHIFT_UP
458
+ end
459
+
460
+ def gen_optioned code, tail
461
+ previous_token == OPTION_UP ? rewind_index : add(OPTION_DOWN)
462
+ gen_dynamic MAPPING[code], tail
463
+ add OPTION_UP
464
+ end
465
+
466
+ def gen_dynamic code, tail
467
+ add [code, true]
468
+ gen_all tail if tail
469
+ add [code, false]
470
+ end
471
+
472
+ # @private
473
+ EMPTY_STRING = ""
474
+ # @private
475
+ OPTION_DOWN = [58, true]
476
+ # @private
477
+ OPTION_UP = [58, false]
478
+ # @private
479
+ SHIFT_DOWN = [56, true]
480
+ # @private
481
+ SHIFT_UP = [56, false]
482
+ end
483
+
484
+ end
485
+
486
+
487
+ ##
488
+ # @note This will only work if a run loop is running
489
+ # framework 'ApplicationServices' if defined? MACRUBY_VERSION
490
+ # Register to be notified if the keyboard layout changes at runtime
491
+ # NSDistributedNotificationCenter.defaultCenter.addObserver Accessibility::String::EventGenerator,
492
+ # selector: 'regenerate_dynamic_mapping',
493
+ # name: KTISNotifySelectedKeyboardInputSourceChanged,
494
+ # object: nil