accessibility_keyboard 1.0.0
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.
- data/.yardopts +14 -0
- data/CONTRIBUTING.markdown +44 -0
- data/History.markdown +4 -0
- data/README.markdown +98 -0
- data/ext/key_coder/extconf.rb +22 -0
- data/ext/key_coder/key_coder.c +171 -0
- data/lib/accessibility/keyboard.rb +34 -0
- data/lib/accessibility/keyboard/event_generator.rb +351 -0
- data/lib/accessibility/keyboard/parser.rb +111 -0
- data/lib/accessibility/keyboard/version.rb +8 -0
- data/test/event_generator_test.rb +149 -0
- data/test/helper.rb +3 -0
- data/test/keyboard_test.rb +29 -0
- data/test/parser_test.rb +62 -0
- data/test/runner +14 -0
- metadata +81 -0
data/.yardopts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Contributing to accessibility\_keyboard
|
|
2
|
+
|
|
3
|
+
First, let me thank you for wanting to contribute! :)
|
|
4
|
+
|
|
5
|
+
There are no contributions that are too small. Even if you are only fixing
|
|
6
|
+
typos, adding documentation, or some example code. The goal is to make the
|
|
7
|
+
process simple and as painless as possible to encourage contributions.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## Reporting Bugs
|
|
11
|
+
|
|
12
|
+
If you think you've found a bug, feel free to log the issue. It is OK to
|
|
13
|
+
make a duplicate bug report; such reports will be closed as duplicates
|
|
14
|
+
with a pointer to the original issue.
|
|
15
|
+
|
|
16
|
+
You may also wish to see if the bug you've reported has already been
|
|
17
|
+
fixed or obsoleted on the master branch.
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
## Code
|
|
21
|
+
|
|
22
|
+
Try to stick within the existing style. The importance of sticking with
|
|
23
|
+
existing style grows linearly with the size of the contribution.
|
|
24
|
+
|
|
25
|
+
It is preferred that code contributions, whether they are new features or
|
|
26
|
+
bug fixes, be accompanied by test(s) where appropriate.
|
|
27
|
+
|
|
28
|
+
The best way to submit code is through a
|
|
29
|
+
[pull request](https://help.github.com/articles/using-pull-requests) on the
|
|
30
|
+
[Github page](https://github.com/AXElements/accessibility_keyboard),
|
|
31
|
+
though email is also acceptable if privacy is a concern. Unless otherwise
|
|
32
|
+
requested, commit history will always be made to reflect who made the
|
|
33
|
+
contribution.
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
## Documentation and Examples
|
|
37
|
+
|
|
38
|
+
As with code, try to fit with the existing style. New examples are always
|
|
39
|
+
welcome and the wiki could always use a new article. :)
|
|
40
|
+
|
|
41
|
+
The documentation in the wiki is open and no permission is required to make
|
|
42
|
+
edits or contributions. For API documentation or example code, it is best to
|
|
43
|
+
submit a pull request (or create a new wiki page if appropriate).
|
|
44
|
+
|
data/History.markdown
ADDED
data/README.markdown
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# keyboard
|
|
2
|
+
|
|
3
|
+
This gem is a component of
|
|
4
|
+
[AXElements](http://github.com/Marketcircle/AXElements). It provides
|
|
5
|
+
an interface for posting keyboard events to the system as well as a
|
|
6
|
+
mixin for parsing a string into a series of events.
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
A port of keyboard simulator code from
|
|
10
|
+
[AXElements](http://github.com/AXElements/AXElements),
|
|
11
|
+
but cleaned up and released as its own gem.
|
|
12
|
+
|
|
13
|
+
By itself, the `accessibility_keyboard` gem has limited use; but in
|
|
14
|
+
combination with a gem for performing other GUI manipulations, like
|
|
15
|
+
AXElements, this gem is very powerful and can be used for tasks such
|
|
16
|
+
as automated functional testing.
|
|
17
|
+
|
|
18
|
+
[](https://gemnasium.com/AXElements/accessibility_keyboard)
|
|
19
|
+
[](https://codeclimate.com/github/AXElements/accessibility_keyboard)
|
|
20
|
+
[](https://travis-ci.org/AXElements/accessibility_keyboard)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
## Examples
|
|
24
|
+
|
|
25
|
+
The basics:
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
require 'accessibility/keyboard'
|
|
29
|
+
|
|
30
|
+
include Accessibility::Keyboard
|
|
31
|
+
|
|
32
|
+
keyboard_events_for("Hey, there!").each do |event|
|
|
33
|
+
KeyCoder.post_event event
|
|
34
|
+
end
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Something a bit more advanced:
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
require 'accessibility/keyboard'
|
|
41
|
+
|
|
42
|
+
include Accessibility::Keyboard
|
|
43
|
+
|
|
44
|
+
keyboard_events_for("\\COMMAND+\t").each do |event|
|
|
45
|
+
KeyCoder.post_event event
|
|
46
|
+
end
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
## Documentation
|
|
51
|
+
|
|
52
|
+
- [Keyboarding Blog Post](http://ferrous26.com/blog/2012/04/03/axelements-part1/).
|
|
53
|
+
- [API documentation](http://rdoc.info/gems/accessibility_keyboard/frames)
|
|
54
|
+
- The AXElements [keyboarding tutorial](https://github.com/AXElements/AXElements/wiki/Keyboarding)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
## Development
|
|
58
|
+
|
|
59
|
+
Development of this library happens as part of AXElements, but tests
|
|
60
|
+
and the API for this component remain separate so that it can be
|
|
61
|
+
released as part of the `accessibility_keyboard` gem.
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
### TODO
|
|
65
|
+
|
|
66
|
+
The API for posting events is ad-hoc for the sake of demonstration;
|
|
67
|
+
AXElements exposes this functionality via `Kernel#type`. The standalone
|
|
68
|
+
API provided here could be improved.
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
## Copyright
|
|
72
|
+
|
|
73
|
+
Copyright (c) 2013, Mark Rada
|
|
74
|
+
All rights reserved.
|
|
75
|
+
|
|
76
|
+
Redistribution and use in source and binary forms, with or without
|
|
77
|
+
modification, are permitted provided that the following conditions are met:
|
|
78
|
+
|
|
79
|
+
* Redistributions of source code must retain the above copyright
|
|
80
|
+
notice, this list of conditions and the following disclaimer.
|
|
81
|
+
* Redistributions in binary form must reproduce the above copyright
|
|
82
|
+
notice, this list of conditions and the following disclaimer in the
|
|
83
|
+
documentation and/or other materials provided with the distribution.
|
|
84
|
+
* Neither the name of Mark Rada nor the names of its
|
|
85
|
+
contributors may be used to endorse or promote products derived
|
|
86
|
+
from this software without specific prior written permission.
|
|
87
|
+
|
|
88
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
89
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
90
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
91
|
+
DISCLAIMED. IN NO EVENT SHALL Mark Rada BE LIABLE FOR ANY
|
|
92
|
+
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
93
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
|
|
94
|
+
GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
95
|
+
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
|
96
|
+
IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
|
|
97
|
+
OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
|
98
|
+
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require 'mkmf'
|
|
2
|
+
|
|
3
|
+
$CFLAGS << ' -std=c99 -Wall -Werror -ObjC'
|
|
4
|
+
$LIBS << ' -framework Cocoa -framework Carbon -framework ApplicationServices'
|
|
5
|
+
|
|
6
|
+
if RUBY_ENGINE == 'macruby'
|
|
7
|
+
$CFLAGS << ' -fobjc-gc'
|
|
8
|
+
else
|
|
9
|
+
unless RbConfig::CONFIG["CC"].match /clang/
|
|
10
|
+
clang = `which clang`.chomp
|
|
11
|
+
if clang.empty?
|
|
12
|
+
$stdout.puts "Clang not installed. Cannot build C extension"
|
|
13
|
+
raise "Clang not installed. Cannot build C extension"
|
|
14
|
+
else
|
|
15
|
+
RbConfig::MAKEFILE_CONFIG["CC"] = clang
|
|
16
|
+
RbConfig::MAKEFILE_CONFIG["CXX"] = clang
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
$CFLAGS << ' -DNOT_MACRUBY'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
create_makefile('accessibility/key_coder')
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* key_coder.c
|
|
3
|
+
* KeyCoder
|
|
4
|
+
*
|
|
5
|
+
* Created by Mark Rada on 11-07-27.
|
|
6
|
+
* Copyright 2013 Mark Rada. All rights reserved.
|
|
7
|
+
* Copyright 2011-2012 Marketcircle Incorporated. All rights reserved.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
#import <Cocoa/Cocoa.h>
|
|
12
|
+
#import <Carbon/Carbon.h>
|
|
13
|
+
#import <ApplicationServices/ApplicationServices.h>
|
|
14
|
+
|
|
15
|
+
#include "ruby.h"
|
|
16
|
+
|
|
17
|
+
static VALUE rb_cEventGenerator;
|
|
18
|
+
static ID sel_regenerate;
|
|
19
|
+
|
|
20
|
+
#ifdef NOT_MACRUBY
|
|
21
|
+
#define RELEASE(x) CFRelease(x)
|
|
22
|
+
#else
|
|
23
|
+
#define RELEASE(x) CFMakeCollectable(x)
|
|
24
|
+
#endif
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
/*
|
|
28
|
+
* Generate the mapping of characters to key codes for keys that can be
|
|
29
|
+
* remapped based on keyboard layout. Changing the keyboard layout at
|
|
30
|
+
* runtime will cause the returned hash to be different.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
*
|
|
34
|
+
* KeyCoder.dynamic_mapping => { "a" => 0, "b" => 24, ... }
|
|
35
|
+
*
|
|
36
|
+
* @return [Hash{String=>Number}]
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
static
|
|
40
|
+
VALUE
|
|
41
|
+
keycoder_dynamic_mapping()
|
|
42
|
+
{
|
|
43
|
+
|
|
44
|
+
VALUE map = rb_hash_new();
|
|
45
|
+
|
|
46
|
+
#ifdef NOT_MACRUBY
|
|
47
|
+
@autoreleasepool {
|
|
48
|
+
#endif
|
|
49
|
+
|
|
50
|
+
TISInputSourceRef keyboard = TISCopyCurrentKeyboardLayoutInputSource();
|
|
51
|
+
CFDataRef layout_data = (CFDataRef)TISGetInputSourceProperty(keyboard, kTISPropertyUnicodeKeyLayoutData);
|
|
52
|
+
const UCKeyboardLayout* layout = (const UCKeyboardLayout*)CFDataGetBytePtr(layout_data);
|
|
53
|
+
|
|
54
|
+
void (^key_coder)(int) = ^(int key_code) {
|
|
55
|
+
UniChar string[255];
|
|
56
|
+
UniCharCount string_length = 0;
|
|
57
|
+
UInt32 dead_key_state = 0;
|
|
58
|
+
UCKeyTranslate(
|
|
59
|
+
layout,
|
|
60
|
+
key_code,
|
|
61
|
+
kUCKeyActionDown,
|
|
62
|
+
0,
|
|
63
|
+
LMGetKbdType(), // kb type
|
|
64
|
+
0, // OptionBits keyTranslateOptions,
|
|
65
|
+
&dead_key_state,
|
|
66
|
+
255,
|
|
67
|
+
&string_length,
|
|
68
|
+
string
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
NSString* nsstring = [NSString stringWithCharacters:string length:string_length];
|
|
72
|
+
rb_hash_aset(map, rb_str_new_cstr([nsstring UTF8String]), INT2FIX(key_code));
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// skip 65-92 since they are hard coded and do not change
|
|
76
|
+
for (int key_code = 0; key_code < 65; key_code++)
|
|
77
|
+
key_coder(key_code);
|
|
78
|
+
for (int key_code = 93; key_code < 127; key_code++)
|
|
79
|
+
key_coder(key_code);
|
|
80
|
+
|
|
81
|
+
RELEASE(keyboard);
|
|
82
|
+
|
|
83
|
+
#ifdef NOT_MACRUBY
|
|
84
|
+
}; // Close the autorelease pool
|
|
85
|
+
#endif
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
return map;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
/*
|
|
93
|
+
* Post the given event to the system and return `true`. This method
|
|
94
|
+
* will also add a small (9000 microsecond) delay after posting to
|
|
95
|
+
* ensure that keyboard actions do not go too fast.
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
*
|
|
99
|
+
* KeyCoder.post_event [0, true] -> true
|
|
100
|
+
*
|
|
101
|
+
* @param [Array(Number, Boolean)]
|
|
102
|
+
* @return [true]
|
|
103
|
+
*/
|
|
104
|
+
|
|
105
|
+
static
|
|
106
|
+
VALUE
|
|
107
|
+
keycoder_post_event(VALUE self, VALUE event)
|
|
108
|
+
{
|
|
109
|
+
VALUE code = rb_ary_entry(event, 0);
|
|
110
|
+
VALUE state = rb_ary_entry(event, 1);
|
|
111
|
+
|
|
112
|
+
CGEventRef event_ref = CGEventCreateKeyboardEvent(NULL, FIX2LONG(code), state);
|
|
113
|
+
CGEventPost(kCGHIDEventTap, event_ref);
|
|
114
|
+
RELEASE(event_ref);
|
|
115
|
+
|
|
116
|
+
usleep(9000); // 9000 is a magic number
|
|
117
|
+
return Qtrue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@interface AKEventGenerator : NSObject
|
|
122
|
+
- (void)regenerateDynamicMapping;
|
|
123
|
+
@end
|
|
124
|
+
|
|
125
|
+
@implementation AKEventGenerator
|
|
126
|
+
|
|
127
|
+
- (void)regenerateDynamicMapping
|
|
128
|
+
{
|
|
129
|
+
rb_funcall(rb_cEventGenerator, sel_regenerate, 0);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@end
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
void
|
|
136
|
+
Init_key_coder()
|
|
137
|
+
{
|
|
138
|
+
/*
|
|
139
|
+
* Document-class: KeyCoder
|
|
140
|
+
*
|
|
141
|
+
* Class that encapsulates some low level work for finding key code mappings
|
|
142
|
+
* and posting keyboard events to the system.
|
|
143
|
+
*
|
|
144
|
+
*/
|
|
145
|
+
VALUE cKeyCoder = rb_define_class("KeyCoder", rb_cObject);
|
|
146
|
+
rb_define_singleton_method(cKeyCoder, "dynamic_mapping", keycoder_dynamic_mapping, 0);
|
|
147
|
+
rb_define_singleton_method(cKeyCoder, "post_event", keycoder_post_event, 1);
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
/* Register to be notified if the keyboard layout changes at runtime. The
|
|
152
|
+
* receiver will tell the event generator to regenerate its mapping.
|
|
153
|
+
* This will only work if a run loop is running (e.g. using the "spin"
|
|
154
|
+
* method that comes with accessibility_core).
|
|
155
|
+
*/
|
|
156
|
+
|
|
157
|
+
VALUE rb_mAccessibility = rb_define_module("Accessibility");
|
|
158
|
+
VALUE rb_mKeyboard = rb_define_module_under(rb_mAccessibility, "Keyboard");
|
|
159
|
+
rb_cEventGenerator = rb_define_class_under(rb_mKeyboard, "EventGenerator", rb_cObject);
|
|
160
|
+
|
|
161
|
+
sel_regenerate = rb_intern("regenerate_dynamic_mapping");
|
|
162
|
+
|
|
163
|
+
static AKEventGenerator* dynamic_regenerator;
|
|
164
|
+
dynamic_regenerator = [[AKEventGenerator alloc] init];
|
|
165
|
+
|
|
166
|
+
[[NSDistributedNotificationCenter defaultCenter]
|
|
167
|
+
addObserver:dynamic_regenerator
|
|
168
|
+
selector:sel_registerName("regenerateDynamicMapping")
|
|
169
|
+
name:(NSString*)kTISNotifySelectedKeyboardInputSourceChanged
|
|
170
|
+
object:nil];
|
|
171
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
require 'accessibility/keyboard/version'
|
|
2
|
+
require 'accessibility/keyboard/parser'
|
|
3
|
+
require 'accessibility/keyboard/event_generator'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
##
|
|
7
|
+
# Parses strings of human readable text into a series of events
|
|
8
|
+
#
|
|
9
|
+
# Events are meant to be processed by {KeyCoder.post_event} or
|
|
10
|
+
# [Accessibility::Core#post](https://github.com/AXElements/accessibility_core/blob/master/lib/accessibility/core/macruby.rb#L597).
|
|
11
|
+
#
|
|
12
|
+
# Supports most, if not all, latin keyboard layouts, maybe some
|
|
13
|
+
# international layouts as well. Japanese layouts can be made to work with
|
|
14
|
+
# use of `String#transform` on MacRuby. See README for examples.
|
|
15
|
+
#
|
|
16
|
+
module Accessibility::Keyboard
|
|
17
|
+
|
|
18
|
+
##
|
|
19
|
+
# Generate keyboard events for the given string
|
|
20
|
+
#
|
|
21
|
+
# Strings should be in a human readable format with a few exceptions.
|
|
22
|
+
# Command key (e.g. control, option, command) should be written in
|
|
23
|
+
# string as they appear in {Accessibility::Keyboard::EventGenerator::CUSTOM}.
|
|
24
|
+
#
|
|
25
|
+
# For more details on event generation, read the
|
|
26
|
+
# [Keyboarding wiki](http://github.com/AXElements/AXElements/wiki/Keyboarding).
|
|
27
|
+
#
|
|
28
|
+
# @param string [#to_s]
|
|
29
|
+
# @return [Array<Array(Fixnum,Boolean)>]
|
|
30
|
+
def keyboard_events_for string
|
|
31
|
+
EventGenerator.new(Parser.new(string).parse).generate
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
end
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
|
|
3
|
+
require 'accessibility/keyboard/version'
|
|
4
|
+
require 'accessibility/key_coder'
|
|
5
|
+
|
|
6
|
+
##
|
|
7
|
+
# Generate a sequence of keyboard events given a sequence of tokens.
|
|
8
|
+
# The token format is defined by the
|
|
9
|
+
# {Accessibility::Keyboard::EventGenerator}
|
|
10
|
+
# class output; it is best to use that class to generate the tokens.
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
#
|
|
14
|
+
# # Upper case 'A'
|
|
15
|
+
# EventGenerator.new(["A"]).generate # => [[56,true],[70,true],[70,false],[56,false]]
|
|
16
|
+
#
|
|
17
|
+
# # Press the volume up key
|
|
18
|
+
# EventGenerator.new([["\\F12"]]).generate # => [[0x6F,true],[0x6F,false]]
|
|
19
|
+
#
|
|
20
|
+
# # Hotkey, press and hold command key and then 'a', then release both
|
|
21
|
+
# EventGenerator.new([["\\CMD",["a"]]]).generate # => [[55,true],[70,true],[70,false],[55,false]]
|
|
22
|
+
#
|
|
23
|
+
# # Press the return/enter key
|
|
24
|
+
# EventGenerator.new(["\n"]).generate # => [[10,true],[10,false]]
|
|
25
|
+
#
|
|
26
|
+
class Accessibility::Keyboard::EventGenerator
|
|
27
|
+
|
|
28
|
+
##
|
|
29
|
+
# Regenerate the portion of the key mapping that is set dynamically
|
|
30
|
+
# based on keyboard layout (e.g. US, Dvorak, etc.).
|
|
31
|
+
#
|
|
32
|
+
# This method should be called whenever the keyboard layout changes.
|
|
33
|
+
# This can be called automatically by registering for a notification
|
|
34
|
+
# in a run looped environment.
|
|
35
|
+
def self.regenerate_dynamic_mapping
|
|
36
|
+
# KeyCoder is declared in the Objective-C extension
|
|
37
|
+
MAPPING.merge! KeyCoder.dynamic_mapping
|
|
38
|
+
# Also add an alias to the mapping
|
|
39
|
+
MAPPING["\n"] = MAPPING["\r"]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
##
|
|
43
|
+
# Dynamic mapping of characters to keycodes. The map is generated at
|
|
44
|
+
# startup time in order to support multiple keyboard layouts.
|
|
45
|
+
#
|
|
46
|
+
# @return [Hash{String=>Fixnum}]
|
|
47
|
+
MAPPING = {}
|
|
48
|
+
|
|
49
|
+
# Initialize the table
|
|
50
|
+
regenerate_dynamic_mapping
|
|
51
|
+
|
|
52
|
+
##
|
|
53
|
+
# @note These mappings are all static and come from `/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/Events.h`
|
|
54
|
+
#
|
|
55
|
+
# Map of custom escape sequences to their hardcoded keycode value.
|
|
56
|
+
#
|
|
57
|
+
# @return [Hash{String=>Fixnum}]
|
|
58
|
+
CUSTOM = {
|
|
59
|
+
"\\FUNCTION" => 0x3F, # Standard Control Keys
|
|
60
|
+
"\\FN" => 0x3F,
|
|
61
|
+
"\\CONTROL" => 0x3B,
|
|
62
|
+
"\\CTRL" => 0x3B,
|
|
63
|
+
"\\OPTION" => 0x3A,
|
|
64
|
+
"\\OPT" => 0x3A,
|
|
65
|
+
"\\ALT" => 0x3A,
|
|
66
|
+
"\\COMMAND" => 0x37,
|
|
67
|
+
"\\CMD" => 0x37,
|
|
68
|
+
"\\LSHIFT" => 0x38,
|
|
69
|
+
"\\SHIFT" => 0x38,
|
|
70
|
+
"\\CAPSLOCK" => 0x39,
|
|
71
|
+
"\\CAPS" => 0x39,
|
|
72
|
+
"\\ROPTION" => 0x3D,
|
|
73
|
+
"\\ROPT" => 0x3D,
|
|
74
|
+
"\\RALT" => 0x3D,
|
|
75
|
+
"\\RCONTROL" => 0x3E,
|
|
76
|
+
"\\RCTRL" => 0x3E,
|
|
77
|
+
"\\RSHIFT" => 0x3C,
|
|
78
|
+
"\\ESCAPE" => 0x35, # Top Row Keys
|
|
79
|
+
"\\ESC" => 0x35,
|
|
80
|
+
"\\VOLUMEUP" => 0x48,
|
|
81
|
+
"\\VOLUP" => 0x48,
|
|
82
|
+
"\\VOLUMEDOWN" => 0x49,
|
|
83
|
+
"\\VOLDOWN" => 0x49,
|
|
84
|
+
"\\MUTE" => 0x4A,
|
|
85
|
+
"\\F1" => 0x7A,
|
|
86
|
+
"\\F2" => 0x78,
|
|
87
|
+
"\\F3" => 0x63,
|
|
88
|
+
"\\F4" => 0x76,
|
|
89
|
+
"\\F5" => 0x60,
|
|
90
|
+
"\\F6" => 0x61,
|
|
91
|
+
"\\F7" => 0x62,
|
|
92
|
+
"\\F8" => 0x64,
|
|
93
|
+
"\\F9" => 0x65,
|
|
94
|
+
"\\F10" => 0x6D,
|
|
95
|
+
"\\F11" => 0x67,
|
|
96
|
+
"\\F12" => 0x6F,
|
|
97
|
+
"\\F13" => 0x69,
|
|
98
|
+
"\\F14" => 0x6B,
|
|
99
|
+
"\\F15" => 0x71,
|
|
100
|
+
"\\F16" => 0x6A,
|
|
101
|
+
"\\F17" => 0x40,
|
|
102
|
+
"\\F18" => 0x4F,
|
|
103
|
+
"\\F19" => 0x50,
|
|
104
|
+
"\\F20" => 0x5A,
|
|
105
|
+
"\\HELP" => 0x72, # Island Keys
|
|
106
|
+
"\\HOME" => 0x73,
|
|
107
|
+
"\\END" => 0x77,
|
|
108
|
+
"\\PAGEUP" => 0x74,
|
|
109
|
+
"\\PAGEDOWN" => 0x79,
|
|
110
|
+
"\\DELETE" => 0x75,
|
|
111
|
+
"\\LEFT" => 0x7B, # Arrow Keys
|
|
112
|
+
"\\<-" => 0x7B,
|
|
113
|
+
"\\RIGHT" => 0x7C,
|
|
114
|
+
"\\->" => 0x7C,
|
|
115
|
+
"\\DOWN" => 0x7D,
|
|
116
|
+
"\\UP" => 0x7E,
|
|
117
|
+
"\\0" => 0x52, # Keypad Keys
|
|
118
|
+
"\\1" => 0x53,
|
|
119
|
+
"\\2" => 0x54,
|
|
120
|
+
"\\3" => 0x55,
|
|
121
|
+
"\\4" => 0x56,
|
|
122
|
+
"\\5" => 0x57,
|
|
123
|
+
"\\6" => 0x58,
|
|
124
|
+
"\\7" => 0x59,
|
|
125
|
+
"\\8" => 0x5B,
|
|
126
|
+
"\\9" => 0x5C,
|
|
127
|
+
"\\DECIMAL" => 0x41,
|
|
128
|
+
"\\." => 0x41,
|
|
129
|
+
"\\PLUS" => 0x45,
|
|
130
|
+
"\\+" => 0x45,
|
|
131
|
+
"\\MULTIPLY" => 0x43,
|
|
132
|
+
"\\*" => 0x43,
|
|
133
|
+
"\\MINUS" => 0x4E,
|
|
134
|
+
"\\-" => 0x4E,
|
|
135
|
+
"\\DIVIDE" => 0x4B,
|
|
136
|
+
"\\/" => 0x4B,
|
|
137
|
+
"\\EQUALS" => 0x51,
|
|
138
|
+
"\\=" => 0x51,
|
|
139
|
+
"\\ENTER" => 0x4C,
|
|
140
|
+
"\\CLEAR" => 0x47,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
##
|
|
144
|
+
# Mapping of shifted (characters written when holding shift) characters
|
|
145
|
+
# to keycodes.
|
|
146
|
+
#
|
|
147
|
+
# @return [Hash{String=>Fixnum}]
|
|
148
|
+
SHIFTED = {
|
|
149
|
+
'~' => '`',
|
|
150
|
+
'!' => '1',
|
|
151
|
+
'@' => '2',
|
|
152
|
+
'#' => '3',
|
|
153
|
+
'$' => '4',
|
|
154
|
+
'%' => '5',
|
|
155
|
+
'^' => '6',
|
|
156
|
+
'&' => '7',
|
|
157
|
+
'*' => '8',
|
|
158
|
+
'(' => '9',
|
|
159
|
+
')' => '0',
|
|
160
|
+
'{' => '[',
|
|
161
|
+
'}' => ']',
|
|
162
|
+
'?' => '/',
|
|
163
|
+
'+' => '=',
|
|
164
|
+
'|' => "\\",
|
|
165
|
+
':' => ';',
|
|
166
|
+
'_' => '-',
|
|
167
|
+
'"' => "'",
|
|
168
|
+
'<' => ',',
|
|
169
|
+
'>' => '.',
|
|
170
|
+
'A' => 'a',
|
|
171
|
+
'B' => 'b',
|
|
172
|
+
'C' => 'c',
|
|
173
|
+
'D' => 'd',
|
|
174
|
+
'E' => 'e',
|
|
175
|
+
'F' => 'f',
|
|
176
|
+
'G' => 'g',
|
|
177
|
+
'H' => 'h',
|
|
178
|
+
'I' => 'i',
|
|
179
|
+
'J' => 'j',
|
|
180
|
+
'K' => 'k',
|
|
181
|
+
'L' => 'l',
|
|
182
|
+
'M' => 'm',
|
|
183
|
+
'N' => 'n',
|
|
184
|
+
'O' => 'o',
|
|
185
|
+
'P' => 'p',
|
|
186
|
+
'Q' => 'q',
|
|
187
|
+
'R' => 'r',
|
|
188
|
+
'S' => 's',
|
|
189
|
+
'T' => 't',
|
|
190
|
+
'U' => 'u',
|
|
191
|
+
'V' => 'v',
|
|
192
|
+
'W' => 'w',
|
|
193
|
+
'X' => 'x',
|
|
194
|
+
'Y' => 'y',
|
|
195
|
+
'Z' => 'z',
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
##
|
|
199
|
+
# Mapping of optioned (characters written when holding option/alt)
|
|
200
|
+
# characters to keycodes.
|
|
201
|
+
#
|
|
202
|
+
# @return [Hash{String=>Fixnum}]
|
|
203
|
+
OPTIONED = {
|
|
204
|
+
'¡' => '1',
|
|
205
|
+
'™' => '2',
|
|
206
|
+
'£' => '3',
|
|
207
|
+
'¢' => '4',
|
|
208
|
+
'∞' => '5',
|
|
209
|
+
'§' => '6',
|
|
210
|
+
'¶' => '7',
|
|
211
|
+
'•' => '8',
|
|
212
|
+
'ª' => '9',
|
|
213
|
+
'º' => '0',
|
|
214
|
+
'“' => '[',
|
|
215
|
+
'‘' => ']',
|
|
216
|
+
'æ' => "'",
|
|
217
|
+
'≤' => ',',
|
|
218
|
+
'≥' => '.',
|
|
219
|
+
'π' => 'p',
|
|
220
|
+
'¥' => 'y',
|
|
221
|
+
'ƒ' => 'f',
|
|
222
|
+
'©' => 'g',
|
|
223
|
+
'®' => 'r',
|
|
224
|
+
'¬' => 'l',
|
|
225
|
+
'÷' => '/',
|
|
226
|
+
'≠' => '=',
|
|
227
|
+
'«' => "\\",
|
|
228
|
+
'å' => 'a',
|
|
229
|
+
'ø' => 'o',
|
|
230
|
+
'´' => 'e',
|
|
231
|
+
'¨' => 'u',
|
|
232
|
+
'ˆ' => 'i',
|
|
233
|
+
'∂' => 'd',
|
|
234
|
+
'˙' => 'h',
|
|
235
|
+
'†' => 't',
|
|
236
|
+
'˜' => 'n',
|
|
237
|
+
'ß' => 's',
|
|
238
|
+
'–' => '-',
|
|
239
|
+
'…' => ';',
|
|
240
|
+
'œ' => 'q',
|
|
241
|
+
'∆' => 'j',
|
|
242
|
+
'˚' => 'k',
|
|
243
|
+
'≈' => 'x',
|
|
244
|
+
'∫' => 'b',
|
|
245
|
+
'µ' => 'm',
|
|
246
|
+
'∑' => 'w',
|
|
247
|
+
'√' => 'v',
|
|
248
|
+
'Ω' => 'z',
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
##
|
|
253
|
+
# Once {#generate} is called, this contains the sequence of
|
|
254
|
+
# events.
|
|
255
|
+
#
|
|
256
|
+
# @return [Array<Array(Fixnum,Boolean)>]
|
|
257
|
+
attr_reader :events
|
|
258
|
+
|
|
259
|
+
# @param tokens [Array<String,Array<String,Array...>>]
|
|
260
|
+
def initialize tokens
|
|
261
|
+
@tokens = tokens
|
|
262
|
+
# *3 since the output array will be at least *2 the
|
|
263
|
+
# number of tokens passed in, but will often be larger
|
|
264
|
+
# due to shifted/optioned characters and custom escapes;
|
|
265
|
+
# though a better number could be derived from
|
|
266
|
+
# analyzing common input...
|
|
267
|
+
@events = Array.new tokens.size*3
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
##
|
|
271
|
+
# Generate the events for the tokens the event generator
|
|
272
|
+
# was initialized with. Returns the generated events, though
|
|
273
|
+
# you can also use {#events} to get the events later.
|
|
274
|
+
#
|
|
275
|
+
# @return [Array<Array(Fixnum,Boolean)>]
|
|
276
|
+
def generate
|
|
277
|
+
@index = 0
|
|
278
|
+
gen_all @tokens
|
|
279
|
+
@events.compact!
|
|
280
|
+
@events
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
private
|
|
285
|
+
|
|
286
|
+
def add event
|
|
287
|
+
@events[@index] = event
|
|
288
|
+
@index += 1
|
|
289
|
+
end
|
|
290
|
+
def previous_token; @events[@index-1] end
|
|
291
|
+
def rewind_index; @index -= 1 end
|
|
292
|
+
|
|
293
|
+
def gen_all tokens
|
|
294
|
+
tokens.each do |token|
|
|
295
|
+
if token.kind_of? Array
|
|
296
|
+
gen_nested token.first, token[1..-1]
|
|
297
|
+
else
|
|
298
|
+
gen_single token
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def gen_nested head, tail
|
|
304
|
+
((code = CUSTOM[head]) && gen_dynamic(code, tail)) ||
|
|
305
|
+
((code = MAPPING[head]) && gen_dynamic(code, tail)) ||
|
|
306
|
+
((code = SHIFTED[head]) && gen_shifted(code, tail)) ||
|
|
307
|
+
((code = OPTIONED[head]) && gen_optioned(code, tail)) ||
|
|
308
|
+
gen_all(head.split(EMPTY_STRING)) # handling a special case :(
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def gen_single token
|
|
312
|
+
((code = MAPPING[token]) && gen_dynamic(code, nil)) ||
|
|
313
|
+
((code = SHIFTED[token]) && gen_shifted(code, nil)) ||
|
|
314
|
+
((code = OPTIONED[token]) && gen_optioned(code, nil)) ||
|
|
315
|
+
raise(ArgumentError, "#{token.inspect} has no mapping, bail!")
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def gen_shifted code, tail
|
|
319
|
+
previous_token == SHIFT_UP ? rewind_index : add(SHIFT_DOWN)
|
|
320
|
+
gen_dynamic MAPPING[code], tail
|
|
321
|
+
add SHIFT_UP
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def gen_optioned code, tail
|
|
325
|
+
previous_token == OPTION_UP ? rewind_index : add(OPTION_DOWN)
|
|
326
|
+
gen_dynamic MAPPING[code], tail
|
|
327
|
+
add OPTION_UP
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def gen_dynamic code, tail
|
|
331
|
+
add [code, true]
|
|
332
|
+
gen_all tail if tail
|
|
333
|
+
add [code, false]
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# @private
|
|
337
|
+
# @return [String]
|
|
338
|
+
EMPTY_STRING = ""
|
|
339
|
+
# @private
|
|
340
|
+
# @return [Array(Number,Boolean)]
|
|
341
|
+
OPTION_DOWN = [58, true]
|
|
342
|
+
# @private
|
|
343
|
+
# @return [Array(Number,Boolean)]
|
|
344
|
+
OPTION_UP = [58, false]
|
|
345
|
+
# @private
|
|
346
|
+
# @return [Array(Number,Boolean)]
|
|
347
|
+
SHIFT_DOWN = [56, true]
|
|
348
|
+
# @private
|
|
349
|
+
# @return [Array(Number,Boolean)]
|
|
350
|
+
SHIFT_UP = [56, false]
|
|
351
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
require 'accessibility/keyboard/version'
|
|
2
|
+
|
|
3
|
+
##
|
|
4
|
+
# Parse arbitrary strings into a stream of keyboard events
|
|
5
|
+
#
|
|
6
|
+
# This class will take a string and break it up into chunks for the keyboard
|
|
7
|
+
# event generator. The structure generated here is an array that contains
|
|
8
|
+
# strings and recursively other arrays of strings and arrays of strings.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
#
|
|
12
|
+
# Parser.new("Hai").parse # => ['H','a','i']
|
|
13
|
+
# Parser.new("\\CONTROL").parse # => [["\\CONTROL"]]
|
|
14
|
+
# Parser.new("\\COMMAND+a").parse # => [["\\COMMAND", ['a']]]
|
|
15
|
+
# Parser.new("One\nTwo").parse # => ['O','n','e',"\n",'T','w','o']
|
|
16
|
+
#
|
|
17
|
+
class Accessibility::Keyboard::Parser
|
|
18
|
+
|
|
19
|
+
##
|
|
20
|
+
# Once a string is parsed, this contains the tokenized structure.
|
|
21
|
+
#
|
|
22
|
+
# @return [Array<String,Array<String,...>]
|
|
23
|
+
attr_accessor :tokens
|
|
24
|
+
|
|
25
|
+
# @param string [String,#to_s]
|
|
26
|
+
def initialize string
|
|
27
|
+
@chars = string.to_s
|
|
28
|
+
@tokens = []
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
##
|
|
32
|
+
# Tokenize the string that the parser was initialized with and
|
|
33
|
+
# return the sequence of tokens that were parsed.
|
|
34
|
+
#
|
|
35
|
+
# @return [Array<String,Array<String,...>]
|
|
36
|
+
def parse
|
|
37
|
+
length = @chars.length
|
|
38
|
+
@index = 0
|
|
39
|
+
while @index < length
|
|
40
|
+
@tokens << if custom?
|
|
41
|
+
parse_custom
|
|
42
|
+
else
|
|
43
|
+
parse_char
|
|
44
|
+
end
|
|
45
|
+
@index += 1
|
|
46
|
+
end
|
|
47
|
+
@tokens
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
##
|
|
54
|
+
# Is it a real custom escape? Kind of a lie, there is one
|
|
55
|
+
# case it does not handle--they get handled in the generator,
|
|
56
|
+
# but maybe they should be handled here?
|
|
57
|
+
# - An upper case letter or symbol following `"\\"` that is
|
|
58
|
+
# not mapped
|
|
59
|
+
def custom?
|
|
60
|
+
@chars[@index] == CUSTOM_ESCAPE &&
|
|
61
|
+
(next_char = @chars[@index+1]) &&
|
|
62
|
+
next_char == next_char.upcase &&
|
|
63
|
+
next_char != SPACE
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# @todo refactor
|
|
67
|
+
# @return [Array]
|
|
68
|
+
def parse_custom
|
|
69
|
+
start = @index
|
|
70
|
+
loop do
|
|
71
|
+
char = @chars[@index]
|
|
72
|
+
if char == PLUS
|
|
73
|
+
if @chars[@index-1] == CUSTOM_ESCAPE # \\+ case
|
|
74
|
+
@index += 1
|
|
75
|
+
return custom_subseq start
|
|
76
|
+
else
|
|
77
|
+
tokens = custom_subseq start
|
|
78
|
+
@index += 1
|
|
79
|
+
return tokens << parse_custom
|
|
80
|
+
end
|
|
81
|
+
elsif char == SPACE
|
|
82
|
+
return custom_subseq start
|
|
83
|
+
elsif char == nil
|
|
84
|
+
raise ArgumentError, "Bad escape sequence" if start == @index
|
|
85
|
+
return custom_subseq start
|
|
86
|
+
else
|
|
87
|
+
@index += 1
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# @return [Array]
|
|
93
|
+
def custom_subseq start
|
|
94
|
+
[@chars[start...@index]]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# @return [String]
|
|
98
|
+
def parse_char
|
|
99
|
+
@chars[@index]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# @private
|
|
103
|
+
# @return [String]
|
|
104
|
+
SPACE = " "
|
|
105
|
+
# @private
|
|
106
|
+
# @return [String]
|
|
107
|
+
PLUS = "+"
|
|
108
|
+
# @private
|
|
109
|
+
# @return [String]
|
|
110
|
+
CUSTOM_ESCAPE = "\\"
|
|
111
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
require 'test/helper'
|
|
3
|
+
require 'accessibility/keyboard/event_generator'
|
|
4
|
+
|
|
5
|
+
class TestEventGenerator < MiniTest::Unit::TestCase
|
|
6
|
+
|
|
7
|
+
def gen tokens
|
|
8
|
+
Accessibility::Keyboard::EventGenerator.new(tokens).generate
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def map; @@map ||= KeyCoder.dynamic_mapping; end
|
|
12
|
+
|
|
13
|
+
def t; true; end
|
|
14
|
+
def f; false; end
|
|
15
|
+
|
|
16
|
+
def a; @@a ||= map['a']; end
|
|
17
|
+
def c; @@c ||= map['c']; end
|
|
18
|
+
def e; @@e ||= map['e']; end
|
|
19
|
+
def h; @@h ||= map['h']; end
|
|
20
|
+
def i; @@i ||= map['i']; end
|
|
21
|
+
def k; @@k ||= map['k']; end
|
|
22
|
+
def m; @@m ||= map["m"]; end
|
|
23
|
+
|
|
24
|
+
def two; @@two ||= map['2']; end
|
|
25
|
+
def four; @@four ||= map['4']; end
|
|
26
|
+
|
|
27
|
+
def retern; @@retern ||= map["\r"]; end
|
|
28
|
+
def tab; @@tab ||= map["\t"]; end
|
|
29
|
+
def space; @@space ||= map["\s"]; end
|
|
30
|
+
|
|
31
|
+
def dash; @@dash ||= map["-"]; end
|
|
32
|
+
def comma; @@comma ||= map[","]; end
|
|
33
|
+
def apos; @@apos ||= map["'"]; end
|
|
34
|
+
def at; @@at ||= map["2"]; end
|
|
35
|
+
def paren; @@paren ||= map["9"]; end
|
|
36
|
+
def chev; @@chev ||= map["."]; end
|
|
37
|
+
|
|
38
|
+
def sigma; @@sigma ||= map["w"]; end
|
|
39
|
+
def tm; @@tm ||= map["2"]; end
|
|
40
|
+
def gbp; @@gbp ||= map["3"]; end
|
|
41
|
+
def omega; @@omega ||= map["z"]; end
|
|
42
|
+
|
|
43
|
+
def bslash; @@blash ||= map["\\"]; end
|
|
44
|
+
|
|
45
|
+
# key code for the left shift key
|
|
46
|
+
def sd; [56,t]; end
|
|
47
|
+
def su; [56,f]; end
|
|
48
|
+
|
|
49
|
+
# key code for the left option key
|
|
50
|
+
def od; [58,t]; end
|
|
51
|
+
def ou; [58,f]; end
|
|
52
|
+
|
|
53
|
+
# key code for the left command key
|
|
54
|
+
def cd; [0x37,t]; end
|
|
55
|
+
def cu; [0x37,f]; end
|
|
56
|
+
|
|
57
|
+
# key code for right arrow key
|
|
58
|
+
def rd; [0x7c,t]; end
|
|
59
|
+
def ru; [0x7c,f]; end
|
|
60
|
+
|
|
61
|
+
# key code for left control key
|
|
62
|
+
def ctrld; [0x3B,t]; end
|
|
63
|
+
def ctrlu; [0x3B,f]; end
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_generate_lowercase
|
|
67
|
+
assert_equal [[a,t],[a,f]], gen(['a'])
|
|
68
|
+
assert_equal [[c,t],[c,f],[k,t],[k,f]], gen(['c','k'])
|
|
69
|
+
assert_equal [[e,t],[e,f],[e,t],[e,f]], gen(['e','e'])
|
|
70
|
+
assert_equal [[c,t],[c,f],[a,t],[a,f],[k,t],[k,f],[e,t],[e,f]], gen(['c','a','k','e'])
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def test_generate_uppercase
|
|
74
|
+
assert_equal [sd,[a,t],[a,f],su], gen(['A'])
|
|
75
|
+
assert_equal [sd,[c,t],[c,f],[k,t],[k,f],su], gen(['C','K'])
|
|
76
|
+
assert_equal [sd,[e,t],[e,f],[e,t],[e,f],su], gen(['E','E'])
|
|
77
|
+
assert_equal [sd,[c,t],[c,f],[a,t],[a,f],[k,t],[k,f],su], gen(['C','A','K'])
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def test_generate_numbers
|
|
81
|
+
assert_equal [[two,t],[two,f]], gen(['2'])
|
|
82
|
+
assert_equal [[four,t],[four,f],[two,t],[two,f]], gen(['4','2'])
|
|
83
|
+
assert_equal [[two,t],[two,f],[two,t],[two,f]], gen(['2','2'])
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def test_generate_ruby_escapes
|
|
87
|
+
assert_equal [[retern,t],[retern,f]], gen(["\r"])
|
|
88
|
+
assert_equal [[retern,t],[retern,f]], gen(["\n"])
|
|
89
|
+
assert_equal [[tab,t],[tab,f]], gen(["\t"])
|
|
90
|
+
assert_equal [[space,t],[space,f]], gen(["\s"])
|
|
91
|
+
assert_equal [[space,t],[space,f]], gen([" "])
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def test_generate_symbols
|
|
95
|
+
assert_equal [[dash,t],[dash,f]], gen(["-"])
|
|
96
|
+
assert_equal [[comma,t],[comma,f]], gen([","])
|
|
97
|
+
assert_equal [[apos,t],[apos,f]], gen(["'"])
|
|
98
|
+
assert_equal [sd,[at,t],[at,f],su], gen(["@"])
|
|
99
|
+
assert_equal [sd,[paren,t],[paren,f],su], gen(["("])
|
|
100
|
+
assert_equal [sd,[chev,t],[chev,f],su], gen([">"])
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def test_generate_unicode # holding option
|
|
104
|
+
assert_equal [od,[sigma,t],[sigma,f],ou], gen(["∑"])
|
|
105
|
+
assert_equal [od,[tm,t],[tm,f],ou], gen(["™"])
|
|
106
|
+
assert_equal [od,[gbp,t],[gbp,f],ou], gen(["£"])
|
|
107
|
+
assert_equal [od,[omega,t],[omega,f],ou], gen(["Ω"])
|
|
108
|
+
assert_equal [od,[tm,t],[tm,f],[gbp,t],[gbp,f],ou], gen(["™","£"])
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def test_generate_backslashes
|
|
112
|
+
assert_equal [[bslash,t],[bslash,f]], gen(["\\"])
|
|
113
|
+
assert_equal [[bslash,t],[bslash,f],[space,t],[space,f]], gen(["\\"," "])
|
|
114
|
+
assert_equal [[bslash,t],[bslash,f],[h,t],[h,f],[m,t],[m,f]], gen(["\\",'h','m'])
|
|
115
|
+
# is this the job of the parser or the lexer?
|
|
116
|
+
assert_equal [[bslash,t],[bslash,f],sd,[h,t],[h,f],[m,t],[m,f],su], gen([["\\HM"]])
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def test_generate_a_custom_escape
|
|
120
|
+
assert_equal [cd,cu], gen([["\\COMMAND"]])
|
|
121
|
+
assert_equal [cd,cu], gen([["\\CMD"]])
|
|
122
|
+
assert_equal [ctrld,ctrlu], gen([["\\CONTROL"]])
|
|
123
|
+
assert_equal [ctrld,ctrlu], gen([["\\CTRL"]])
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def test_generate_hotkey
|
|
127
|
+
assert_equal [ctrld,[a,t],[a,f],ctrlu], gen([["\\CONTROL",["a"]]])
|
|
128
|
+
assert_equal [cd,sd,rd,ru,su,cu], gen([["\\COMMAND",['\SHIFT',['\->']]]])
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def test_generate_real_use # a regression
|
|
132
|
+
assert_equal [ctrld,[a,t],[a,f],ctrlu,[h,t],[h,f]], gen([["\\CTRL",["a"]],"h"])
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def test_bails_for_unmapped_token
|
|
136
|
+
# cannot generate snowmen :(
|
|
137
|
+
e = assert_raises(ArgumentError) { gen(["☃"]) }
|
|
138
|
+
assert_match /bail/i, e.message
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def test_generate_arbitrary_nested_array_sequence
|
|
142
|
+
assert_equal [[c,t],[a,t],[k,t],[e,t],[e,f],[k,f],[a,f],[c,f]], gen([["c",["a",["k",["e"]]]]])
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def test_generate_command_A
|
|
146
|
+
assert_equal [cd,sd,[a,t],[a,f],su,cu], gen([["\\COMMAND",["A"]]])
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
end
|
data/test/helper.rb
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
require 'test/helper'
|
|
2
|
+
require 'accessibility/keyboard'
|
|
3
|
+
|
|
4
|
+
# @note DO NOT TEST POSTING EVENTS HERE
|
|
5
|
+
# We only want to test posting events if all the tests in this file pass,
|
|
6
|
+
# otherwise the posted events may be unpredictable depending on what fails.
|
|
7
|
+
# Test event posting in the integration tests.
|
|
8
|
+
class TestKeyboard < MiniTest::Unit::TestCase
|
|
9
|
+
include Accessibility::Keyboard
|
|
10
|
+
|
|
11
|
+
# basic test to make sure the lexer and generator get along
|
|
12
|
+
def test_keyboard_events_for
|
|
13
|
+
events = keyboard_events_for 'cheezburger'
|
|
14
|
+
assert_kind_of Array, events
|
|
15
|
+
refute_empty events
|
|
16
|
+
|
|
17
|
+
assert_equal true, events[0][1]
|
|
18
|
+
assert_equal false, events[1][1]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def test_dynamic_map_initialized
|
|
22
|
+
refute_empty Accessibility::Keyboard::EventGenerator::MAPPING
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def test_can_parse_empty_string
|
|
26
|
+
assert_equal [], keyboard_events_for('')
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
end
|
data/test/parser_test.rb
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
require 'test/helper'
|
|
3
|
+
require 'accessibility/keyboard/parser'
|
|
4
|
+
|
|
5
|
+
class TestParser < MiniTest::Unit::TestCase
|
|
6
|
+
|
|
7
|
+
def parse string
|
|
8
|
+
Accessibility::Keyboard::Parser.new(string).parse
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def test_parse_simple_string
|
|
12
|
+
assert_equal [], parse('')
|
|
13
|
+
assert_equal ['"',"J","u","s","t"," ","W","o","r","k","s",'"',"™"], parse('"Just Works"™')
|
|
14
|
+
assert_equal ["M","i","l","k",","," ","s","h","a","k","e","."], parse("Milk, shake.")
|
|
15
|
+
assert_equal ["D","B","7"], parse("DB7")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def test_parse_single_custom_escape
|
|
19
|
+
assert_equal [["\\CMD"]], parse("\\CMD")
|
|
20
|
+
assert_equal [["\\1"]], parse("\\1")
|
|
21
|
+
assert_equal [["\\F1"]], parse("\\F1")
|
|
22
|
+
assert_equal [["\\*"]], parse("\\*")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def test_parse_hotkey_custom_escape
|
|
26
|
+
assert_equal [["\\COMMAND",[","]]], parse("\\COMMAND+,")
|
|
27
|
+
assert_equal [["\\COMMAND",["\\SHIFT",["s"]]]], parse("\\COMMAND+\\SHIFT+s")
|
|
28
|
+
assert_equal [["\\COMMAND",["\\+"]]], parse("\\COMMAND+\\+")
|
|
29
|
+
assert_equal [["\\FN",["\\F10"]]], parse("\\FN+\\F10")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def test_parse_ruby_escapes
|
|
33
|
+
assert_equal ["\n","\r","\t","\b"], parse("\n\r\t\b")
|
|
34
|
+
assert_equal ["O","n","e","\n","T","w","o"], parse("One\nTwo")
|
|
35
|
+
assert_equal ["L","i","e","\b","\b","\b","d","e","l","i","s","h"], parse("Lie\b\b\bdelish")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def test_parse_compparse_string
|
|
39
|
+
assert_equal ["T","e","s","t",["\\CMD",["s"]]], parse("Test\\CMD+s")
|
|
40
|
+
assert_equal ["Z","O","M","G"," ","1","3","3","7","!","!","1"], parse("ZOMG 1337!!1")
|
|
41
|
+
assert_equal ["F","u","u","!","@","#","%",["\\CMD",["a"]],"\b"], parse("Fuu!@#%\\CMD+a \b")
|
|
42
|
+
assert_equal [["\\CMD",["a"]],"\b","A","l","l"," ","g","o","n","e","!"], parse("\\CMD+a \bAll gone!")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def test_parse_backslash # make sure we handle these edge cases predictably
|
|
46
|
+
assert_equal ["\\"], parse("\\")
|
|
47
|
+
assert_equal ["\\"," "], parse("\\ ")
|
|
48
|
+
assert_equal ["\\","h","m","m"], parse("\\hmm")
|
|
49
|
+
assert_equal [["\\HMM"]], parse("\\HMM") # the one missed case
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def test_parse_plus_escape
|
|
53
|
+
assert_equal [["\\+"]], parse("\\+")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def test_parse_bad_custom_escape_sequence
|
|
57
|
+
assert_raises ArgumentError do
|
|
58
|
+
parse("\\COMMAND+")
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
end
|
data/test/runner
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
$LOAD_PATH << File.expand_path('../../', __FILE__)
|
|
4
|
+
$LOAD_PATH << File.expand_path('../../lib', __FILE__)
|
|
5
|
+
require 'test/helper'
|
|
6
|
+
|
|
7
|
+
ARGV.each do |file|
|
|
8
|
+
if file == '-v' || file == '--verbose'
|
|
9
|
+
$VERBOSE = true
|
|
10
|
+
else
|
|
11
|
+
require file
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
metadata
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: accessibility_keyboard
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
prerelease:
|
|
6
|
+
platform: ruby
|
|
7
|
+
authors:
|
|
8
|
+
- Mark Rada
|
|
9
|
+
autorequire:
|
|
10
|
+
bindir: bin
|
|
11
|
+
cert_chain: []
|
|
12
|
+
date: 2013-02-03 00:00:00.000000000 Z
|
|
13
|
+
dependencies: []
|
|
14
|
+
description: ! 'Simulate keyboard input via the Mac OS X Accessibility Framework.
|
|
15
|
+
This
|
|
16
|
+
|
|
17
|
+
gem is a component of AXElements.
|
|
18
|
+
|
|
19
|
+
'
|
|
20
|
+
email: mrada@marketcircle.com
|
|
21
|
+
executables: []
|
|
22
|
+
extensions:
|
|
23
|
+
- ext/key_coder/extconf.rb
|
|
24
|
+
extra_rdoc_files:
|
|
25
|
+
- README.markdown
|
|
26
|
+
- History.markdown
|
|
27
|
+
- CONTRIBUTING.markdown
|
|
28
|
+
- .yardopts
|
|
29
|
+
files:
|
|
30
|
+
- lib/accessibility/keyboard.rb
|
|
31
|
+
- lib/accessibility/keyboard/version.rb
|
|
32
|
+
- lib/accessibility/keyboard/parser.rb
|
|
33
|
+
- lib/accessibility/keyboard/event_generator.rb
|
|
34
|
+
- ext/key_coder/key_coder.c
|
|
35
|
+
- ext/key_coder/extconf.rb
|
|
36
|
+
- test/keyboard_test.rb
|
|
37
|
+
- test/parser_test.rb
|
|
38
|
+
- test/event_generator_test.rb
|
|
39
|
+
- test/helper.rb
|
|
40
|
+
- test/runner
|
|
41
|
+
- README.markdown
|
|
42
|
+
- History.markdown
|
|
43
|
+
- CONTRIBUTING.markdown
|
|
44
|
+
- .yardopts
|
|
45
|
+
homepage: http://github.com/AXElements/accessibility_keyboard
|
|
46
|
+
licenses:
|
|
47
|
+
- BSD 3-clause
|
|
48
|
+
post_install_message:
|
|
49
|
+
rdoc_options: []
|
|
50
|
+
require_paths:
|
|
51
|
+
- lib
|
|
52
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
53
|
+
none: false
|
|
54
|
+
requirements:
|
|
55
|
+
- - ! '>='
|
|
56
|
+
- !ruby/object:Gem::Version
|
|
57
|
+
version: '0'
|
|
58
|
+
segments:
|
|
59
|
+
- 0
|
|
60
|
+
hash: -4062551165478306694
|
|
61
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
62
|
+
none: false
|
|
63
|
+
requirements:
|
|
64
|
+
- - ! '>='
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '0'
|
|
67
|
+
segments:
|
|
68
|
+
- 0
|
|
69
|
+
hash: -4062551165478306694
|
|
70
|
+
requirements: []
|
|
71
|
+
rubyforge_project:
|
|
72
|
+
rubygems_version: 1.8.24
|
|
73
|
+
signing_key:
|
|
74
|
+
specification_version: 3
|
|
75
|
+
summary: Keyboard simulation for OS X
|
|
76
|
+
test_files:
|
|
77
|
+
- test/keyboard_test.rb
|
|
78
|
+
- test/parser_test.rb
|
|
79
|
+
- test/event_generator_test.rb
|
|
80
|
+
- test/helper.rb
|
|
81
|
+
- test/runner
|