| // |
| // Copyright (c) 2015 The ANGLE Project Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| // |
| |
| // OSXWindow.mm: Implementation of OSWindow for OSX |
| |
| #include "osx/OSXWindow.h" |
| |
| #include <set> |
| // Include Carbon to use the keycode names in Carbon's Event.h |
| #include <Carbon/Carbon.h> |
| |
| #include "common/debug.h" |
| |
| // On OSX 10.12 a number of AppKit interfaces have been renamed for consistency, and the previous |
| // symbols tagged as deprecated. However we can't simply use the new symbols as it would break |
| // compilation on our automated testing that doesn't use OSX 10.12 yet. So we just ignore the |
| // warnings. |
| #pragma GCC diagnostic ignored "-Wdeprecated-declarations" |
| |
| // Some events such as "ShouldTerminate" are sent to the whole application so we keep a list of |
| // all the windows in order to forward the event to each of them. However this and calling pushEvent |
| // in ApplicationDelegate is inherently unsafe in a multithreaded environment. |
| static std::set<OSXWindow*> gAllWindows; |
| |
| @interface Application : NSApplication |
| @end |
| |
| @implementation Application |
| - (void) sendEvent: (NSEvent*) nsEvent |
| { |
| if ([nsEvent type] == NSApplicationDefined) |
| { |
| for (auto window : gAllWindows) |
| { |
| if ([window->getNSWindow() windowNumber] == [nsEvent windowNumber]) |
| { |
| Event event; |
| event.Type = Event::EVENT_TEST; |
| window->pushEvent(event); |
| } |
| } |
| } |
| [super sendEvent: nsEvent]; |
| } |
| @end |
| |
| // The Delegate receiving application-wide events. |
| @interface ApplicationDelegate : NSObject |
| @end |
| |
| @implementation ApplicationDelegate |
| - (NSApplicationTerminateReply) applicationShouldTerminate: (NSApplication*) sender |
| { |
| Event event; |
| event.Type = Event::EVENT_CLOSED; |
| for (auto window : gAllWindows) |
| { |
| window->pushEvent(event); |
| } |
| return NSTerminateCancel; |
| } |
| @end |
| static ApplicationDelegate *gApplicationDelegate = nil; |
| |
| static bool InitializeAppKit() |
| { |
| if (NSApp != nil) |
| { |
| return true; |
| } |
| |
| // Initialize the global variable "NSApp" |
| [Application sharedApplication]; |
| |
| // Make us appear in the dock |
| [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; |
| |
| // Register our global event handler |
| gApplicationDelegate = [[ApplicationDelegate alloc] init]; |
| if (gApplicationDelegate == nil) |
| { |
| return false; |
| } |
| [NSApp setDelegate: static_cast<id>(gApplicationDelegate)]; |
| |
| // Set our status to "started" so we are not bouncing in the doc and can activate |
| [NSApp finishLaunching]; |
| return true; |
| } |
| |
| // NS's and CG's coordinate systems start at the bottom left, while OSWindow's coordinate |
| // system starts at the top left. This function converts the Y coordinate accordingly. |
| static float YCoordToFromCG(float y) |
| { |
| float screenHeight = CGDisplayBounds(CGMainDisplayID()).size.height; |
| return screenHeight - y; |
| } |
| |
| // Delegate for window-wide events, note that the protocol doesn't contain anything input related. |
| @implementation WindowDelegate |
| - (id) initWithWindow: (OSXWindow*) window |
| { |
| self = [super init]; |
| if (self != nil) |
| { |
| mWindow = window; |
| } |
| return self; |
| } |
| |
| - (void) onOSXWindowDeleted |
| { |
| mWindow = nil; |
| } |
| |
| - (BOOL) windowShouldClose: (id) sender |
| { |
| Event event; |
| event.Type = Event::EVENT_CLOSED; |
| mWindow->pushEvent(event); |
| return NO; |
| } |
| |
| - (void) windowDidResize: (NSNotification*) notification |
| { |
| NSSize windowSize = [[mWindow->getNSWindow() contentView] frame].size; |
| Event event; |
| event.Type = Event::EVENT_RESIZED; |
| event.Size.Width = windowSize.width; |
| event.Size.Height = windowSize.height; |
| mWindow->pushEvent(event); |
| } |
| |
| - (void) windowDidMove: (NSNotification*) notification |
| { |
| NSRect screenspace = [mWindow->getNSWindow() frame]; |
| Event event; |
| event.Type = Event::EVENT_MOVED; |
| event.Move.X = screenspace.origin.x; |
| event.Move.Y = YCoordToFromCG(screenspace.origin.y + screenspace.size.height); |
| mWindow->pushEvent(event); |
| } |
| |
| - (void) windowDidBecomeKey: (NSNotification*) notification |
| { |
| Event event; |
| event.Type = Event::EVENT_GAINED_FOCUS; |
| mWindow->pushEvent(event); |
| [self retain]; |
| } |
| |
| - (void) windowDidResignKey: (NSNotification*) notification |
| { |
| if (mWindow != nil) |
| { |
| Event event; |
| event.Type = Event::EVENT_LOST_FOCUS; |
| mWindow->pushEvent(event); |
| } |
| [self release]; |
| } |
| @end |
| |
| static Key NSCodeToKey(int keyCode) |
| { |
| // Missing KEY_PAUSE |
| switch (keyCode) |
| { |
| case kVK_Shift: return KEY_LSHIFT; |
| case kVK_RightShift: return KEY_RSHIFT; |
| case kVK_Option: return KEY_LALT; |
| case kVK_RightOption: return KEY_RALT; |
| case kVK_Control: return KEY_LCONTROL; |
| case kVK_RightControl: return KEY_RCONTROL; |
| case kVK_Command: return KEY_LSYSTEM; |
| // Right System doesn't have a name, but shows up as 0x36. |
| case 0x36: return KEY_RSYSTEM; |
| case kVK_Function: return KEY_MENU; |
| |
| case kVK_ANSI_Semicolon: return KEY_SEMICOLON; |
| case kVK_ANSI_Slash: return KEY_SLASH; |
| case kVK_ANSI_Equal: return KEY_EQUAL; |
| case kVK_ANSI_Minus: return KEY_DASH; |
| case kVK_ANSI_LeftBracket: return KEY_LBRACKET; |
| case kVK_ANSI_RightBracket: return KEY_RBRACKET; |
| case kVK_ANSI_Comma: return KEY_COMMA; |
| case kVK_ANSI_Period: return KEY_PERIOD; |
| case kVK_ANSI_Backslash: return KEY_BACKSLASH; |
| case kVK_ANSI_Grave: return KEY_TILDE; |
| case kVK_Escape: return KEY_ESCAPE; |
| case kVK_Space: return KEY_SPACE; |
| case kVK_Return: return KEY_RETURN; |
| case kVK_Delete: return KEY_BACK; |
| case kVK_Tab: return KEY_TAB; |
| case kVK_PageUp: return KEY_PAGEUP; |
| case kVK_PageDown: return KEY_PAGEDOWN; |
| case kVK_End: return KEY_END; |
| case kVK_Home: return KEY_HOME; |
| case kVK_Help: return KEY_INSERT; |
| case kVK_ForwardDelete: return KEY_DELETE; |
| case kVK_ANSI_KeypadPlus: return KEY_ADD; |
| case kVK_ANSI_KeypadMinus: return KEY_SUBTRACT; |
| case kVK_ANSI_KeypadMultiply: return KEY_MULTIPLY; |
| case kVK_ANSI_KeypadDivide: return KEY_DIVIDE; |
| |
| case kVK_F1: return KEY_F1; |
| case kVK_F2: return KEY_F2; |
| case kVK_F3: return KEY_F3; |
| case kVK_F4: return KEY_F4; |
| case kVK_F5: return KEY_F5; |
| case kVK_F6: return KEY_F6; |
| case kVK_F7: return KEY_F7; |
| case kVK_F8: return KEY_F8; |
| case kVK_F9: return KEY_F9; |
| case kVK_F10: return KEY_F10; |
| case kVK_F11: return KEY_F11; |
| case kVK_F12: return KEY_F12; |
| case kVK_F13: return KEY_F13; |
| case kVK_F14: return KEY_F14; |
| case kVK_F15: return KEY_F15; |
| |
| case kVK_LeftArrow: return KEY_LEFT; |
| case kVK_RightArrow: return KEY_RIGHT; |
| case kVK_DownArrow: return KEY_DOWN; |
| case kVK_UpArrow: return KEY_UP; |
| |
| case kVK_ANSI_Keypad0: return KEY_NUMPAD0; |
| case kVK_ANSI_Keypad1: return KEY_NUMPAD1; |
| case kVK_ANSI_Keypad2: return KEY_NUMPAD2; |
| case kVK_ANSI_Keypad3: return KEY_NUMPAD3; |
| case kVK_ANSI_Keypad4: return KEY_NUMPAD4; |
| case kVK_ANSI_Keypad5: return KEY_NUMPAD5; |
| case kVK_ANSI_Keypad6: return KEY_NUMPAD6; |
| case kVK_ANSI_Keypad7: return KEY_NUMPAD7; |
| case kVK_ANSI_Keypad8: return KEY_NUMPAD8; |
| case kVK_ANSI_Keypad9: return KEY_NUMPAD9; |
| |
| case kVK_ANSI_A: return KEY_A; |
| case kVK_ANSI_B: return KEY_B; |
| case kVK_ANSI_C: return KEY_C; |
| case kVK_ANSI_D: return KEY_D; |
| case kVK_ANSI_E: return KEY_E; |
| case kVK_ANSI_F: return KEY_F; |
| case kVK_ANSI_G: return KEY_G; |
| case kVK_ANSI_H: return KEY_H; |
| case kVK_ANSI_I: return KEY_I; |
| case kVK_ANSI_J: return KEY_J; |
| case kVK_ANSI_K: return KEY_K; |
| case kVK_ANSI_L: return KEY_L; |
| case kVK_ANSI_M: return KEY_M; |
| case kVK_ANSI_N: return KEY_N; |
| case kVK_ANSI_O: return KEY_O; |
| case kVK_ANSI_P: return KEY_P; |
| case kVK_ANSI_Q: return KEY_Q; |
| case kVK_ANSI_R: return KEY_R; |
| case kVK_ANSI_S: return KEY_S; |
| case kVK_ANSI_T: return KEY_T; |
| case kVK_ANSI_U: return KEY_U; |
| case kVK_ANSI_V: return KEY_V; |
| case kVK_ANSI_W: return KEY_W; |
| case kVK_ANSI_X: return KEY_X; |
| case kVK_ANSI_Y: return KEY_Y; |
| case kVK_ANSI_Z: return KEY_Z; |
| |
| case kVK_ANSI_1: return KEY_NUM1; |
| case kVK_ANSI_2: return KEY_NUM2; |
| case kVK_ANSI_3: return KEY_NUM3; |
| case kVK_ANSI_4: return KEY_NUM4; |
| case kVK_ANSI_5: return KEY_NUM5; |
| case kVK_ANSI_6: return KEY_NUM6; |
| case kVK_ANSI_7: return KEY_NUM7; |
| case kVK_ANSI_8: return KEY_NUM8; |
| case kVK_ANSI_9: return KEY_NUM9; |
| case kVK_ANSI_0: return KEY_NUM0; |
| } |
| |
| return Key(0); |
| } |
| |
| static void AddNSKeyStateToEvent(Event *event, int state) |
| { |
| event->Key.Shift = state & NSShiftKeyMask; |
| event->Key.Control = state & NSControlKeyMask; |
| event->Key.Alt = state & NSAlternateKeyMask; |
| event->Key.System = state & NSCommandKeyMask; |
| } |
| |
| static MouseButton TranslateMouseButton(int button) |
| { |
| switch (button) |
| { |
| case 2: |
| return MOUSEBUTTON_MIDDLE; |
| case 3: |
| return MOUSEBUTTON_BUTTON4; |
| case 4: |
| return MOUSEBUTTON_BUTTON5; |
| default: |
| return MOUSEBUTTON_UNKNOWN; |
| } |
| } |
| |
| // Delegate for NSView events, mostly the input events |
| @implementation ContentView |
| - (id) initWithWindow: (OSXWindow*) window |
| { |
| self = [super init]; |
| if (self != nil) |
| { |
| mWindow = window; |
| mTrackingArea = nil; |
| mCurrentModifier = 0; |
| [self updateTrackingAreas]; |
| } |
| return self; |
| } |
| |
| - (void) dealloc |
| { |
| [mTrackingArea release]; |
| [super dealloc]; |
| } |
| |
| - (void) updateTrackingAreas |
| { |
| if (mTrackingArea != nil) |
| { |
| [self removeTrackingArea: mTrackingArea]; |
| [mTrackingArea release]; |
| mTrackingArea = nil; |
| } |
| |
| NSRect bounds = [self bounds]; |
| NSTrackingAreaOptions flags = NSTrackingMouseEnteredAndExited | |
| NSTrackingActiveInKeyWindow | |
| NSTrackingCursorUpdate | |
| NSTrackingInVisibleRect | |
| NSTrackingAssumeInside; |
| mTrackingArea = [[NSTrackingArea alloc] initWithRect: bounds |
| options: flags |
| owner: self |
| userInfo: nil]; |
| |
| [self addTrackingArea: mTrackingArea]; |
| [super updateTrackingAreas]; |
| } |
| |
| // Helps with performance |
| - (BOOL) isOpaque |
| { |
| return YES; |
| } |
| |
| - (BOOL) canBecomeKeyView |
| { |
| return YES; |
| } |
| |
| - (BOOL) acceptsFirstResponder |
| { |
| return YES; |
| } |
| |
| // Handle mouse events from the NSResponder protocol |
| - (float) translateMouseY: (float) y |
| { |
| return [self frame].size.height - y; |
| } |
| |
| - (void) addButtonEvent: (NSEvent*) nsEvent type:(Event::EventType) eventType button:(MouseButton) button |
| { |
| Event event; |
| event.Type = eventType; |
| event.MouseButton.Button = button; |
| event.MouseButton.X = [nsEvent locationInWindow].x; |
| event.MouseButton.Y = [self translateMouseY: [nsEvent locationInWindow].y]; |
| mWindow->pushEvent(event); |
| } |
| |
| - (void) mouseDown: (NSEvent*) event |
| { |
| [self addButtonEvent: event |
| type: Event::EVENT_MOUSE_BUTTON_PRESSED |
| button: MOUSEBUTTON_LEFT]; |
| } |
| |
| - (void) mouseDragged: (NSEvent*) event |
| { |
| [self mouseMoved: event]; |
| } |
| |
| - (void) mouseUp: (NSEvent*) event |
| { |
| [self addButtonEvent: event |
| type: Event::EVENT_MOUSE_BUTTON_RELEASED |
| button: MOUSEBUTTON_LEFT]; |
| } |
| |
| - (void) mouseMoved: (NSEvent*) nsEvent |
| { |
| Event event; |
| event.Type = Event::EVENT_MOUSE_MOVED; |
| event.MouseMove.X = [nsEvent locationInWindow].x; |
| event.MouseMove.Y = [self translateMouseY: [nsEvent locationInWindow].y]; |
| mWindow->pushEvent(event); |
| } |
| |
| - (void) mouseEntered: (NSEvent*) nsEvent |
| { |
| Event event; |
| event.Type = Event::EVENT_MOUSE_ENTERED; |
| mWindow->pushEvent(event); |
| } |
| |
| - (void) mouseExited: (NSEvent*) nsEvent |
| { |
| Event event; |
| event.Type = Event::EVENT_MOUSE_LEFT; |
| mWindow->pushEvent(event); |
| } |
| |
| - (void)rightMouseDown:(NSEvent *)event |
| { |
| [self addButtonEvent: event |
| type: Event::EVENT_MOUSE_BUTTON_PRESSED |
| button: MOUSEBUTTON_RIGHT]; |
| } |
| |
| - (void) rightMouseDragged: (NSEvent*) event |
| { |
| [self mouseMoved: event]; |
| } |
| |
| - (void) rightMouseUp: (NSEvent*)event |
| { |
| [self addButtonEvent: event |
| type: Event::EVENT_MOUSE_BUTTON_RELEASED |
| button: MOUSEBUTTON_RIGHT]; |
| } |
| |
| - (void) otherMouseDown: (NSEvent*) event |
| { |
| [self addButtonEvent: event |
| type: Event::EVENT_MOUSE_BUTTON_PRESSED |
| button: TranslateMouseButton([event buttonNumber])]; |
| } |
| |
| - (void) otherMouseDragged: (NSEvent*) event |
| { |
| [self mouseMoved: event]; |
| } |
| |
| - (void) otherMouseUp: (NSEvent*) event |
| { |
| [self addButtonEvent: event |
| type: Event::EVENT_MOUSE_BUTTON_RELEASED |
| button: TranslateMouseButton([event buttonNumber])]; |
| } |
| |
| - (void) scrollWheel: (NSEvent*) nsEvent |
| { |
| if (static_cast<int>([nsEvent deltaY]) == 0) |
| { |
| return; |
| } |
| |
| Event event; |
| event.Type = Event::EVENT_MOUSE_WHEEL_MOVED; |
| event.MouseWheel.Delta = [nsEvent deltaY]; |
| mWindow->pushEvent(event); |
| } |
| |
| // Handle key events from the NSResponder protocol |
| - (void) keyDown: (NSEvent*) nsEvent |
| { |
| // TODO(cwallez) also send text events |
| Event event; |
| event.Type = Event::EVENT_KEY_PRESSED; |
| event.Key.Code = NSCodeToKey([nsEvent keyCode]); |
| AddNSKeyStateToEvent(&event, [nsEvent modifierFlags]); |
| mWindow->pushEvent(event); |
| } |
| |
| - (void) keyUp: (NSEvent*) nsEvent |
| { |
| Event event; |
| event.Type = Event::EVENT_KEY_RELEASED; |
| event.Key.Code = NSCodeToKey([nsEvent keyCode]); |
| AddNSKeyStateToEvent(&event, [nsEvent modifierFlags]); |
| mWindow->pushEvent(event); |
| } |
| |
| // Modifier keys do not trigger keyUp/Down events but only flagsChanged events. |
| - (void) flagsChanged: (NSEvent*) nsEvent |
| { |
| Event event; |
| |
| // Guess if the key has been pressed or released with the change of modifiers |
| // It currently doesn't work when modifiers are unchanged, such as when pressing |
| // both shift keys. GLFW has a solution for this but it requires tracking the |
| // state of the keys. Implementing this is still TODO(cwallez) |
| int modifier = [nsEvent modifierFlags] & NSDeviceIndependentModifierFlagsMask; |
| if (modifier < mCurrentModifier) |
| { |
| event.Type = Event::EVENT_KEY_RELEASED; |
| } |
| else |
| { |
| event.Type = Event::EVENT_KEY_PRESSED; |
| } |
| mCurrentModifier = modifier; |
| |
| event.Key.Code = NSCodeToKey([nsEvent keyCode]); |
| AddNSKeyStateToEvent(&event, [nsEvent modifierFlags]); |
| mWindow->pushEvent(event); |
| } |
| @end |
| |
| OSXWindow::OSXWindow() |
| : mWindow(nil), |
| mDelegate(nil), |
| mView(nil) |
| { |
| } |
| |
| OSXWindow::~OSXWindow() |
| { |
| destroy(); |
| } |
| |
| bool OSXWindow::initialize(const std::string &name, size_t width, size_t height) |
| { |
| if (!InitializeAppKit()) |
| { |
| return false; |
| } |
| |
| unsigned int styleMask = NSTitledWindowMask | NSClosableWindowMask | NSResizableWindowMask | |
| NSMiniaturizableWindowMask; |
| mWindow = [[NSWindow alloc] initWithContentRect: NSMakeRect(0, 0, width, height) |
| styleMask: styleMask |
| backing: NSBackingStoreBuffered |
| defer: NO]; |
| |
| if (mWindow == nil) |
| { |
| return false; |
| } |
| |
| mDelegate = [[WindowDelegate alloc] initWithWindow: this]; |
| if (mDelegate == nil) |
| { |
| return false; |
| } |
| [mWindow setDelegate: static_cast<id>(mDelegate)]; |
| |
| mView = [[ContentView alloc] initWithWindow: this]; |
| if (mView == nil) |
| { |
| return false; |
| } |
| [mView setWantsLayer:YES]; |
| |
| [mWindow setContentView: mView]; |
| [mWindow setTitle: [NSString stringWithUTF8String: name.c_str()]]; |
| [mWindow setAcceptsMouseMovedEvents: YES]; |
| [mWindow center]; |
| |
| [NSApp activateIgnoringOtherApps: YES]; |
| |
| mX = 0; |
| mY = 0; |
| mWidth = width; |
| mHeight = height; |
| |
| gAllWindows.insert(this); |
| return true; |
| } |
| |
| void OSXWindow::destroy() |
| { |
| gAllWindows.erase(this); |
| |
| [mView release]; |
| mView = nil; |
| [mDelegate onOSXWindowDeleted]; |
| [mDelegate release]; |
| mDelegate = nil; |
| [mWindow release]; |
| mWindow = nil; |
| } |
| |
| EGLNativeWindowType OSXWindow::getNativeWindow() const |
| { |
| return [mView layer]; |
| } |
| |
| EGLNativeDisplayType OSXWindow::getNativeDisplay() const |
| { |
| // TODO(cwallez): implement it once we have defined what EGLNativeDisplayType is |
| return static_cast<EGLNativeDisplayType>(0); |
| } |
| |
| void OSXWindow::messageLoop() |
| { |
| @autoreleasepool |
| { |
| while (true) |
| { |
| NSEvent* event = [NSApp nextEventMatchingMask: NSAnyEventMask |
| untilDate: [NSDate distantPast] |
| inMode: NSDefaultRunLoopMode |
| dequeue: YES]; |
| if (event == nil) |
| { |
| break; |
| } |
| |
| if ([event type] == NSAppKitDefined) |
| { |
| continue; |
| } |
| [NSApp sendEvent: event]; |
| } |
| } |
| } |
| |
| void OSXWindow::setMousePosition(int x, int y) |
| { |
| y = [mWindow frame].size.height - y -1; |
| NSPoint screenspace; |
| |
| #if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_7 |
| screenspace = [mWindow convertBaseToScreen: NSMakePoint(x, y)]; |
| #else |
| screenspace = [mWindow convertRectToScreen: NSMakeRect(x, y, 0, 0)].origin; |
| #endif |
| CGWarpMouseCursorPosition(CGPointMake(screenspace.x, YCoordToFromCG(screenspace.y))); |
| } |
| |
| bool OSXWindow::setPosition(int x, int y) |
| { |
| // Given CG and NS's coordinate system, the "Y" position of a window is the Y coordinate |
| // of the bottom of the window. |
| int newBottom = [mWindow frame].size.height + y; |
| NSRect emptyRect = NSMakeRect(x, YCoordToFromCG(newBottom), 0, 0); |
| [mWindow setFrameOrigin: [mWindow frameRectForContentRect: emptyRect].origin]; |
| return true; |
| } |
| |
| bool OSXWindow::resize(int width, int height) |
| { |
| [mWindow setContentSize: NSMakeSize(width, height)]; |
| return true; |
| } |
| |
| void OSXWindow::setVisible(bool isVisible) |
| { |
| if (isVisible) |
| { |
| [mWindow makeKeyAndOrderFront: nil]; |
| } |
| else |
| { |
| [mWindow orderOut: nil]; |
| } |
| } |
| |
| void OSXWindow::signalTestEvent() |
| { |
| @autoreleasepool |
| { |
| NSEvent *event = [NSEvent otherEventWithType: NSApplicationDefined |
| location: NSMakePoint(0, 0) |
| modifierFlags: 0 |
| timestamp: 0.0 |
| windowNumber: [mWindow windowNumber] |
| context: nil |
| subtype: 0 |
| data1: 0 |
| data2: 0]; |
| [NSApp postEvent: event atStart: YES]; |
| } |
| } |
| |
| NSWindow* OSXWindow::getNSWindow() const |
| { |
| return mWindow; |
| } |
| |
| OSWindow *CreateOSWindow() |
| { |
| return new OSXWindow; |
| } |