Skip to content

Commit b3be5c9

Browse files
authored
Merge pull request #805 from octalmage/test/macos-keyboard-input
fix(macOS): emit explicit modifier events.
2 parents 8221401 + d064384 commit b3be5c9

2 files changed

Lines changed: 122 additions & 31 deletions

File tree

src/keypress.c

Lines changed: 61 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,24 @@ static io_connect_t _getAuxiliaryKeyDriver(void)
4949
}
5050
return sEventDrvrRef;
5151
}
52+
53+
static void postMacKeyEvent(MMKeyCode code, const bool down, CGEventFlags flags)
54+
{
55+
CGEventRef keyEvent = CGEventCreateKeyboardEvent(NULL,
56+
(CGKeyCode)code, down);
57+
assert(keyEvent != NULL);
58+
59+
/* Hardware arrow-key events carry this flag. Mission Control ignores
60+
* synthetic Control+Arrow shortcuts without it. */
61+
if (code == K_UP || code == K_DOWN || code == K_LEFT || code == K_RIGHT) {
62+
flags |= kCGEventFlagMaskSecondaryFn;
63+
}
64+
65+
CGEventSetType(keyEvent, down ? kCGEventKeyDown : kCGEventKeyUp);
66+
CGEventSetFlags(keyEvent, flags);
67+
CGEventPost(kCGSessionEventTap, keyEvent);
68+
CFRelease(keyEvent);
69+
}
5270
#endif
5371

5472
#if defined(IS_WINDOWS)
@@ -125,22 +143,51 @@ void toggleKeyCode(MMKeyCode code, const bool down, MMKeyFlags flags)
125143
event.compound.misc.L[0] = evtInfo;
126144
kr = IOHIDPostEvent( _getAuxiliaryKeyDriver(), NX_SYSDEFINED, loc, &event, kNXEventDataVersion, 0, FALSE );
127145
assert( KERN_SUCCESS == kr );
128-
} else {
129-
CGEventFlags eventFlags = (CGEventFlags)flags;
130-
CGEventRef keyEvent = CGEventCreateKeyboardEvent(NULL,
131-
(CGKeyCode)code, down);
132-
assert(keyEvent != NULL);
133-
134-
/* Hardware arrow-key events carry this flag. Mission Control ignores
135-
* synthetic Control+Arrow shortcuts without it. */
136-
if (code == K_UP || code == K_DOWN || code == K_LEFT || code == K_RIGHT) {
137-
eventFlags |= kCGEventFlagMaskSecondaryFn;
146+
} else if (down) {
147+
CGEventFlags activeFlags = 0;
148+
149+
/* Post real modifier events around the key. Chromium and other apps
150+
* ignore bare flag bits for some shortcuts, and explicit key-up events
151+
* prevent the modifier from remaining active after the key tap. */
152+
if (flags & MOD_META) {
153+
activeFlags |= kCGEventFlagMaskCommand;
154+
postMacKeyEvent(K_META, true, activeFlags);
155+
}
156+
if (flags & MOD_ALT) {
157+
activeFlags |= kCGEventFlagMaskAlternate;
158+
postMacKeyEvent(K_ALT, true, activeFlags);
138159
}
160+
if (flags & MOD_CONTROL) {
161+
activeFlags |= kCGEventFlagMaskControl;
162+
postMacKeyEvent(K_CONTROL, true, activeFlags);
163+
}
164+
if (flags & MOD_SHIFT) {
165+
activeFlags |= kCGEventFlagMaskShift;
166+
postMacKeyEvent(K_SHIFT, true, activeFlags);
167+
}
168+
169+
postMacKeyEvent(code, true, (CGEventFlags)flags);
170+
} else {
171+
CGEventFlags activeFlags = (CGEventFlags)flags;
172+
173+
postMacKeyEvent(code, false, activeFlags);
139174

140-
CGEventSetType(keyEvent, down ? kCGEventKeyDown : kCGEventKeyUp);
141-
CGEventSetFlags(keyEvent, eventFlags);
142-
CGEventPost(kCGSessionEventTap, keyEvent);
143-
CFRelease(keyEvent);
175+
if (flags & MOD_SHIFT) {
176+
activeFlags &= ~kCGEventFlagMaskShift;
177+
postMacKeyEvent(K_SHIFT, false, activeFlags);
178+
}
179+
if (flags & MOD_CONTROL) {
180+
activeFlags &= ~kCGEventFlagMaskControl;
181+
postMacKeyEvent(K_CONTROL, false, activeFlags);
182+
}
183+
if (flags & MOD_ALT) {
184+
activeFlags &= ~kCGEventFlagMaskAlternate;
185+
postMacKeyEvent(K_ALT, false, activeFlags);
186+
}
187+
if (flags & MOD_META) {
188+
activeFlags &= ~kCGEventFlagMaskCommand;
189+
postMacKeyEvent(K_META, false, activeFlags);
190+
}
144191
}
145192
#elif defined(IS_WINDOWS)
146193
const DWORD dwFlags = down ? 0 : KEYEVENTF_KEYUP;

test/integration/keyboard.js

Lines changed: 61 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,24 @@ robot.setMouseDelay(100);
77
var target, elements;
88
var originalTimeout;
99

10+
function expectNextTypedText(expected, done, next) {
11+
const handleType = element => {
12+
if (element.id !== 'input_1') {
13+
return;
14+
}
15+
16+
target.removeListener('type', handleType);
17+
expect(element.text).toEqual(expected);
18+
if (next) {
19+
next();
20+
} else {
21+
done();
22+
}
23+
};
24+
25+
target.on('type', handleType);
26+
}
27+
1028
describe('Integration/Keyboard', () => {
1129
beforeAll(() => {
1230
originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
@@ -32,15 +50,8 @@ describe('Integration/Keyboard', () => {
3250

3351
it('types', done => {
3452
const stringToType = 'hello world';
35-
// Currently Target Practice waits for the "user" to finish typing before sending the event.
36-
const handleType = element => {
37-
expect(element.id).toEqual('input_1');
38-
if (element.text === stringToType) {
39-
target.removeListener('type', handleType);
40-
done();
41-
}
42-
};
43-
target.on('type', handleType);
53+
// Target Practice emits after the user has stopped typing.
54+
expectNextTypedText(stringToType, done);
4455

4556
const input_1 = elements.input_1;
4657
robot.moveMouse(input_1.x, input_1.y);
@@ -51,21 +62,54 @@ describe('Integration/Keyboard', () => {
5162
// Regression for https://github.com/octalmage/robotjs/pull/797
5263
it('types shifted symbols', done => {
5364
const stringToType = '!@#$%^&*()_+{}|:"<>?';
54-
const handleType = element => {
55-
expect(element.id).toEqual('input_1');
56-
if (element.text === stringToType) {
57-
target.removeListener('type', handleType);
58-
done();
59-
}
60-
};
61-
target.on('type', handleType);
65+
expectNextTypedText(stringToType, done);
6266

6367
const input_1 = elements.input_1;
6468
robot.moveMouse(input_1.x, input_1.y);
6569
robot.mouseClick();
6670
robot.typeString(stringToType);
6771
});
6872

73+
it('replaces selected input with a command-modified key tap on macOS', done => {
74+
if (process.platform !== 'darwin') {
75+
pending('macOS only: verifies command-modified keyboard events.');
76+
return;
77+
}
78+
79+
const initial = 'initial content';
80+
const replacement = 'replacement content';
81+
expectNextTypedText(initial, done, () => {
82+
expectNextTypedText(replacement, done);
83+
robot.keyTap('a', 'command');
84+
robot.typeString(replacement);
85+
});
86+
87+
const input_1 = elements.input_1;
88+
robot.moveMouse(input_1.x, input_1.y);
89+
robot.mouseClick();
90+
robot.typeString(initial);
91+
});
92+
93+
it('types a non-ASCII character with unicodeTap on macOS', done => {
94+
if (process.platform !== 'darwin') {
95+
pending('macOS only: verifies Unicode keyboard events.');
96+
return;
97+
}
98+
99+
const marker = 'x';
100+
const character = '嗨';
101+
expectNextTypedText(marker, done, () => {
102+
expectNextTypedText(character, done);
103+
robot.keyTap('backspace');
104+
robot.unicodeTap(character.charCodeAt(0));
105+
});
106+
107+
const input_1 = elements.input_1;
108+
robot.moveMouse(input_1.x, input_1.y);
109+
robot.mouseClick();
110+
robot.typeString(marker);
111+
});
112+
69113
// Regression for https://github.com/octalmage/robotjs/issues/789
70114
// keyTap("tab") and other multi-character key names threw "Invalid key code specified".
71115
it('keyTap accepts multi-character key names', () => {

0 commit comments

Comments
 (0)