1// Copyright 2005-2019 The Mumble Developers. All rights reserved.
2// Use of this source code is governed by a BSD-style license
3// that can be found in the LICENSE file at the root of the
4// Mumble source tree or at <https://www.mumble.info/LICENSE>.
5
6#include "mumble_pch.hpp"
7
8#import <AppKit/AppKit.h>
9#import <Carbon/Carbon.h>
10
11#include "GlobalShortcut_macx.h"
12#include "OverlayClient.h"
13
14#define MOD_OFFSET   0x10000
15#define MOUSE_OFFSET 0x20000
16
17GlobalShortcutEngine *GlobalShortcutEngine::platformInit() {
18	return new GlobalShortcutMac();
19}
20
21CGEventRef GlobalShortcutMac::callback(CGEventTapProxy proxy, CGEventType type,
22                                       CGEventRef event, void *udata) {
23	GlobalShortcutMac *gs = reinterpret_cast<GlobalShortcutMac *>(udata);
24	unsigned int keycode;
25	bool suppress = false;
26	bool forward = false;
27	bool down = false;
28	int64_t repeat = 0;
29
30	Q_UNUSED(proxy);
31
32	switch (type) {
33		case kCGEventLeftMouseDown:
34		case kCGEventRightMouseDown:
35		case kCGEventOtherMouseDown:
36			down = true;
37		case kCGEventLeftMouseUp:
38		case kCGEventRightMouseUp:
39		case kCGEventOtherMouseUp: {
40			keycode = static_cast<unsigned int>(CGEventGetIntegerValueField(event, kCGMouseEventButtonNumber));
41			suppress = gs->handleButton(MOUSE_OFFSET+keycode, down);
42			/* Suppressing "the" mouse button is probably not a good idea :-) */
43			if (keycode == 0)
44				suppress = false;
45			forward = !suppress;
46			break;
47		}
48
49		case kCGEventMouseMoved:
50		case kCGEventLeftMouseDragged:
51		case kCGEventRightMouseDragged:
52		case kCGEventOtherMouseDragged: {
53			if (g.ocIntercept) {
54				int64_t dx = CGEventGetIntegerValueField(event, kCGMouseEventDeltaX);
55				int64_t dy = CGEventGetIntegerValueField(event, kCGMouseEventDeltaY);
56				g.ocIntercept->iMouseX = qBound<int>(0, g.ocIntercept->iMouseX + static_cast<int>(dx), g.ocIntercept->uiWidth - 1);
57				g.ocIntercept->iMouseY = qBound<int>(0, g.ocIntercept->iMouseY + static_cast<int>(dy), g.ocIntercept->uiHeight - 1);
58				QMetaObject::invokeMethod(g.ocIntercept, "updateMouse", Qt::QueuedConnection);
59				forward = true;
60			}
61			break;
62		}
63
64		case kCGEventScrollWheel:
65			forward = true;
66			break;
67
68		case kCGEventKeyDown:
69			down = true;
70		case kCGEventKeyUp:
71			repeat = CGEventGetIntegerValueField(event, kCGKeyboardEventAutorepeat);
72			if (! repeat) {
73				keycode = static_cast<unsigned int>(CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode));
74				suppress = gs->handleButton(keycode, down);
75			}
76			forward = true;
77			break;
78
79		case kCGEventFlagsChanged: {
80			CGEventFlags f = CGEventGetFlags(event);
81
82			// Dump active event taps on Ctrl+Alt+Cmd.
83			CGEventFlags ctrlAltCmd = static_cast<CGEventFlags>(kCGEventFlagMaskControl|kCGEventFlagMaskAlternate|kCGEventFlagMaskCommand);
84			if ((f & ctrlAltCmd) == ctrlAltCmd)
85				gs->dumpEventTaps();
86
87			suppress = gs->handleModButton(f);
88			forward = !suppress;
89			break;
90		}
91
92		case kCGEventTapDisabledByTimeout:
93			qWarning("GlobalShortcutMac: EventTap disabled by timeout. Re-enabling.");
94			/*
95			 * On Snow Leopard, we get this event type quite often. It disables our event
96			 * tap completely. Possible Apple bug.
97			 *
98			 * For now, simply call CGEventTapEnable() to enable our event tap again.
99			 *
100			 * See: http://lists.apple.com/archives/quartz-dev/2009/Sep/msg00007.html
101			 */
102			CGEventTapEnable(gs->port, true);
103			break;
104
105		case kCGEventTapDisabledByUserInput:
106			break;
107
108		default:
109			break;
110	}
111
112		if (forward && g.ocIntercept) {
113			NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
114			NSEvent *evt = [[NSEvent eventWithCGEvent:event] retain];
115			QMetaObject::invokeMethod(gs, "forwardEvent", Qt::QueuedConnection, Q_ARG(void *, evt));
116			[pool release];
117			return NULL;
118		}
119
120	return suppress ? NULL : event;
121}
122
123GlobalShortcutMac::GlobalShortcutMac()
124    : loop(Q_NULLPTR)
125    , port(Q_NULLPTR)
126    , modmask(static_cast<CGEventFlags>(0)) {
127#ifndef QT_NO_DEBUG
128	qWarning("GlobalShortcutMac: Debug build detected. Disabling shortcut engine.");
129	return;
130#endif
131
132	CGEventMask evmask = CGEventMaskBit(kCGEventLeftMouseDown) |
133	                     CGEventMaskBit(kCGEventLeftMouseUp) |
134	                     CGEventMaskBit(kCGEventRightMouseDown) |
135	                     CGEventMaskBit(kCGEventRightMouseUp) |
136	                     CGEventMaskBit(kCGEventOtherMouseDown) |
137	                     CGEventMaskBit(kCGEventOtherMouseUp) |
138	                     CGEventMaskBit(kCGEventKeyDown) |
139	                     CGEventMaskBit(kCGEventKeyUp) |
140	                     CGEventMaskBit(kCGEventFlagsChanged) |
141	                     CGEventMaskBit(kCGEventMouseMoved) |
142	                     CGEventMaskBit(kCGEventLeftMouseDragged) |
143	                     CGEventMaskBit(kCGEventRightMouseDragged) |
144	                     CGEventMaskBit(kCGEventOtherMouseDragged) |
145	                     CGEventMaskBit(kCGEventScrollWheel);
146	port = CGEventTapCreate(kCGSessionEventTap,
147	                        kCGTailAppendEventTap,
148	                        kCGEventTapOptionDefault, // active filter (not only a listener)
149	                        evmask,
150	                        GlobalShortcutMac::callback,
151	                        this);
152
153	if (! port) {
154		qWarning("GlobalShortcutMac: Unable to create EventTap. Global Shortcuts will not be available.");
155		return;
156	}
157
158	kbdLayout = NULL;
159
160#if MAC_OS_X_VERSION_MAX_ALLOWED >= 1050
161# if MAC_OS_X_VERSION_MIN_REQUIRED < 1050
162	if (TISCopyCurrentKeyboardInputSource && TISGetInputSourceProperty)
163# endif
164	{
165		TISInputSourceRef inputSource = TISCopyCurrentKeyboardInputSource();
166		if (inputSource) {
167			CFDataRef data = static_cast<CFDataRef>(TISGetInputSourceProperty(inputSource, kTISPropertyUnicodeKeyLayoutData));
168			if (data)
169				kbdLayout = reinterpret_cast<UCKeyboardLayout *>(const_cast<UInt8 *>(CFDataGetBytePtr(data)));
170		}
171	}
172#endif
173#ifndef __LP64__
174	if (! kbdLayout) {
175		SInt16 currentKeyScript = GetScriptManagerVariable(smKeyScript);
176		SInt16 lastKeyLayoutID = GetScriptVariable(currentKeyScript, smScriptKeys);
177		Handle handle = GetResource('uchr', lastKeyLayoutID);
178		if (handle)
179			kbdLayout = reinterpret_cast<UCKeyboardLayout *>(*handle);
180	}
181#endif
182	if (! kbdLayout)
183		qWarning("GlobalShortcutMac: No keyboard layout mapping available. Unable to perform key translation.");
184
185	start(QThread::TimeCriticalPriority);
186}
187
188GlobalShortcutMac::~GlobalShortcutMac() {
189#ifndef QT_NO_DEBUG
190	return;
191#endif
192	if (loop) {
193		CFRunLoopStop(loop);
194		loop = Q_NULLPTR;
195		wait();
196	}
197}
198
199void GlobalShortcutMac::dumpEventTaps() {
200	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
201	uint32_t ntaps = 0;
202	CGEventTapInformation table[64];
203	if (CGGetEventTapList(20, table, &ntaps) == kCGErrorSuccess) {
204		qWarning("--- Installed Event Taps ---");
205		for (uint32_t i = 0; i < ntaps; i++) {
206			CGEventTapInformation *info = &table[i];
207
208			ProcessSerialNumber psn;
209			NSString *processName = nil;
210			OSStatus err = GetProcessForPID(info->tappingProcess, &psn);
211			if (err == noErr) {
212				CFStringRef str = NULL;
213				CopyProcessName(&psn, &str);
214				processName = (NSString *) str;
215				[processName autorelease];
216			}
217
218			qWarning("{");
219			qWarning("  eventTapID: %u", info->eventTapID);
220			qWarning("  tapPoint: 0x%x", info->tapPoint);
221			qWarning("  options = 0x%x", info->options);
222			qWarning("  eventsOfInterest = 0x%llx", info->eventsOfInterest);
223			qWarning("  tappingProcess = %i (%s)", info->tappingProcess, [processName UTF8String]);
224			qWarning("  processBeingTapped = %i", info->processBeingTapped);
225			qWarning("  enabled = %s", info->enabled ? "true":"false");
226			qWarning("  minUsecLatency = %.2f", info->minUsecLatency);
227			qWarning("  avgUsecLatency = %.2f", info->avgUsecLatency);
228			qWarning("  maxUsecLatency = %.2f", info->maxUsecLatency);
229			qWarning("}");
230		}
231		qWarning("--- End of Event Taps ---");
232	}
233	[pool release];
234}
235
236void GlobalShortcutMac::forwardEvent(void *evt) {
237	NSEvent *event = (NSEvent *)evt;
238	SEL sel = nil;
239
240	if (! g.ocIntercept)
241		return;
242
243	QWidget *vp = g.ocIntercept->qgv.viewport();
244	NSView *view = (NSView *) vp->winId();
245
246	switch ([event type]) {
247		case NSLeftMouseDown:
248			sel = @selector(mouseDown:);
249			break;
250		case NSLeftMouseUp:
251			sel = @selector(mouseUp:);
252			break;
253		case NSLeftMouseDragged:
254			sel = @selector(mouseDragged:);
255			break;
256		case NSRightMouseDown:
257			sel = @selector(rightMouseDown:);
258			break;
259		case NSRightMouseUp:
260			sel = @selector(rightMouseUp:);
261			break;
262		case NSRightMouseDragged:
263			sel = @selector(rightMouseDragged:);
264			break;
265		case NSOtherMouseDown:
266			sel = @selector(otherMouseDown:);
267			break;
268		case NSOtherMouseUp:
269			sel = @selector(otherMouseUp:);
270			break;
271		case NSOtherMouseDragged:
272			sel = @selector(otherMouseDragged:);
273			break;
274		case NSMouseEntered:
275			sel = @selector(mouseEntered:);
276			break;
277		case NSMouseExited:
278			sel = @selector(mouseExited:);
279			break;
280		case NSMouseMoved:
281			sel = @selector(mouseMoved:);
282			break;
283		default:
284			// Ignore the rest. We only care about mouse events.
285			break;
286	}
287
288	if (sel) {
289		NSPoint p; p.x = (CGFloat) g.ocIntercept->iMouseX;
290		p.y = (CGFloat) (g.ocIntercept->uiHeight - g.ocIntercept->iMouseY);
291		NSEvent *mouseEvent = [NSEvent mouseEventWithType:[event type] location:p modifierFlags:[event modifierFlags] timestamp:[event timestamp]
292		                               windowNumber:0 context:nil eventNumber:[event eventNumber] clickCount:[event clickCount]
293		                               pressure:[event pressure]];
294		if ([view respondsToSelector:sel])
295				[view performSelector:sel withObject:mouseEvent];
296		[event release];
297		return;
298	}
299
300	switch ([event type]) {
301		case NSKeyDown:
302			sel = @selector(keyDown:);
303			break;
304		case NSKeyUp:
305			sel = @selector(keyUp:);
306			break;
307		case NSFlagsChanged:
308			sel = @selector(flagsChanged:);
309			break;
310		case NSScrollWheel:
311			sel = @selector(scrollWheel:);
312			break;
313		default:
314			break;
315	}
316
317	if (sel) {
318		if ([view respondsToSelector:sel])
319				[view performSelector:sel withObject:event];
320	}
321
322	[event release];
323}
324
325void GlobalShortcutMac::run() {
326	loop = CFRunLoopGetCurrent();
327	CFRunLoopSourceRef src = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, port, 0);
328	CFRunLoopAddSource(loop, src, kCFRunLoopCommonModes);
329	CFRunLoopRun();
330}
331
332void GlobalShortcutMac::needRemap() {
333	remap();
334}
335
336bool GlobalShortcutMac::handleModButton(const CGEventFlags newmask) {
337	bool down;
338	bool suppress = false;
339
340#define MOD_CHANGED(mask, btn) do { \
341	    if ((newmask & mask) != (modmask & mask)) { \
342	        down = newmask & mask; \
343	        suppress = handleButton(MOD_OFFSET+btn, down); \
344	        modmask = newmask; \
345	        return suppress; \
346	    }} while (0)
347
348	MOD_CHANGED(kCGEventFlagMaskAlphaShift, 0);
349	MOD_CHANGED(kCGEventFlagMaskShift, 1);
350	MOD_CHANGED(kCGEventFlagMaskControl, 2);
351	MOD_CHANGED(kCGEventFlagMaskAlternate, 3);
352	MOD_CHANGED(kCGEventFlagMaskCommand, 4);
353	MOD_CHANGED(kCGEventFlagMaskHelp, 5);
354	MOD_CHANGED(kCGEventFlagMaskSecondaryFn, 6);
355	MOD_CHANGED(kCGEventFlagMaskNumericPad, 7);
356
357	return false;
358}
359
360QString GlobalShortcutMac::translateMouseButton(const unsigned int keycode) const {
361	return QString::fromLatin1("Mouse Button %1").arg(keycode-MOUSE_OFFSET+1);
362}
363
364QString GlobalShortcutMac::translateModifierKey(const unsigned int keycode) const {
365	unsigned int key = keycode - MOD_OFFSET;
366	switch (key) {
367		case 0:
368			return QLatin1String("Caps Lock");
369		case 1:
370			return QLatin1String("Shift");
371		case 2:
372			return QLatin1String("Control");
373		case 3:
374			return QLatin1String("Alt/Option");
375		case 4:
376			return QLatin1String("Command");
377		case 5:
378			return QLatin1String("Help");
379		case 6:
380			return QLatin1String("Fn");
381		case 7:
382			return QLatin1String("Num Lock");
383	}
384	return QString::fromLatin1("Modifier %1").arg(key);
385}
386
387QString GlobalShortcutMac::translateKeyName(const unsigned int keycode) const {
388	UInt32 junk = 0;
389	UniCharCount len = 64;
390	UniChar unicodeString[len];
391
392	if (! kbdLayout)
393		return QString();
394
395	OSStatus err = UCKeyTranslate(kbdLayout, static_cast<UInt16>(keycode),
396	                              kUCKeyActionDisplay, 0, LMGetKbdType(),
397	                              kUCKeyTranslateNoDeadKeysBit, &junk,
398	                              len, &len, unicodeString);
399	if (err != noErr)
400		return QString();
401
402	if (len == 1) {
403		switch (unicodeString[0]) {
404			case '\t':
405				return QLatin1String("Tab");
406			case '\r':
407				return QLatin1String("Enter");
408			case '\b':
409				return QLatin1String("Backspace");
410			case '\e':
411				return QLatin1String("Escape");
412			case ' ':
413				return QLatin1String("Space");
414			case 28:
415				return QLatin1String("Left");
416			case 29:
417				return QLatin1String("Right");
418			case 30:
419				return QLatin1String("Up");
420			case 31:
421				return QLatin1String("Down");
422		}
423
424		if (unicodeString[0] < ' ') {
425			qWarning("GlobalShortcutMac: Unknown translation for keycode %u: %u", keycode, unicodeString[0]);
426			return QString();
427		}
428	}
429
430	return QString(reinterpret_cast<const QChar *>(unicodeString), len).toUpper();
431}
432
433QString GlobalShortcutMac::buttonName(const QVariant &v) {
434	bool ok;
435	unsigned int key = v.toUInt(&ok);
436	if (!ok)
437		return QString();
438
439	if (key >= MOUSE_OFFSET)
440		return translateMouseButton(key);
441	else if (key >= MOD_OFFSET)
442		return translateModifierKey(key);
443	else {
444		QString str = translateKeyName(key);
445		if (!str.isEmpty())
446			return str;
447	}
448
449	return QString::fromLatin1("Keycode %1").arg(key);
450}
451
452void GlobalShortcutMac::setEnabled(bool b) {
453	// Since Mojave, passing NULL to CGEventTapEnable() segfaults.
454	if (port) {
455		CGEventTapEnable(port, b);
456	}
457}
458
459bool GlobalShortcutMac::enabled() {
460	if (!port) {
461		return false;
462	}
463
464	return CGEventTapIsEnabled(port);
465}
466
467bool GlobalShortcutMac::canSuppress() {
468	return true;
469}
470
471bool GlobalShortcutMac::canDisable() {
472	return true;
473}
474