// // 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 // Include Carbon to use the keycode names in Carbon's Event.h #include #include "common/debug.h" // 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 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(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([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(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(0); } void OSXWindow::messageLoop() { @autoreleasepool { while (true) { NSEvent* event = [NSApp nextEventMatchingMask: NSAnyEventMask untilDate: [NSDate distantPast] inMode: NSDefaultRunLoopMode dequeue: YES]; if (event == nil) { break; } [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; }