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