AXElements 0.6.0beta2 → 0.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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