/* SPDX-License-Identifier: GPL-2.0-or-later * Copyright 2001-2002 NaN Holding BV. All rights reserved. */ #include "GHOST_SystemCocoa.h" #include "GHOST_DisplayManagerCocoa.h" #include "GHOST_EventButton.h" #include "GHOST_EventCursor.h" #include "GHOST_EventDragnDrop.h" #include "GHOST_EventKey.h" #include "GHOST_EventString.h" #include "GHOST_EventTrackpad.h" #include "GHOST_EventWheel.h" #include "GHOST_TimerManager.h" #include "GHOST_TimerTask.h" #include "GHOST_WindowCocoa.h" #include "GHOST_WindowManager.h" #include "GHOST_ContextCGL.h" #ifdef WITH_INPUT_NDOF # include "GHOST_NDOFManagerCocoa.h" #endif #include "AssertMacros.h" #import /* For the currently not ported to Cocoa keyboard layout functions (64bit & 10.6 compatible) */ #include #include #include #include #include #pragma mark KeyMap, mouse converters static GHOST_TButton convertButton(int button) { switch (button) { case 0: return GHOST_kButtonMaskLeft; case 1: return GHOST_kButtonMaskRight; case 2: return GHOST_kButtonMaskMiddle; case 3: return GHOST_kButtonMaskButton4; case 4: return GHOST_kButtonMaskButton5; case 5: return GHOST_kButtonMaskButton6; case 6: return GHOST_kButtonMaskButton7; default: return GHOST_kButtonMaskLeft; } } /** * Converts Mac raw-key codes (same for Cocoa & Carbon) * into GHOST key codes * \param rawCode: The raw physical key code * \param recvChar: the character ignoring modifiers (except for shift) * \return Ghost key code */ static GHOST_TKey convertKey(int rawCode, unichar recvChar, UInt16 keyAction) { // printf("\nrecvchar %c 0x%x",recvChar,recvChar); switch (rawCode) { /* Physical key-codes: (not used due to map changes in int'l keyboards). */ #if 0 case kVK_ANSI_A: return GHOST_kKeyA; case kVK_ANSI_B: return GHOST_kKeyB; case kVK_ANSI_C: return GHOST_kKeyC; case kVK_ANSI_D: return GHOST_kKeyD; case kVK_ANSI_E: return GHOST_kKeyE; case kVK_ANSI_F: return GHOST_kKeyF; case kVK_ANSI_G: return GHOST_kKeyG; case kVK_ANSI_H: return GHOST_kKeyH; case kVK_ANSI_I: return GHOST_kKeyI; case kVK_ANSI_J: return GHOST_kKeyJ; case kVK_ANSI_K: return GHOST_kKeyK; case kVK_ANSI_L: return GHOST_kKeyL; case kVK_ANSI_M: return GHOST_kKeyM; case kVK_ANSI_N: return GHOST_kKeyN; case kVK_ANSI_O: return GHOST_kKeyO; case kVK_ANSI_P: return GHOST_kKeyP; case kVK_ANSI_Q: return GHOST_kKeyQ; case kVK_ANSI_R: return GHOST_kKeyR; case kVK_ANSI_S: return GHOST_kKeyS; case kVK_ANSI_T: return GHOST_kKeyT; case kVK_ANSI_U: return GHOST_kKeyU; case kVK_ANSI_V: return GHOST_kKeyV; case kVK_ANSI_W: return GHOST_kKeyW; case kVK_ANSI_X: return GHOST_kKeyX; case kVK_ANSI_Y: return GHOST_kKeyY; case kVK_ANSI_Z: return GHOST_kKeyZ; #endif /* Numbers keys: mapped to handle some int'l keyboard (e.g. French). */ case kVK_ANSI_1: return GHOST_kKey1; case kVK_ANSI_2: return GHOST_kKey2; case kVK_ANSI_3: return GHOST_kKey3; case kVK_ANSI_4: return GHOST_kKey4; case kVK_ANSI_5: return GHOST_kKey5; case kVK_ANSI_6: return GHOST_kKey6; case kVK_ANSI_7: return GHOST_kKey7; case kVK_ANSI_8: return GHOST_kKey8; case kVK_ANSI_9: return GHOST_kKey9; case kVK_ANSI_0: return GHOST_kKey0; case kVK_ANSI_Keypad0: return GHOST_kKeyNumpad0; case kVK_ANSI_Keypad1: return GHOST_kKeyNumpad1; case kVK_ANSI_Keypad2: return GHOST_kKeyNumpad2; case kVK_ANSI_Keypad3: return GHOST_kKeyNumpad3; case kVK_ANSI_Keypad4: return GHOST_kKeyNumpad4; case kVK_ANSI_Keypad5: return GHOST_kKeyNumpad5; case kVK_ANSI_Keypad6: return GHOST_kKeyNumpad6; case kVK_ANSI_Keypad7: return GHOST_kKeyNumpad7; case kVK_ANSI_Keypad8: return GHOST_kKeyNumpad8; case kVK_ANSI_Keypad9: return GHOST_kKeyNumpad9; case kVK_ANSI_KeypadDecimal: return GHOST_kKeyNumpadPeriod; case kVK_ANSI_KeypadEnter: return GHOST_kKeyNumpadEnter; case kVK_ANSI_KeypadPlus: return GHOST_kKeyNumpadPlus; case kVK_ANSI_KeypadMinus: return GHOST_kKeyNumpadMinus; case kVK_ANSI_KeypadMultiply: return GHOST_kKeyNumpadAsterisk; case kVK_ANSI_KeypadDivide: return GHOST_kKeyNumpadSlash; case kVK_ANSI_KeypadClear: return GHOST_kKeyUnknown; case kVK_F1: return GHOST_kKeyF1; case kVK_F2: return GHOST_kKeyF2; case kVK_F3: return GHOST_kKeyF3; case kVK_F4: return GHOST_kKeyF4; case kVK_F5: return GHOST_kKeyF5; case kVK_F6: return GHOST_kKeyF6; case kVK_F7: return GHOST_kKeyF7; case kVK_F8: return GHOST_kKeyF8; case kVK_F9: return GHOST_kKeyF9; case kVK_F10: return GHOST_kKeyF10; case kVK_F11: return GHOST_kKeyF11; case kVK_F12: return GHOST_kKeyF12; case kVK_F13: return GHOST_kKeyF13; case kVK_F14: return GHOST_kKeyF14; case kVK_F15: return GHOST_kKeyF15; case kVK_F16: return GHOST_kKeyF16; case kVK_F17: return GHOST_kKeyF17; case kVK_F18: return GHOST_kKeyF18; case kVK_F19: return GHOST_kKeyF19; case kVK_F20: return GHOST_kKeyF20; case kVK_UpArrow: return GHOST_kKeyUpArrow; case kVK_DownArrow: return GHOST_kKeyDownArrow; case kVK_LeftArrow: return GHOST_kKeyLeftArrow; case kVK_RightArrow: return GHOST_kKeyRightArrow; case kVK_Return: return GHOST_kKeyEnter; case kVK_Delete: return GHOST_kKeyBackSpace; case kVK_ForwardDelete: return GHOST_kKeyDelete; case kVK_Escape: return GHOST_kKeyEsc; case kVK_Tab: return GHOST_kKeyTab; case kVK_Space: return GHOST_kKeySpace; case kVK_Home: return GHOST_kKeyHome; case kVK_End: return GHOST_kKeyEnd; case kVK_PageUp: return GHOST_kKeyUpPage; case kVK_PageDown: return GHOST_kKeyDownPage; #if 0 /* TODO: why are these commented? */ case kVK_ANSI_Minus: return GHOST_kKeyMinus; case kVK_ANSI_Equal: return GHOST_kKeyEqual; case kVK_ANSI_Comma: return GHOST_kKeyComma; case kVK_ANSI_Period: return GHOST_kKeyPeriod; case kVK_ANSI_Slash: return GHOST_kKeySlash; case kVK_ANSI_Semicolon: return GHOST_kKeySemicolon; case kVK_ANSI_Quote: return GHOST_kKeyQuote; case kVK_ANSI_Backslash: return GHOST_kKeyBackslash; case kVK_ANSI_LeftBracket: return GHOST_kKeyLeftBracket; case kVK_ANSI_RightBracket: return GHOST_kKeyRightBracket; case kVK_ANSI_Grave: return GHOST_kKeyAccentGrave; case kVK_ISO_Section: return GHOST_kKeyUnknown; #endif case kVK_VolumeUp: case kVK_VolumeDown: case kVK_Mute: return GHOST_kKeyUnknown; default: { /* Alphanumerical or punctuation key that is remappable in int'l keyboards. */ if ((recvChar >= 'A') && (recvChar <= 'Z')) { return (GHOST_TKey)(recvChar - 'A' + GHOST_kKeyA); } else if ((recvChar >= 'a') && (recvChar <= 'z')) { return (GHOST_TKey)(recvChar - 'a' + GHOST_kKeyA); } else { /* Leopard and Snow Leopard 64bit compatible API. */ CFDataRef uchrHandle; /* The keyboard layout. */ TISInputSourceRef kbdTISHandle; kbdTISHandle = TISCopyCurrentKeyboardLayoutInputSource(); uchrHandle = (CFDataRef)TISGetInputSourceProperty(kbdTISHandle, kTISPropertyUnicodeKeyLayoutData); CFRelease(kbdTISHandle); /* Get actual character value of the "remappable" keys in int'l keyboards, * if keyboard layout is not correctly reported (e.g. some non Apple keyboards in Tiger), * then fallback on using the received #charactersIgnoringModifiers. */ if (uchrHandle) { UInt32 deadKeyState = 0; UniCharCount actualStrLength = 0; UCKeyTranslate((UCKeyboardLayout *)CFDataGetBytePtr(uchrHandle), rawCode, keyAction, 0, LMGetKbdType(), kUCKeyTranslateNoDeadKeysBit, &deadKeyState, 1, &actualStrLength, &recvChar); } switch (recvChar) { case '-': return GHOST_kKeyMinus; case '+': return GHOST_kKeyPlus; case '=': return GHOST_kKeyEqual; case ',': return GHOST_kKeyComma; case '.': return GHOST_kKeyPeriod; case '/': return GHOST_kKeySlash; case ';': return GHOST_kKeySemicolon; case '\'': return GHOST_kKeyQuote; case '\\': return GHOST_kKeyBackslash; case '[': return GHOST_kKeyLeftBracket; case ']': return GHOST_kKeyRightBracket; case '`': case '<': /* The position of '`' is equivalent to this symbol in the French layout. */ return GHOST_kKeyAccentGrave; default: return GHOST_kKeyUnknown; } } } } return GHOST_kKeyUnknown; } #pragma mark Utility functions #define FIRSTFILEBUFLG 512 static bool g_hasFirstFile = false; static char g_firstFileBuf[512]; // TODO: Need to investigate this. // Function called too early in creator.c to have g_hasFirstFile == true extern "C" int GHOST_HACK_getFirstFile(char buf[FIRSTFILEBUFLG]) { if (g_hasFirstFile) { strncpy(buf, g_firstFileBuf, FIRSTFILEBUFLG - 1); buf[FIRSTFILEBUFLG - 1] = '\0'; return 1; } else { return 0; } } #pragma mark Cocoa objects /** * CocoaAppDelegate * ObjC object to capture applicationShouldTerminate, and send quit event */ @interface CocoaAppDelegate : NSObject { GHOST_SystemCocoa *systemCocoa; } - (id)init; - (void)dealloc; - (void)setSystemCocoa:(GHOST_SystemCocoa *)sysCocoa; - (void)applicationDidFinishLaunching:(NSNotification *)aNotification; - (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename; - (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender; - (void)applicationWillTerminate:(NSNotification *)aNotification; - (void)applicationWillBecomeActive:(NSNotification *)aNotification; - (void)toggleFullScreen:(NSNotification *)notification; - (void)windowWillClose:(NSNotification *)notification; @end @implementation CocoaAppDelegate : NSObject - (id)init { self = [super init]; NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; [center addObserver:self selector:@selector(windowWillClose:) name:NSWindowWillCloseNotification object:nil]; return self; } - (void)dealloc { NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; [center removeObserver:self name:NSWindowWillCloseNotification object:nil]; [super dealloc]; } - (void)setSystemCocoa:(GHOST_SystemCocoa *)sysCocoa { systemCocoa = sysCocoa; } - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { if (systemCocoa->m_windowFocus) { // Raise application to front, convenient when starting from the terminal // and important for launching the animation player. we call this after the // application finishes launching, as doing it earlier can make us end up // with a frontmost window but an inactive application. [NSApp activateIgnoringOtherApps:YES]; } [NSEvent setMouseCoalescingEnabled:NO]; } - (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename { return systemCocoa->handleOpenDocumentRequest(filename); } - (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender { /* TODO: implement graceful termination through Cocoa mechanism * to avoid session log off to be canceled. */ /* Note that Cmd+Q is already handled by key-handler. */ systemCocoa->handleQuitRequest(); return NSTerminateCancel; } // To avoid canceling a log off process, we must use Cocoa termination process // And this function is the only chance to perform clean up // So WM_exit needs to be called directly, as the event loop will never run before termination - (void)applicationWillTerminate:(NSNotification *)aNotification { #if 0 G.is_break = false; /* Let Cocoa perform the termination at the end. */ WM_exit(C); #endif } - (void)applicationWillBecomeActive:(NSNotification *)aNotification { systemCocoa->handleApplicationBecomeActiveEvent(); } - (void)toggleFullScreen:(NSNotification *)notification { } // The purpose of this function is to make sure closing "About" window does not // leave Blender with no key windows. This is needed due to a custom event loop // nature of the application: for some reason only using [NSApp run] will ensure // correct behavior in this case. // // This is similar to an issue solved in SDL: // https://bugzilla.libsdl.org/show_bug.cgi?id=1825 // // Our solution is different, since we want Blender to keep track of what is // the key window during normal operation. In order to do so we exploit the // fact that "About" window is never in the orderedWindows array: we only force // key window from here if the closing one is not in the orderedWindows. This // saves lack of key windows when closing "About", but does not interfere with // Blender's window manager when closing Blender's windows. - (void)windowWillClose:(NSNotification *)notification { NSWindow *closing_window = (NSWindow *)[notification object]; NSInteger index = [[NSApp orderedWindows] indexOfObject:closing_window]; if (index != NSNotFound) { return; } // Find first suitable window from the current space. for (NSWindow *current_window in [NSApp orderedWindows]) { if (current_window == closing_window) { continue; } if ([current_window isOnActiveSpace] && [current_window canBecomeKeyWindow]) { [current_window makeKeyAndOrderFront:nil]; return; } } // If that didn't find any windows, we try to find any suitable window of // the application. for (NSNumber *window_number in [NSWindow windowNumbersWithOptions:0]) { NSWindow *current_window = [NSApp windowWithWindowNumber:[window_number integerValue]]; if (current_window == closing_window) { continue; } if ([current_window canBecomeKeyWindow]) { [current_window makeKeyAndOrderFront:nil]; return; } } } @end #pragma mark initialization/finalization GHOST_SystemCocoa::GHOST_SystemCocoa() { int mib[2]; struct timeval boottime; size_t len; char *rstring = NULL; m_modifierMask = 0; m_outsideLoopEventProcessed = false; m_needDelayedApplicationBecomeActiveEventProcessing = false; m_displayManager = new GHOST_DisplayManagerCocoa(); GHOST_ASSERT(m_displayManager, "GHOST_SystemCocoa::GHOST_SystemCocoa(): m_displayManager==0\n"); m_displayManager->initialize(); // NSEvent timeStamp is given in system uptime, state start date is boot time mib[0] = CTL_KERN; mib[1] = KERN_BOOTTIME; len = sizeof(struct timeval); sysctl(mib, 2, &boottime, &len, NULL, 0); m_start_time = ((boottime.tv_sec * 1000) + (boottime.tv_usec / 1000)); /* Detect multi-touch track-pad. */ mib[0] = CTL_HW; mib[1] = HW_MODEL; sysctl(mib, 2, NULL, &len, NULL, 0); rstring = (char *)malloc(len); sysctl(mib, 2, rstring, &len, NULL, 0); free(rstring); rstring = NULL; m_ignoreWindowSizedMessages = false; m_ignoreMomentumScroll = false; m_multiTouchScroll = false; m_last_warp_timestamp = 0; } GHOST_SystemCocoa::~GHOST_SystemCocoa() { } GHOST_TSuccess GHOST_SystemCocoa::init() { GHOST_TSuccess success = GHOST_System::init(); if (success) { #ifdef WITH_INPUT_NDOF m_ndofManager = new GHOST_NDOFManagerCocoa(*this); #endif // ProcessSerialNumber psn; // Carbon stuff to move window & menu to foreground #if 0 if (!GetCurrentProcess(&psn)) { TransformProcessType(&psn, kProcessTransformToForegroundApplication); SetFrontProcess(&psn); } #endif @autoreleasepool { [NSApplication sharedApplication]; // initializes NSApp if ([NSApp mainMenu] == nil) { NSMenu *mainMenubar = [[NSMenu alloc] init]; NSMenuItem *menuItem; NSMenu *windowMenu; NSMenu *appMenu; // Create the application menu appMenu = [[NSMenu alloc] initWithTitle:@"Blender"]; [appMenu addItemWithTitle:@"About Blender" action:@selector(orderFrontStandardAboutPanel:) keyEquivalent:@""]; [appMenu addItem:[NSMenuItem separatorItem]]; menuItem = [appMenu addItemWithTitle:@"Hide Blender" action:@selector(hide:) keyEquivalent:@"h"]; [menuItem setKeyEquivalentModifierMask:NSEventModifierFlagCommand]; menuItem = [appMenu addItemWithTitle:@"Hide Others" action:@selector(hideOtherApplications:) keyEquivalent:@"h"]; [menuItem setKeyEquivalentModifierMask:(NSEventModifierFlagOption | NSEventModifierFlagCommand)]; [appMenu addItemWithTitle:@"Show All" action:@selector(unhideAllApplications:) keyEquivalent:@""]; menuItem = [appMenu addItemWithTitle:@"Quit Blender" action:@selector(terminate:) keyEquivalent:@"q"]; [menuItem setKeyEquivalentModifierMask:NSEventModifierFlagCommand]; menuItem = [[NSMenuItem alloc] init]; [menuItem setSubmenu:appMenu]; [mainMenubar addItem:menuItem]; [menuItem release]; [NSApp performSelector:@selector(setAppleMenu:) withObject:appMenu]; // Needed for 10.5 [appMenu release]; // Create the window menu windowMenu = [[NSMenu alloc] initWithTitle:@"Window"]; menuItem = [windowMenu addItemWithTitle:@"Minimize" action:@selector(performMiniaturize:) keyEquivalent:@"m"]; [menuItem setKeyEquivalentModifierMask:NSEventModifierFlagCommand]; [windowMenu addItemWithTitle:@"Zoom" action:@selector(performZoom:) keyEquivalent:@""]; menuItem = [windowMenu addItemWithTitle:@"Enter Full Screen" action:@selector(toggleFullScreen:) keyEquivalent:@"f"]; [menuItem setKeyEquivalentModifierMask:NSEventModifierFlagControl | NSEventModifierFlagCommand]; menuItem = [windowMenu addItemWithTitle:@"Close" action:@selector(performClose:) keyEquivalent:@"w"]; [menuItem setKeyEquivalentModifierMask:NSEventModifierFlagCommand]; menuItem = [[NSMenuItem alloc] init]; [menuItem setSubmenu:windowMenu]; [mainMenubar addItem:menuItem]; [menuItem release]; [NSApp setMainMenu:mainMenubar]; [NSApp setWindowsMenu:windowMenu]; [windowMenu release]; } if ([NSApp delegate] == nil) { CocoaAppDelegate *appDelegate = [[CocoaAppDelegate alloc] init]; [appDelegate setSystemCocoa:this]; [NSApp setDelegate:appDelegate]; } // AppKit provides automatic window tabbing. Blender is a single-tabbed application // without a macOS tab bar, and should explicitly opt-out of this. This is also // controlled by the macOS user default #NSWindowTabbingEnabled. NSWindow.allowsAutomaticWindowTabbing = NO; [NSApp finishLaunching]; } } return success; } #pragma mark window management uint64_t GHOST_SystemCocoa::getMilliSeconds() const { // Cocoa equivalent exists in 10.6 ([[NSProcessInfo processInfo] systemUptime]) struct timeval currentTime; gettimeofday(¤tTime, NULL); // Return timestamp of system uptime return ((currentTime.tv_sec * 1000) + (currentTime.tv_usec / 1000) - m_start_time); } uint8_t GHOST_SystemCocoa::getNumDisplays() const { // Note that OS X supports monitor hot plug // We do not support multiple monitors at the moment @autoreleasepool { return NSScreen.screens.count; } } void GHOST_SystemCocoa::getMainDisplayDimensions(uint32_t &width, uint32_t &height) const { @autoreleasepool { // Get visible frame, that is frame excluding dock and top menu bar NSRect frame = [[NSScreen mainScreen] visibleFrame]; // Returns max window contents (excluding title bar...) NSRect contentRect = [NSWindow contentRectForFrameRect:frame styleMask:(NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable)]; width = contentRect.size.width; height = contentRect.size.height; } } void GHOST_SystemCocoa::getAllDisplayDimensions(uint32_t &width, uint32_t &height) const { /* TODO! */ getMainDisplayDimensions(width, height); } GHOST_IWindow *GHOST_SystemCocoa::createWindow(const char *title, int32_t left, int32_t top, uint32_t width, uint32_t height, GHOST_TWindowState state, GHOST_GLSettings glSettings, const bool exclusive, const bool is_dialog, const GHOST_IWindow *parentWindow) { GHOST_IWindow *window = NULL; @autoreleasepool { // Get the available rect for including window contents NSRect frame = [[NSScreen mainScreen] visibleFrame]; NSRect contentRect = [NSWindow contentRectForFrameRect:frame styleMask:(NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable)]; int32_t bottom = (contentRect.size.height - 1) - height - top; // Ensures window top left is inside this available rect left = left > contentRect.origin.x ? left : contentRect.origin.x; // Add contentRect.origin.y to respect docksize bottom = bottom > contentRect.origin.y ? bottom + contentRect.origin.y : contentRect.origin.y; window = new GHOST_WindowCocoa(this, title, left, bottom, width, height, state, glSettings.context_type, glSettings.flags & GHOST_glStereoVisual, glSettings.flags & GHOST_glDebugContext, is_dialog, (GHOST_WindowCocoa *)parentWindow); if (window->getValid()) { // Store the pointer to the window GHOST_ASSERT(m_windowManager, "m_windowManager not initialized"); m_windowManager->addWindow(window); m_windowManager->setActiveWindow(window); /* Need to tell window manager the new window is the active one * (Cocoa does not send the event activate upon window creation). */ pushEvent(new GHOST_Event(getMilliSeconds(), GHOST_kEventWindowActivate, window)); pushEvent(new GHOST_Event(getMilliSeconds(), GHOST_kEventWindowSize, window)); } else { GHOST_PRINT("GHOST_SystemCocoa::createWindow(): window invalid\n"); delete window; window = NULL; } } return window; } /** * Create a new off-screen context. * Never explicitly delete the context, use #disposeContext() instead. * \return The new context (or 0 if creation failed). */ GHOST_IContext *GHOST_SystemCocoa::createOffscreenContext(GHOST_GLSettings glSettings) { GHOST_Context *context = new GHOST_ContextCGL(false, NULL, NULL, NULL, glSettings.context_type); if (context->initializeDrawingContext()) return context; else delete context; return NULL; } /** * Dispose of a context. * \param context: Pointer to the context to be disposed. * \return Indication of success. */ GHOST_TSuccess GHOST_SystemCocoa::disposeContext(GHOST_IContext *context) { delete context; return GHOST_kSuccess; } GHOST_IWindow *GHOST_SystemCocoa::getWindowUnderCursor(int32_t x, int32_t y) { NSPoint scr_co = NSMakePoint(x, y); int windowNumberAtPoint = [NSWindow windowNumberAtPoint:scr_co belowWindowWithWindowNumber:0]; NSWindow *nswindow = [NSApp windowWithWindowNumber:windowNumberAtPoint]; if (nswindow == nil) { return nil; } return m_windowManager->getWindowAssociatedWithOSWindow((void *)nswindow); } /** * \note returns coordinates in Cocoa screen coordinates. */ GHOST_TSuccess GHOST_SystemCocoa::getCursorPosition(int32_t &x, int32_t &y) const { NSPoint mouseLoc = [NSEvent mouseLocation]; // Returns the mouse location in screen coordinates x = (int32_t)mouseLoc.x; y = (int32_t)mouseLoc.y; return GHOST_kSuccess; } /** * \note expect Cocoa screen coordinates. */ GHOST_TSuccess GHOST_SystemCocoa::setCursorPosition(int32_t x, int32_t y) { GHOST_WindowCocoa *window = (GHOST_WindowCocoa *)m_windowManager->getActiveWindow(); if (!window) return GHOST_kFailure; // Cursor and mouse dissociation placed here not to interfere with continuous grab // (in cont. grab setMouseCursorPosition is directly called) CGAssociateMouseAndMouseCursorPosition(false); setMouseCursorPosition(x, y); CGAssociateMouseAndMouseCursorPosition(true); // Force mouse move event (not pushed by Cocoa) pushEvent(new GHOST_EventCursor( getMilliSeconds(), GHOST_kEventCursorMove, window, x, y, window->GetCocoaTabletData())); m_outsideLoopEventProcessed = true; return GHOST_kSuccess; } GHOST_TSuccess GHOST_SystemCocoa::setMouseCursorPosition(int32_t x, int32_t y) { float xf = (float)x, yf = (float)y; GHOST_WindowCocoa *window = (GHOST_WindowCocoa *)m_windowManager->getActiveWindow(); if (!window) return GHOST_kFailure; @autoreleasepool { NSScreen *windowScreen = window->getScreen(); NSRect screenRect = [windowScreen frame]; // Set position relative to current screen xf -= screenRect.origin.x; yf -= screenRect.origin.y; // Quartz Display Services uses the old coordinates (top left origin) yf = screenRect.size.height - yf; CGDisplayMoveCursorToPoint((CGDirectDisplayID)[[[windowScreen deviceDescription] objectForKey:@"NSScreenNumber"] unsignedIntValue], CGPointMake(xf, yf)); // See https://stackoverflow.com/a/17559012. By default, hardware events // will be suppressed for 500ms after a synthetic mouse event. For unknown // reasons CGEventSourceSetLocalEventsSuppressionInterval does not work, // however calling CGAssociateMouseAndMouseCursorPosition also removes the // delay, even if this is undocumented. CGAssociateMouseAndMouseCursorPosition(true); } return GHOST_kSuccess; } GHOST_TSuccess GHOST_SystemCocoa::getModifierKeys(GHOST_ModifierKeys &keys) const { keys.set(GHOST_kModifierKeyLeftOS, (m_modifierMask & NSEventModifierFlagCommand) ? true : false); keys.set(GHOST_kModifierKeyLeftAlt, (m_modifierMask & NSEventModifierFlagOption) ? true : false); keys.set(GHOST_kModifierKeyLeftShift, (m_modifierMask & NSEventModifierFlagShift) ? true : false); keys.set(GHOST_kModifierKeyLeftControl, (m_modifierMask & NSEventModifierFlagControl) ? true : false); return GHOST_kSuccess; } GHOST_TSuccess GHOST_SystemCocoa::getButtons(GHOST_Buttons &buttons) const { UInt32 button_state = GetCurrentEventButtonState(); buttons.clear(); buttons.set(GHOST_kButtonMaskLeft, button_state & (1 << 0)); buttons.set(GHOST_kButtonMaskRight, button_state & (1 << 1)); buttons.set(GHOST_kButtonMaskMiddle, button_state & (1 << 2)); buttons.set(GHOST_kButtonMaskButton4, button_state & (1 << 3)); buttons.set(GHOST_kButtonMaskButton5, button_state & (1 << 4)); return GHOST_kSuccess; } #pragma mark Event handlers /** * The event queue polling function */ bool GHOST_SystemCocoa::processEvents(bool waitForEvent) { bool anyProcessed = false; NSEvent *event; /* TODO: implement timer? */ #if 0 do { GHOST_TimerManager* timerMgr = getTimerManager(); if (waitForEvent) { uint64_t next = timerMgr->nextFireTime(); double timeOut; if (next == GHOST_kFireTimeNever) { timeOut = kEventDurationForever; } else { timeOut = (double)(next - getMilliSeconds())/1000.0; if (timeOut < 0.0) timeOut = 0.0; } ::ReceiveNextEvent(0, NULL, timeOut, false, &event); } if (timerMgr->fireTimers(getMilliSeconds())) { anyProcessed = true; } #endif do { @autoreleasepool { event = [NSApp nextEventMatchingMask:NSEventMaskAny untilDate:[NSDate distantPast] inMode:NSDefaultRunLoopMode dequeue:YES]; if (event == nil) { break; } anyProcessed = true; // Send event to NSApp to ensure Mac wide events are handled, // this will send events to CocoaWindow which will call back // to handleKeyEvent, handleMouseEvent and handleTabletEvent // There is on special exception for ctrl+(shift)+tab. We do not // get keyDown events delivered to the view because they are // special hotkeys to switch between views, so override directly if ([event type] == NSEventTypeKeyDown && [event keyCode] == kVK_Tab && ([event modifierFlags] & NSEventModifierFlagControl)) { handleKeyEvent(event); } else { // For some reason NSApp is swallowing the key up events when modifier // key is pressed, even if there seems to be no apparent reason to do // so, as a workaround we always handle these up events. if ([event type] == NSEventTypeKeyUp && ([event modifierFlags] & (NSEventModifierFlagCommand | NSEventModifierFlagOption))) handleKeyEvent(event); [NSApp sendEvent:event]; } } } while (event != nil); #if 0 } while (waitForEvent && !anyProcessed); // Needed only for timer implementation #endif if (m_needDelayedApplicationBecomeActiveEventProcessing) handleApplicationBecomeActiveEvent(); if (m_outsideLoopEventProcessed) { m_outsideLoopEventProcessed = false; return true; } m_ignoreWindowSizedMessages = false; return anyProcessed; } /* NOTE: called from #NSApplication delegate. */ GHOST_TSuccess GHOST_SystemCocoa::handleApplicationBecomeActiveEvent() { for (GHOST_IWindow *iwindow : m_windowManager->getWindows()) { GHOST_WindowCocoa *window = (GHOST_WindowCocoa *)iwindow; if (window->isDialog()) { [window->getCocoaWindow() makeKeyAndOrderFront:nil]; } } // Update the modifiers key mask, as its status may have changed when the application // was not active (that is when update events are sent to another application). unsigned int modifiers; GHOST_IWindow *window = m_windowManager->getActiveWindow(); if (!window) { m_needDelayedApplicationBecomeActiveEventProcessing = true; return GHOST_kFailure; } else m_needDelayedApplicationBecomeActiveEventProcessing = false; modifiers = [[[NSApplication sharedApplication] currentEvent] modifierFlags]; if ((modifiers & NSEventModifierFlagShift) != (m_modifierMask & NSEventModifierFlagShift)) { pushEvent(new GHOST_EventKey(getMilliSeconds(), (modifiers & NSEventModifierFlagShift) ? GHOST_kEventKeyDown : GHOST_kEventKeyUp, window, GHOST_kKeyLeftShift, false)); } if ((modifiers & NSEventModifierFlagControl) != (m_modifierMask & NSEventModifierFlagControl)) { pushEvent(new GHOST_EventKey(getMilliSeconds(), (modifiers & NSEventModifierFlagControl) ? GHOST_kEventKeyDown : GHOST_kEventKeyUp, window, GHOST_kKeyLeftControl, false)); } if ((modifiers & NSEventModifierFlagOption) != (m_modifierMask & NSEventModifierFlagOption)) { pushEvent(new GHOST_EventKey(getMilliSeconds(), (modifiers & NSEventModifierFlagOption) ? GHOST_kEventKeyDown : GHOST_kEventKeyUp, window, GHOST_kKeyLeftAlt, false)); } if ((modifiers & NSEventModifierFlagCommand) != (m_modifierMask & NSEventModifierFlagCommand)) { pushEvent(new GHOST_EventKey(getMilliSeconds(), (modifiers & NSEventModifierFlagCommand) ? GHOST_kEventKeyDown : GHOST_kEventKeyUp, window, GHOST_kKeyLeftOS, false)); } m_modifierMask = modifiers; m_outsideLoopEventProcessed = true; return GHOST_kSuccess; } bool GHOST_SystemCocoa::hasDialogWindow() { for (GHOST_IWindow *iwindow : m_windowManager->getWindows()) { GHOST_WindowCocoa *window = (GHOST_WindowCocoa *)iwindow; if (window->isDialog()) { return true; } } return false; } void GHOST_SystemCocoa::notifyExternalEventProcessed() { m_outsideLoopEventProcessed = true; } /* NOTE: called from #NSWindow delegate. */ GHOST_TSuccess GHOST_SystemCocoa::handleWindowEvent(GHOST_TEventType eventType, GHOST_WindowCocoa *window) { if (!validWindow(window)) { return GHOST_kFailure; } switch (eventType) { case GHOST_kEventWindowClose: pushEvent(new GHOST_Event(getMilliSeconds(), GHOST_kEventWindowClose, window)); break; case GHOST_kEventWindowActivate: m_windowManager->setActiveWindow(window); window->loadCursor(window->getCursorVisibility(), window->getCursorShape()); pushEvent(new GHOST_Event(getMilliSeconds(), GHOST_kEventWindowActivate, window)); break; case GHOST_kEventWindowDeactivate: m_windowManager->setWindowInactive(window); pushEvent(new GHOST_Event(getMilliSeconds(), GHOST_kEventWindowDeactivate, window)); break; case GHOST_kEventWindowUpdate: if (m_nativePixel) { window->setNativePixelSize(); pushEvent(new GHOST_Event(getMilliSeconds(), GHOST_kEventNativeResolutionChange, window)); } pushEvent(new GHOST_Event(getMilliSeconds(), GHOST_kEventWindowUpdate, window)); break; case GHOST_kEventWindowMove: pushEvent(new GHOST_Event(getMilliSeconds(), GHOST_kEventWindowMove, window)); break; case GHOST_kEventWindowSize: if (!m_ignoreWindowSizedMessages) { // Enforce only one resize message per event loop // (coalescing all the live resize messages) window->updateDrawingContext(); pushEvent(new GHOST_Event(getMilliSeconds(), GHOST_kEventWindowSize, window)); // Mouse up event is trapped by the resizing event loop, // so send it anyway to the window manager. pushEvent(new GHOST_EventButton(getMilliSeconds(), GHOST_kEventButtonUp, window, GHOST_kButtonMaskLeft, GHOST_TABLET_DATA_NONE)); // m_ignoreWindowSizedMessages = true; } break; case GHOST_kEventNativeResolutionChange: if (m_nativePixel) { window->setNativePixelSize(); pushEvent(new GHOST_Event(getMilliSeconds(), GHOST_kEventNativeResolutionChange, window)); } default: return GHOST_kFailure; break; } m_outsideLoopEventProcessed = true; return GHOST_kSuccess; } /* NOTE: called from #NSWindow subclass. */ GHOST_TSuccess GHOST_SystemCocoa::handleDraggingEvent(GHOST_TEventType eventType, GHOST_TDragnDropTypes draggedObjectType, GHOST_WindowCocoa *window, int mouseX, int mouseY, void *data) { if (!validWindow(window)) { return GHOST_kFailure; } switch (eventType) { case GHOST_kEventDraggingEntered: case GHOST_kEventDraggingUpdated: case GHOST_kEventDraggingExited: window->clientToScreenIntern(mouseX, mouseY, mouseX, mouseY); pushEvent(new GHOST_EventDragnDrop( getMilliSeconds(), eventType, draggedObjectType, window, mouseX, mouseY, NULL)); break; case GHOST_kEventDraggingDropDone: { uint8_t *temp_buff; GHOST_TStringArray *strArray; NSArray *droppedArray; size_t pastedTextSize; NSString *droppedStr; GHOST_TEventDataPtr eventData; int i; if (!data) return GHOST_kFailure; switch (draggedObjectType) { case GHOST_kDragnDropTypeFilenames: droppedArray = (NSArray *)data; strArray = (GHOST_TStringArray *)malloc(sizeof(GHOST_TStringArray)); if (!strArray) return GHOST_kFailure; strArray->count = [droppedArray count]; if (strArray->count == 0) { free(strArray); return GHOST_kFailure; } strArray->strings = (uint8_t **)malloc(strArray->count * sizeof(uint8_t *)); for (i = 0; i < strArray->count; i++) { droppedStr = [droppedArray objectAtIndex:i]; pastedTextSize = [droppedStr lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; temp_buff = (uint8_t *)malloc(pastedTextSize + 1); if (!temp_buff) { strArray->count = i; break; } strncpy((char *)temp_buff, [droppedStr cStringUsingEncoding:NSUTF8StringEncoding], pastedTextSize); temp_buff[pastedTextSize] = '\0'; strArray->strings[i] = temp_buff; } eventData = (GHOST_TEventDataPtr)strArray; break; case GHOST_kDragnDropTypeString: droppedStr = (NSString *)data; pastedTextSize = [droppedStr lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; temp_buff = (uint8_t *)malloc(pastedTextSize + 1); if (temp_buff == NULL) { return GHOST_kFailure; } strncpy((char *)temp_buff, [droppedStr cStringUsingEncoding:NSUTF8StringEncoding], pastedTextSize); temp_buff[pastedTextSize] = '\0'; eventData = (GHOST_TEventDataPtr)temp_buff; break; case GHOST_kDragnDropTypeBitmap: { NSImage *droppedImg = (NSImage *)data; NSSize imgSize = [droppedImg size]; ImBuf *ibuf = NULL; uint8_t *rasterRGB = NULL; uint8_t *rasterRGBA = NULL; uint8_t *toIBuf = NULL; int x, y, to_i, from_i; NSBitmapImageRep *blBitmapFormatImageRGB, *blBitmapFormatImageRGBA, *bitmapImage = nil; NSEnumerator *enumerator; NSImageRep *representation; ibuf = IMB_allocImBuf(imgSize.width, imgSize.height, 32, IB_rect); if (!ibuf) { [droppedImg release]; return GHOST_kFailure; } /* Get the bitmap of the image. */ enumerator = [[droppedImg representations] objectEnumerator]; while ((representation = [enumerator nextObject])) { if ([representation isKindOfClass:[NSBitmapImageRep class]]) { bitmapImage = (NSBitmapImageRep *)representation; break; } } if (bitmapImage == nil) return GHOST_kFailure; if (([bitmapImage bitsPerPixel] == 32) && (([bitmapImage bitmapFormat] & 0x5) == 0) && ![bitmapImage isPlanar]) { /* Try a fast copy if the image is a meshed RGBA 32bit bitmap. */ toIBuf = (uint8_t *)ibuf->rect; rasterRGB = (uint8_t *)[bitmapImage bitmapData]; for (y = 0; y < imgSize.height; y++) { to_i = (imgSize.height - y - 1) * imgSize.width; from_i = y * imgSize.width; memcpy(toIBuf + 4 * to_i, rasterRGB + 4 * from_i, 4 * imgSize.width); } } else { /* Tell cocoa image resolution is same as current system one */ [bitmapImage setSize:imgSize]; /* Convert the image in a RGBA 32bit format */ /* As Core Graphics does not support contexts with non premutliplied alpha, * we need to get alpha key values in a separate batch */ /* First get RGB values w/o Alpha to avoid pre-multiplication, * 32bit but last byte is unused */ blBitmapFormatImageRGB = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL pixelsWide:imgSize.width pixelsHigh:imgSize.height bitsPerSample:8 samplesPerPixel:3 hasAlpha:NO isPlanar:NO colorSpaceName:NSDeviceRGBColorSpace bitmapFormat:(NSBitmapFormat)0 bytesPerRow:4 * imgSize.width bitsPerPixel:32 /* RGB format padded to 32bits. */]; [NSGraphicsContext saveGraphicsState]; [NSGraphicsContext setCurrentContext:[NSGraphicsContext graphicsContextWithBitmapImageRep:blBitmapFormatImageRGB]]; [bitmapImage draw]; [NSGraphicsContext restoreGraphicsState]; rasterRGB = (uint8_t *)[blBitmapFormatImageRGB bitmapData]; if (rasterRGB == NULL) { [bitmapImage release]; [blBitmapFormatImageRGB release]; [droppedImg release]; return GHOST_kFailure; } /* Then get Alpha values by getting the RGBA image (that is pre-multiplied BTW) */ blBitmapFormatImageRGBA = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL pixelsWide:imgSize.width pixelsHigh:imgSize.height bitsPerSample:8 samplesPerPixel:4 hasAlpha:YES isPlanar:NO colorSpaceName:NSDeviceRGBColorSpace bitmapFormat:(NSBitmapFormat)0 bytesPerRow:4 * imgSize.width bitsPerPixel:32 /* RGBA */]; [NSGraphicsContext saveGraphicsState]; [NSGraphicsContext setCurrentContext:[NSGraphicsContext graphicsContextWithBitmapImageRep:blBitmapFormatImageRGBA]]; [bitmapImage draw]; [NSGraphicsContext restoreGraphicsState]; rasterRGBA = (uint8_t *)[blBitmapFormatImageRGBA bitmapData]; if (rasterRGBA == NULL) { [bitmapImage release]; [blBitmapFormatImageRGB release]; [blBitmapFormatImageRGBA release]; [droppedImg release]; return GHOST_kFailure; } /* Copy the image to ibuf, flipping it vertically. */ toIBuf = (uint8_t *)ibuf->rect; for (y = 0; y < imgSize.height; y++) { for (x = 0; x < imgSize.width; x++) { to_i = (imgSize.height - y - 1) * imgSize.width + x; from_i = y * imgSize.width + x; toIBuf[4 * to_i] = rasterRGB[4 * from_i]; /* R */ toIBuf[4 * to_i + 1] = rasterRGB[4 * from_i + 1]; /* G */ toIBuf[4 * to_i + 2] = rasterRGB[4 * from_i + 2]; /* B */ toIBuf[4 * to_i + 3] = rasterRGBA[4 * from_i + 3]; /* A */ } } [blBitmapFormatImageRGB release]; [blBitmapFormatImageRGBA release]; [droppedImg release]; } eventData = (GHOST_TEventDataPtr)ibuf; break; } default: return GHOST_kFailure; break; } window->clientToScreenIntern(mouseX, mouseY, mouseX, mouseY); pushEvent(new GHOST_EventDragnDrop( getMilliSeconds(), eventType, draggedObjectType, window, mouseX, mouseY, eventData)); break; } default: return GHOST_kFailure; } m_outsideLoopEventProcessed = true; return GHOST_kSuccess; } void GHOST_SystemCocoa::handleQuitRequest() { GHOST_Window *window = (GHOST_Window *)m_windowManager->getActiveWindow(); // Discard quit event if we are in cursor grab sequence if (window && window->getCursorGrabModeIsWarp()) return; // Push the event to Blender so it can open a dialog if needed pushEvent(new GHOST_Event(getMilliSeconds(), GHOST_kEventQuitRequest, window)); m_outsideLoopEventProcessed = true; } bool GHOST_SystemCocoa::handleOpenDocumentRequest(void *filepathStr) { NSString *filepath = (NSString *)filepathStr; NSArray *windowsList; char *temp_buff; size_t filenameTextSize; /* Check for blender opened windows and make the frontmost key. In case blender * is minimized, opened on another desktop space, or in full-screen mode. */ windowsList = [NSApp orderedWindows]; if ([windowsList count]) { [[windowsList objectAtIndex:0] makeKeyAndOrderFront:nil]; } GHOST_Window *window = (GHOST_Window *)m_windowManager->getActiveWindow(); if (!window) { return NO; } /* Discard event if we are in cursor grab sequence, * it'll lead to "stuck cursor" situation if the alert panel is raised. */ if (window && window->getCursorGrabModeIsWarp()) { return NO; } filenameTextSize = [filepath lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; temp_buff = (char *)malloc(filenameTextSize + 1); if (temp_buff == NULL) { return GHOST_kFailure; } strncpy(temp_buff, [filepath cStringUsingEncoding:NSUTF8StringEncoding], filenameTextSize); temp_buff[filenameTextSize] = '\0'; pushEvent(new GHOST_EventString( getMilliSeconds(), GHOST_kEventOpenMainFile, window, (GHOST_TEventDataPtr)temp_buff)); return YES; } GHOST_TSuccess GHOST_SystemCocoa::handleTabletEvent(void *eventPtr, short eventType) { NSEvent *event = (NSEvent *)eventPtr; GHOST_IWindow *window; window = m_windowManager->getWindowAssociatedWithOSWindow((void *)[event window]); if (!window) { // printf("\nW failure for event 0x%x",[event type]); return GHOST_kFailure; } GHOST_TabletData &ct = ((GHOST_WindowCocoa *)window)->GetCocoaTabletData(); switch (eventType) { case NSEventTypeTabletPoint: // workaround 2 cornercases: // 1. if [event isEnteringProximity] was not triggered since program-start // 2. device is not sending [event pointingDeviceType], due no eraser if (ct.Active == GHOST_kTabletModeNone) ct.Active = GHOST_kTabletModeStylus; ct.Pressure = [event pressure]; ct.Xtilt = [event tilt].x; ct.Ytilt = [event tilt].y; break; case NSEventTypeTabletProximity: /* Reset tablet data when device enters proximity or leaves. */ ct = GHOST_TABLET_DATA_NONE; if ([event isEnteringProximity]) { /* Pointer is entering tablet area proximity. */ switch ([event pointingDeviceType]) { case NSPointingDeviceTypePen: ct.Active = GHOST_kTabletModeStylus; break; case NSPointingDeviceTypeEraser: ct.Active = GHOST_kTabletModeEraser; break; case NSPointingDeviceTypeCursor: case NSPointingDeviceTypeUnknown: default: break; } } break; default: GHOST_ASSERT(FALSE, "GHOST_SystemCocoa::handleTabletEvent : unknown event received"); return GHOST_kFailure; break; } return GHOST_kSuccess; } bool GHOST_SystemCocoa::handleTabletEvent(void *eventPtr) { NSEvent *event = (NSEvent *)eventPtr; switch ([event subtype]) { case NSEventSubtypeTabletPoint: handleTabletEvent(eventPtr, NSEventTypeTabletPoint); return true; case NSEventSubtypeTabletProximity: handleTabletEvent(eventPtr, NSEventTypeTabletProximity); return true; default: // No tablet event included : do nothing return false; } } GHOST_TSuccess GHOST_SystemCocoa::handleMouseEvent(void *eventPtr) { NSEvent *event = (NSEvent *)eventPtr; GHOST_WindowCocoa *window; CocoaWindow *cocoawindow; /* [event window] returns other windows if mouse-over, that's OSX input standard * however, if mouse exits window(s), the windows become inactive, until you click. * We then fall back to the active window from ghost. */ window = (GHOST_WindowCocoa *)m_windowManager->getWindowAssociatedWithOSWindow( (void *)[event window]); if (!window) { window = (GHOST_WindowCocoa *)m_windowManager->getActiveWindow(); if (!window) { // printf("\nW failure for event 0x%x",[event type]); return GHOST_kFailure; } } cocoawindow = (CocoaWindow *)window->getOSWindow(); switch ([event type]) { case NSEventTypeLeftMouseDown: handleTabletEvent(event); // Update window tablet state to be included in event. pushEvent(new GHOST_EventButton([event timestamp] * 1000, GHOST_kEventButtonDown, window, GHOST_kButtonMaskLeft, window -> GetCocoaTabletData())); break; case NSEventTypeRightMouseDown: handleTabletEvent(event); // Update window tablet state to be included in event. pushEvent(new GHOST_EventButton([event timestamp] * 1000, GHOST_kEventButtonDown, window, GHOST_kButtonMaskRight, window -> GetCocoaTabletData())); break; case NSEventTypeOtherMouseDown: handleTabletEvent(event); // Handle tablet events combined with mouse events pushEvent(new GHOST_EventButton([event timestamp] * 1000, GHOST_kEventButtonDown, window, convertButton([event buttonNumber]), window -> GetCocoaTabletData())); break; case NSEventTypeLeftMouseUp: handleTabletEvent(event); // Update window tablet state to be included in event. pushEvent(new GHOST_EventButton([event timestamp] * 1000, GHOST_kEventButtonUp, window, GHOST_kButtonMaskLeft, window -> GetCocoaTabletData())); break; case NSEventTypeRightMouseUp: handleTabletEvent(event); // Update window tablet state to be included in event. pushEvent(new GHOST_EventButton([event timestamp] * 1000, GHOST_kEventButtonUp, window, GHOST_kButtonMaskRight, window -> GetCocoaTabletData())); break; case NSEventTypeOtherMouseUp: handleTabletEvent(event); // Update window tablet state to be included in event. pushEvent(new GHOST_EventButton([event timestamp] * 1000, GHOST_kEventButtonUp, window, convertButton([event buttonNumber]), window -> GetCocoaTabletData())); break; case NSEventTypeLeftMouseDragged: case NSEventTypeRightMouseDragged: case NSEventTypeOtherMouseDragged: handleTabletEvent(event); // Update window tablet state to be included in event. case NSEventTypeMouseMoved: { GHOST_TGrabCursorMode grab_mode = window->getCursorGrabMode(); /* TODO: CHECK IF THIS IS A TABLET EVENT */ bool is_tablet = false; if (is_tablet && window->getCursorGrabModeIsWarp()) { grab_mode = GHOST_kGrabDisable; } switch (grab_mode) { case GHOST_kGrabHide: // Cursor hidden grab operation : no cursor move { int32_t x_warp, y_warp, x_accum, y_accum, x, y; window->getCursorGrabInitPos(x_warp, y_warp); window->screenToClientIntern(x_warp, y_warp, x_warp, y_warp); window->getCursorGrabAccum(x_accum, y_accum); x_accum += [event deltaX]; y_accum += -[event deltaY]; // Strange Apple implementation (inverted coordinates for the deltaY) ... window->setCursorGrabAccum(x_accum, y_accum); window->clientToScreenIntern(x_warp + x_accum, y_warp + y_accum, x, y); pushEvent(new GHOST_EventCursor([event timestamp] * 1000, GHOST_kEventCursorMove, window, x, y, window -> GetCocoaTabletData())); break; } case GHOST_kGrabWrap: // Wrap cursor at area/window boundaries { NSTimeInterval timestamp = [event timestamp]; if (timestamp < m_last_warp_timestamp) { /* After warping we can still receive older unwrapped mouse events, * ignore those. */ break; } NSPoint mousePos = [event locationInWindow]; int32_t x_mouse = mousePos.x; int32_t y_mouse = mousePos.y; GHOST_Rect bounds, windowBounds, correctedBounds; /* fallback to window bounds */ if (window->getCursorGrabBounds(bounds) == GHOST_kFailure) window->getClientBounds(bounds); /* Switch back to Cocoa coordinates orientation * (y=0 at bottom, the same as blender internal BTW!), and to client coordinates. */ window->getClientBounds(windowBounds); window->screenToClient(bounds.m_l, bounds.m_b, correctedBounds.m_l, correctedBounds.m_t); window->screenToClient(bounds.m_r, bounds.m_t, correctedBounds.m_r, correctedBounds.m_b); correctedBounds.m_b = (windowBounds.m_b - windowBounds.m_t) - correctedBounds.m_b; correctedBounds.m_t = (windowBounds.m_b - windowBounds.m_t) - correctedBounds.m_t; // Get accumulation from previous mouse warps int32_t x_accum, y_accum; window->getCursorGrabAccum(x_accum, y_accum); // Warp mouse cursor if needed int32_t warped_x_mouse = x_mouse; int32_t warped_y_mouse = y_mouse; correctedBounds.wrapPoint( warped_x_mouse, warped_y_mouse, 4, window->getCursorGrabAxis()); // Set new cursor position if (x_mouse != warped_x_mouse || y_mouse != warped_y_mouse) { int32_t warped_x, warped_y; window->clientToScreenIntern(warped_x_mouse, warped_y_mouse, warped_x, warped_y); setMouseCursorPosition(warped_x, warped_y); /* wrap */ window->setCursorGrabAccum(x_accum + (x_mouse - warped_x_mouse), y_accum + (y_mouse - warped_y_mouse)); /* This is the current time that matches NSEvent timestamp. */ m_last_warp_timestamp = [[NSProcessInfo processInfo] systemUptime]; } // Generate event int32_t x, y; window->clientToScreenIntern(x_mouse + x_accum, y_mouse + y_accum, x, y); pushEvent(new GHOST_EventCursor([event timestamp] * 1000, GHOST_kEventCursorMove, window, x, y, window -> GetCocoaTabletData())); break; } default: { // Normal cursor operation: send mouse position in window NSPoint mousePos = [event locationInWindow]; int32_t x, y; window->clientToScreenIntern(mousePos.x, mousePos.y, x, y); pushEvent(new GHOST_EventCursor([event timestamp] * 1000, GHOST_kEventCursorMove, window, x, y, window -> GetCocoaTabletData())); break; } } } break; case NSEventTypeScrollWheel: { NSEventPhase momentumPhase = NSEventPhaseNone; NSEventPhase phase = NSEventPhaseNone; momentumPhase = [event momentumPhase]; phase = [event phase]; /* when pressing a key while momentum scrolling continues after * lifting fingers off the trackpad, the action can unexpectedly * change from e.g. scrolling to zooming. this works around the * issue by ignoring momentum scroll after a key press */ if (momentumPhase) { if (m_ignoreMomentumScroll) break; } else { m_ignoreMomentumScroll = false; } /* we assume phases are only set for gestures from trackpad or magic * mouse events. note that using tablet at the same time may not work * since this is a static variable */ if (phase == NSEventPhaseBegan && m_multitouchGestures) m_multiTouchScroll = true; else if (phase == NSEventPhaseEnded) m_multiTouchScroll = false; /* Standard scroll-wheel case, if no swiping happened, * and no momentum (kinetic scroll) works. */ if (!m_multiTouchScroll && momentumPhase == NSEventPhaseNone) { int32_t delta; double deltaF = [event deltaY]; if (deltaF == 0.0) deltaF = [event deltaX]; // make blender decide if it's horizontal scroll if (deltaF == 0.0) break; // discard trackpad delta=0 events delta = deltaF > 0.0 ? 1 : -1; pushEvent(new GHOST_EventWheel([event timestamp] * 1000, window, delta)); } else { NSPoint mousePos = [event locationInWindow]; int32_t x, y; double dx; double dy; /* with 10.7 nice scrolling deltas are supported */ dx = [event scrollingDeltaX]; dy = [event scrollingDeltaY]; /* However, Wacom tablet (intuos5) needs old deltas, * it then has momentum and phase at zero. */ if (phase == NSEventPhaseNone && momentumPhase == NSEventPhaseNone) { dx = [event deltaX]; dy = [event deltaY]; } window->clientToScreenIntern(mousePos.x, mousePos.y, x, y); NSPoint delta = [[cocoawindow contentView] convertPointToBacking:NSMakePoint(dx, dy)]; pushEvent(new GHOST_EventTrackpad([event timestamp] * 1000, window, GHOST_kTrackpadEventScroll, x, y, delta.x, delta.y, [event isDirectionInvertedFromDevice])); } } break; case NSEventTypeMagnify: { NSPoint mousePos = [event locationInWindow]; int32_t x, y; window->clientToScreenIntern(mousePos.x, mousePos.y, x, y); pushEvent(new GHOST_EventTrackpad([event timestamp] * 1000, window, GHOST_kTrackpadEventMagnify, x, y, [event magnification] * 125.0 + 0.1, 0, false)); } break; case NSEventTypeSmartMagnify: { NSPoint mousePos = [event locationInWindow]; int32_t x, y; window->clientToScreenIntern(mousePos.x, mousePos.y, x, y); pushEvent(new GHOST_EventTrackpad( [event timestamp] * 1000, window, GHOST_kTrackpadEventSmartMagnify, x, y, 0, 0, false)); } break; case NSEventTypeRotate: { NSPoint mousePos = [event locationInWindow]; int32_t x, y; window->clientToScreenIntern(mousePos.x, mousePos.y, x, y); pushEvent(new GHOST_EventTrackpad([event timestamp] * 1000, window, GHOST_kTrackpadEventRotate, x, y, [event rotation] * -5.0, 0, false)); } default: return GHOST_kFailure; break; } return GHOST_kSuccess; } GHOST_TSuccess GHOST_SystemCocoa::handleKeyEvent(void *eventPtr) { NSEvent *event = (NSEvent *)eventPtr; GHOST_IWindow *window; unsigned int modifiers; NSString *characters; NSData *convertedCharacters; GHOST_TKey keyCode; NSString *charsIgnoringModifiers; window = m_windowManager->getWindowAssociatedWithOSWindow((void *)[event window]); if (!window) { // printf("\nW failure for event 0x%x",[event type]); return GHOST_kFailure; } char utf8_buf[6] = {'\0'}; switch ([event type]) { case NSEventTypeKeyDown: case NSEventTypeKeyUp: charsIgnoringModifiers = [event charactersIgnoringModifiers]; if ([charsIgnoringModifiers length] > 0) { keyCode = convertKey([event keyCode], [charsIgnoringModifiers characterAtIndex:0], [event type] == NSEventTypeKeyDown ? kUCKeyActionDown : kUCKeyActionUp); } else { keyCode = convertKey([event keyCode], 0, [event type] == NSEventTypeKeyDown ? kUCKeyActionDown : kUCKeyActionUp); } characters = [event characters]; if ([characters length] > 0) { convertedCharacters = [characters dataUsingEncoding:NSUTF8StringEncoding]; for (int x = 0; x < [convertedCharacters length]; x++) { utf8_buf[x] = ((char *)[convertedCharacters bytes])[x]; } } /* arrow keys should not have utf8 */ if ((keyCode >= GHOST_kKeyLeftArrow) && (keyCode <= GHOST_kKeyDownArrow)) { utf8_buf[0] = '\0'; } /* F keys should not have utf8 */ if ((keyCode >= GHOST_kKeyF1) && (keyCode <= GHOST_kKeyF20)) utf8_buf[0] = '\0'; /* no text with command key pressed */ if (m_modifierMask & NSEventModifierFlagCommand) utf8_buf[0] = '\0'; if ((keyCode == GHOST_kKeyQ) && (m_modifierMask & NSEventModifierFlagCommand)) break; // Cmd-Q is directly handled by Cocoa if ([event type] == NSEventTypeKeyDown) { pushEvent(new GHOST_EventKey([event timestamp] * 1000, GHOST_kEventKeyDown, window, keyCode, [event isARepeat], utf8_buf)); #if 0 printf("Key down rawCode=0x%x charsIgnoringModifiers=%c keyCode=%u utf8=%s\n", [event keyCode], [charsIgnoringModifiers length] > 0 ? [charsIgnoringModifiers characterAtIndex:0] : ' ', keyCode, utf8_buf); #endif } else { pushEvent(new GHOST_EventKey( [event timestamp] * 1000, GHOST_kEventKeyUp, window, keyCode, false, NULL)); #if 0 printf("Key up rawCode=0x%x charsIgnoringModifiers=%c keyCode=%u utf8=%s\n", [event keyCode], [charsIgnoringModifiers length] > 0 ? [charsIgnoringModifiers characterAtIndex:0] : ' ', keyCode, utf8_buf); #endif } m_ignoreMomentumScroll = true; break; case NSEventTypeFlagsChanged: modifiers = [event modifierFlags]; if ((modifiers & NSEventModifierFlagShift) != (m_modifierMask & NSEventModifierFlagShift)) { pushEvent(new GHOST_EventKey([event timestamp] * 1000, (modifiers & NSEventModifierFlagShift) ? GHOST_kEventKeyDown : GHOST_kEventKeyUp, window, GHOST_kKeyLeftShift, false)); } if ((modifiers & NSEventModifierFlagControl) != (m_modifierMask & NSEventModifierFlagControl)) { pushEvent(new GHOST_EventKey( [event timestamp] * 1000, (modifiers & NSEventModifierFlagControl) ? GHOST_kEventKeyDown : GHOST_kEventKeyUp, window, GHOST_kKeyLeftControl, false)); } if ((modifiers & NSEventModifierFlagOption) != (m_modifierMask & NSEventModifierFlagOption)) { pushEvent(new GHOST_EventKey( [event timestamp] * 1000, (modifiers & NSEventModifierFlagOption) ? GHOST_kEventKeyDown : GHOST_kEventKeyUp, window, GHOST_kKeyLeftAlt, false)); } if ((modifiers & NSEventModifierFlagCommand) != (m_modifierMask & NSEventModifierFlagCommand)) { pushEvent(new GHOST_EventKey( [event timestamp] * 1000, (modifiers & NSEventModifierFlagCommand) ? GHOST_kEventKeyDown : GHOST_kEventKeyUp, window, GHOST_kKeyLeftOS, false)); } m_modifierMask = modifiers; m_ignoreMomentumScroll = true; break; default: return GHOST_kFailure; break; } return GHOST_kSuccess; } #pragma mark Clipboard get/set char *GHOST_SystemCocoa::getClipboard(bool selection) const { char *temp_buff; size_t pastedTextSize; @autoreleasepool { NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard]; NSString *textPasted = [pasteBoard stringForType:NSStringPboardType]; if (textPasted == nil) { return NULL; } pastedTextSize = [textPasted lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; temp_buff = (char *)malloc(pastedTextSize + 1); if (temp_buff == NULL) { return NULL; } strncpy(temp_buff, [textPasted cStringUsingEncoding:NSUTF8StringEncoding], pastedTextSize); temp_buff[pastedTextSize] = '\0'; if (temp_buff) { return temp_buff; } else { return NULL; } } } void GHOST_SystemCocoa::putClipboard(const char *buffer, bool selection) const { if (selection) return; // for copying the selection, used on X11 @autoreleasepool { NSPasteboard *pasteBoard = NSPasteboard.generalPasteboard; [pasteBoard declareTypes:@[ NSStringPboardType ] owner:nil]; NSString *textToCopy = [NSString stringWithCString:buffer encoding:NSUTF8StringEncoding]; [pasteBoard setString:textToCopy forType:NSStringPboardType]; } }