#region License // // CocoaNativeWindow.cs // // Author: // Olle HÃ¥kansson // // Copyright (c) 2014 Olle HÃ¥kansson // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // #endregion using System; using System.ComponentModel; using System.Diagnostics; using System.Drawing; using System.Runtime.InteropServices; using System.Threading; using OpenTK.Graphics; using OpenTK.Input; namespace OpenTK.Platform.MacOS { class CocoaNativeWindow : NativeWindowBase { static int UniqueId; static readonly IntPtr selNextEventMatchingMask = Selector.Get("nextEventMatchingMask:untilDate:inMode:dequeue:"); static readonly IntPtr selSendEvent = Selector.Get("sendEvent:"); //static readonly IntPtr selUpdateWindows = Selector.Get("updateWindows"); static readonly IntPtr selContentView = Selector.Get("contentView"); static readonly IntPtr selConvertRectFromScreen = Selector.Get("convertRectFromScreen:"); static readonly IntPtr selConvertRectToScreen = Selector.Get("convertRectToScreen:"); static readonly IntPtr selPerformClose = Selector.Get("performClose:"); static readonly IntPtr selClose = Selector.Get("close"); static readonly IntPtr selTitle = Selector.Get("title"); static readonly IntPtr selSetTitle = Selector.Get("setTitle:"); static readonly IntPtr selSetApplicationIconImage = Selector.Get("setApplicationIconImage:"); static readonly IntPtr selIsKeyWindow = Selector.Get("isKeyWindow"); static readonly IntPtr selIsVisible = Selector.Get("isVisible"); static readonly IntPtr selSetIsVisible = Selector.Get("setIsVisible:"); static readonly IntPtr selFrame = Selector.Get("frame"); static readonly IntPtr selVisibleFrame = Selector.Get("visibleFrame"); static readonly IntPtr selBounds = Selector.Get("bounds"); static readonly IntPtr selScreen = Selector.Get("screen"); static readonly IntPtr selSetFrame = Selector.Get("setFrame:display:"); static readonly IntPtr selConvertRectToBacking = Selector.Get("convertRectToBacking:"); static readonly IntPtr selConvertRectFromBacking = Selector.Get("convertRectFromBacking:"); static readonly IntPtr selFrameRectForContentRect = Selector.Get("frameRectForContentRect:"); static readonly IntPtr selType = Selector.Get("type"); static readonly IntPtr selKeyCode = Selector.Get("keyCode"); static readonly IntPtr selModifierFlags = Selector.Get("modifierFlags"); static readonly IntPtr selIsARepeat = Selector.Get("isARepeat"); static readonly IntPtr selCharactersIgnoringModifiers = Selector.Get("charactersIgnoringModifiers"); static readonly IntPtr selAddTrackingArea = Selector.Get("addTrackingArea:"); static readonly IntPtr selRemoveTrackingArea = Selector.Get("removeTrackingArea:"); static readonly IntPtr selTrackingArea = Selector.Get("trackingArea"); static readonly IntPtr selInitWithSize = Selector.Get("initWithSize:"); static readonly IntPtr selInitWithRect = Selector.Get("initWithRect:options:owner:userInfo:"); static readonly IntPtr selOwner = Selector.Get("owner"); static readonly IntPtr selLocationInWindowOwner = Selector.Get("locationInWindow"); static readonly IntPtr selHide = Selector.Get("hide"); static readonly IntPtr selUnhide = Selector.Get("unhide"); static readonly IntPtr selScrollingDeltaX = Selector.Get("scrollingDeltaX"); static readonly IntPtr selScrollingDeltaY = Selector.Get("scrollingDeltaY"); static readonly IntPtr selDeltaX = Selector.Get("deltaX"); static readonly IntPtr selDeltaY = Selector.Get("deltaY"); static readonly IntPtr selButtonNumber = Selector.Get("buttonNumber"); static readonly IntPtr selSetStyleMask = Selector.Get("setStyleMask:"); static readonly IntPtr selStyleMask = Selector.Get("styleMask"); static readonly IntPtr selHasPreciseScrollingDeltas = Selector.Get("hasPreciseScrollingDeltas"); //static readonly IntPtr selIsMiniaturized = Selector.Get("isMiniaturized"); //static readonly IntPtr selIsZoomed = Selector.Get("isZoomed"); //static readonly IntPtr selPerformMiniaturize = Selector.Get("performMiniaturize:"); static readonly IntPtr selMiniaturize = Selector.Get("miniaturize:"); static readonly IntPtr selDeminiaturize = Selector.Get("deminiaturize:"); //static readonly IntPtr selPerformZoom = Selector.Get("performZoom:"); //static readonly IntPtr selZoom = Selector.Get("zoom:"); static readonly IntPtr selLevel = Selector.Get("level"); static readonly IntPtr selSetLevel = Selector.Get("setLevel:"); static readonly IntPtr selPresentationOptions = Selector.Get("presentationOptions"); static readonly IntPtr selSetPresentationOptions = Selector.Get("setPresentationOptions:"); //static readonly IntPtr selIsInFullScreenMode = Selector.Get("isInFullScreenMode"); //static readonly IntPtr selExitFullScreenModeWithOptions = Selector.Get("exitFullScreenModeWithOptions:"); //static readonly IntPtr selEnterFullScreenModeWithOptions = Selector.Get("enterFullScreenMode:withOptions:"); static readonly IntPtr selArrowCursor = Selector.Get("arrowCursor"); static readonly IntPtr selAddCursorRect = Selector.Get("addCursorRect:cursor:"); static readonly IntPtr selInvalidateCursorRectsForView = Selector.Get("invalidateCursorRectsForView:"); static readonly IntPtr selInitWithBitmapDataPlanes = Selector.Get("initWithBitmapDataPlanes:pixelsWide:pixelsHigh:bitsPerSample:samplesPerPixel:hasAlpha:isPlanar:colorSpaceName:bitmapFormat:bytesPerRow:bitsPerPixel:"); static readonly IntPtr selBitmapData = Selector.Get("bitmapData"); static readonly IntPtr selAddRepresentation = Selector.Get("addRepresentation:"); static readonly IntPtr selInitWithImageHotSpot = Selector.Get("initWithImage:hotSpot:"); static readonly IntPtr NSDefaultRunLoopMode; static readonly IntPtr NSCursor; static readonly IntPtr NSImage; static readonly IntPtr NSBitmapImageRep; static readonly IntPtr NSDeviceRGBColorSpace = Cocoa.ToNSString("NSDeviceRGBColorSpace"); static CocoaNativeWindow() { Cocoa.Initialize(); NSApplication.Initialize(); // Problem: This does not allow creating a separate app and using CocoaNativeWindow. NSDefaultRunLoopMode = Cocoa.GetStringConstant(Cocoa.FoundationLibrary, "NSDefaultRunLoopMode"); NSCursor = Class.Get("NSCursor"); NSImage = Class.Get("NSImage"); NSBitmapImageRep = Class.Get("NSBitmapImageRep"); } private CocoaWindowInfo windowInfo; private IntPtr windowClass; private IntPtr trackingArea; private IntPtr current_icon_handle; private bool disposed = false; private bool exists; private bool cursorVisible = true; private System.Drawing.Icon icon; private WindowBorder windowBorder = WindowBorder.Resizable; private Nullable deferredWindowBorder; private Nullable previousWindowBorder; private WindowState windowState = WindowState.Normal; private string title; private RectangleF previousBounds; private int normalLevel; private bool shouldClose; private int suppressResize; private MouseCursor selectedCursor = MouseCursor.Default; // user-selected cursor public CocoaNativeWindow(int x, int y, int width, int height, string title, GraphicsMode mode, GameWindowFlags options, DisplayDevice device) { // Create callback methods. We need to store those, // otherwise the GC may collect them while they are // still active. WindowKeyDownHandler = WindowKeyDown; WindowDidResizeHandler = WindowDidResize; WindowDidMoveHandler = WindowDidMove; WindowDidBecomeKeyHandler = WindowDidBecomeKey; WindowDidResignKeyHandler = WindowDidResignKey; WindowWillMiniaturizeHandler = WindowWillMiniaturize; WindowDidMiniaturizeHandler = WindowDidMiniaturize; WindowDidDeminiaturizeHandler = WindowDidDeminiaturize; WindowShouldZoomToFrameHandler = WindowShouldZoomToFrame; WindowShouldCloseHandler = WindowShouldClose; AcceptsFirstResponderHandler = AcceptsFirstResponder; CanBecomeKeyWindowHandler = CanBecomeKeyWindow; CanBecomeMainWindowHandler = CanBecomeMainWindow; ResetCursorRectsHandler = ResetCursorRects; // Create the window class int unique_id = Interlocked.Increment(ref UniqueId); windowClass = Class.AllocateClass("OpenTK_GameWindow" + unique_id, "NSWindow"); Class.RegisterMethod(windowClass, WindowKeyDownHandler, "keyDown:", "v@:@"); Class.RegisterMethod(windowClass, WindowDidResizeHandler, "windowDidResize:", "v@:@"); Class.RegisterMethod(windowClass, WindowDidMoveHandler, "windowDidMove:", "v@:@"); Class.RegisterMethod(windowClass, WindowDidBecomeKeyHandler, "windowDidBecomeKey:", "v@:@"); Class.RegisterMethod(windowClass, WindowDidResignKeyHandler, "windowDidResignKey:", "v@:@"); Class.RegisterMethod(windowClass, WindowWillMiniaturizeHandler, "windowWillMiniaturize:", "v@:@"); Class.RegisterMethod(windowClass, WindowDidMiniaturizeHandler, "windowDidMiniaturize:", "v@:@"); Class.RegisterMethod(windowClass, WindowDidDeminiaturizeHandler, "windowDidDeminiaturize:", "v@:@"); Class.RegisterMethod(windowClass, WindowShouldZoomToFrameHandler, "windowShouldZoom:toFrame:", "b@:@{NSRect={NSPoint=ff}{NSSize=ff}}"); Class.RegisterMethod(windowClass, WindowShouldCloseHandler, "windowShouldClose:", "b@:@"); Class.RegisterMethod(windowClass, AcceptsFirstResponderHandler, "acceptsFirstResponder", "b@:"); Class.RegisterMethod(windowClass, CanBecomeKeyWindowHandler, "canBecomeKeyWindow", "b@:"); Class.RegisterMethod(windowClass, CanBecomeMainWindowHandler, "canBecomeMainWindow", "b@:"); Class.RegisterClass(windowClass); IntPtr viewClass = Class.AllocateClass("OpenTK_NSView" + unique_id, "NSView"); Class.RegisterMethod(viewClass, ResetCursorRectsHandler, "resetCursorRects", "v@:"); Class.RegisterClass(viewClass); // Create window instance // Note: The coordinate system of Cocoa places (0,0) at the bottom left. // We need to get the height of the main screen and flip that in order // to place the window at the correct position. // Note: NSWindows are laid out relative to the main screen. var screenRect = Cocoa.SendRect( Cocoa.SendIntPtr( Cocoa.SendIntPtr(Class.Get("NSScreen"), Selector.Get("screens")), Selector.Get("objectAtIndex:"), 0), Selector.Get("frame")); var contentRect = new System.Drawing.RectangleF(x, screenRect.Height - height - y, width, height); var style = GetStyleMask(windowBorder); var bufferingType = NSBackingStore.Buffered; IntPtr classPtr; classPtr = Cocoa.SendIntPtr(windowClass, Selector.Alloc); if (classPtr == IntPtr.Zero) { Debug.Print("[Error] Failed to allocate window class."); throw new PlatformException(); } bool defer = false; IntPtr windowPtr = Cocoa.SendIntPtr(classPtr, Selector.Get("initWithContentRect:styleMask:backing:defer:"), contentRect, (int)style, (int)bufferingType, defer); if (windowPtr == IntPtr.Zero) { Debug.Print("[Error] Failed to initialize window with ({0}, {1}, {2}, {3}).", contentRect, style, bufferingType, defer); throw new PlatformException(); } // Replace view with our custom implementation // that overrides resetCursorRects (maybe there is // a better way to implement this override?) // Existing view: IntPtr viewPtr = Cocoa.SendIntPtr(windowPtr, Selector.Get("contentView")); if (viewPtr == IntPtr.Zero) { Debug.Print("[Error] Failed to retrieve content view for window {0}.", windowPtr); throw new PlatformException(); } // Our custom view with the same bounds: viewPtr = Cocoa.SendIntPtr( Cocoa.SendIntPtr(viewClass, Selector.Alloc), Selector.Get("initWithFrame:"), Cocoa.SendRect(viewPtr, selBounds)); if (viewPtr != IntPtr.Zero) { Cocoa.SendVoid(windowPtr, Selector.Get("setContentView:"), viewPtr); } else { Debug.Print("[Error] Failed to initialize content view with frame {0}.", selBounds); throw new PlatformException(); } windowInfo = new CocoaWindowInfo(windowPtr); // Set up behavior Cocoa.SendIntPtr(windowPtr, Selector.Get("setDelegate:"), windowPtr); // The window class acts as its own delegate Cocoa.SendVoid(windowPtr, Selector.Get("makeKeyWindow")); SetTitle(title, false); ResetTrackingArea(); exists = true; NSApplication.Quit += ApplicationQuit; } [UnmanagedFunctionPointer(CallingConvention.Winapi)] delegate void WindowKeyDownDelegate(IntPtr self, IntPtr cmd, IntPtr notification); [UnmanagedFunctionPointer(CallingConvention.Winapi)] delegate void WindowDidResizeDelegate(IntPtr self, IntPtr cmd, IntPtr notification); [UnmanagedFunctionPointer(CallingConvention.Winapi)] delegate void WindowDidMoveDelegate(IntPtr self, IntPtr cmd, IntPtr notification); [UnmanagedFunctionPointer(CallingConvention.Winapi)] delegate void WindowDidBecomeKeyDelegate(IntPtr self, IntPtr cmd, IntPtr notification); [UnmanagedFunctionPointer(CallingConvention.Winapi)] delegate void WindowDidResignKeyDelegate(IntPtr self, IntPtr cmd, IntPtr notification); [UnmanagedFunctionPointer(CallingConvention.Winapi)] delegate void WindowWillMiniaturizeDelegate(IntPtr self, IntPtr cmd, IntPtr notification); [UnmanagedFunctionPointer(CallingConvention.Winapi)] delegate void WindowDidMiniaturizeDelegate(IntPtr self, IntPtr cmd, IntPtr notification); [UnmanagedFunctionPointer(CallingConvention.Winapi)] delegate void WindowDidDeminiaturizeDelegate(IntPtr self, IntPtr cmd, IntPtr notification); [UnmanagedFunctionPointer(CallingConvention.Winapi)] delegate bool WindowShouldZoomToFrameDelegate(IntPtr self, IntPtr cmd, IntPtr nsWindow, RectangleF toFrame); [UnmanagedFunctionPointer(CallingConvention.Winapi)] delegate bool WindowShouldCloseDelegate(IntPtr self, IntPtr cmd, IntPtr sender); [UnmanagedFunctionPointer(CallingConvention.Winapi)] delegate bool AcceptsFirstResponderDelegate(IntPtr self, IntPtr cmd); [UnmanagedFunctionPointer(CallingConvention.Winapi)] delegate bool CanBecomeKeyWindowDelegate(IntPtr self, IntPtr cmd); [UnmanagedFunctionPointer(CallingConvention.Winapi)] delegate bool CanBecomeMainWindowDelegate(IntPtr self, IntPtr cmd); [UnmanagedFunctionPointer(CallingConvention.Winapi)] delegate void ResetCursorRectsDelegate(IntPtr self, IntPtr cmd); WindowKeyDownDelegate WindowKeyDownHandler; WindowDidResizeDelegate WindowDidResizeHandler; WindowDidMoveDelegate WindowDidMoveHandler; WindowDidBecomeKeyDelegate WindowDidBecomeKeyHandler; WindowDidResignKeyDelegate WindowDidResignKeyHandler; WindowWillMiniaturizeDelegate WindowWillMiniaturizeHandler; WindowDidMiniaturizeDelegate WindowDidMiniaturizeHandler; WindowDidDeminiaturizeDelegate WindowDidDeminiaturizeHandler; WindowShouldZoomToFrameDelegate WindowShouldZoomToFrameHandler; WindowShouldCloseDelegate WindowShouldCloseHandler; AcceptsFirstResponderDelegate AcceptsFirstResponderHandler; CanBecomeKeyWindowDelegate CanBecomeKeyWindowHandler; CanBecomeMainWindowDelegate CanBecomeMainWindowHandler; ResetCursorRectsDelegate ResetCursorRectsHandler; private void WindowKeyDown(IntPtr self, IntPtr cmd, IntPtr notification) { // Steal the event to remove the "beep" sound that is normally played for unhandled key events. } private void WindowDidResize(IntPtr self, IntPtr cmd, IntPtr notification) { try { OnResize(true); } catch (Exception e) { Debug.Print(e.ToString()); } } private void OnResize(bool resetTracking) { if (resetTracking) { ResetTrackingArea(); } var context = GraphicsContext.CurrentContext; if (context != null) { context.Update(windowInfo); } if (suppressResize == 0) OnResize(EventArgs.Empty); } private void ApplicationQuit(object sender, CancelEventArgs e) { try { bool close = WindowShouldClose(windowInfo.Handle, IntPtr.Zero, IntPtr.Zero); e.Cancel |= !close; } catch (Exception ex) { Debug.Print(ex.ToString()); } } private void WindowDidMove(IntPtr self, IntPtr cmd, IntPtr notification) { try { // Problem: Called only when you stop moving for a brief moment, // not each frame as it is on PC. OnMove(EventArgs.Empty); } catch (Exception e) { Debug.Print(e.ToString()); } } private void WindowDidBecomeKey(IntPtr self, IntPtr cmd, IntPtr notification) { try { OnFocusedChanged(EventArgs.Empty); } catch (Exception e) { Debug.Print(e.ToString()); } } private void WindowDidResignKey(IntPtr self, IntPtr cmd, IntPtr notification) { try { OnFocusedChanged(EventArgs.Empty); } catch (Exception e) { Debug.Print(e.ToString()); } } private void WindowWillMiniaturize(IntPtr self, IntPtr cmd, IntPtr notification) { try { // Can get stuck in weird states if we maximize, then minimize; // restoring to the old state would override the normalBounds. // To avoid this without adding complexity, just restore state here. RestoreWindowState(); // Avoid getting in weird states } catch (Exception e) { Debug.Print(e.ToString()); } } private void WindowDidMiniaturize(IntPtr self, IntPtr cmd, IntPtr notification) { try { windowState = WindowState.Minimized; OnWindowStateChanged(EventArgs.Empty); OnResize(false); // Don't set tracking area when we minimize } catch (Exception e) { Debug.Print(e.ToString()); } } private void WindowDidDeminiaturize(IntPtr self, IntPtr cmd, IntPtr notification) { try { windowState = WindowState.Normal; OnWindowStateChanged(EventArgs.Empty); OnResize(true); } catch (Exception e) { Debug.Print(e.ToString()); } } private bool WindowShouldZoomToFrame(IntPtr self, IntPtr cmd, IntPtr nsWindow, RectangleF toFrame) { try { if (windowState == WindowState.Maximized) { WindowState = WindowState.Normal; } else { previousBounds = InternalBounds; previousWindowBorder = WindowBorder; InternalBounds = toFrame; windowState = WindowState.Maximized; OnWindowStateChanged(EventArgs.Empty); } } catch (Exception e) { Debug.Print(e.ToString()); } return false; } private bool WindowShouldClose(IntPtr self, IntPtr cmd, IntPtr sender) { try { var cancelArgs = new CancelEventArgs(); OnClosing(cancelArgs); if (!cancelArgs.Cancel) { OnClosed(EventArgs.Empty); return true; } } catch (Exception e) { Debug.Print(e.ToString()); } return false; } private bool AcceptsFirstResponder(IntPtr self, IntPtr cmd) { return true; } private bool CanBecomeKeyWindow(IntPtr self, IntPtr cmd) { return true; } private bool CanBecomeMainWindow(IntPtr self, IntPtr cmd) { return true; } private void ResetTrackingArea() { try { var owner = windowInfo.ViewHandle; if (trackingArea != IntPtr.Zero) { Cocoa.SendVoid(owner, selRemoveTrackingArea, trackingArea); Cocoa.SendVoid(trackingArea, Selector.Release); } var ownerBounds = Cocoa.SendRect(owner, selBounds); var options = (int)( NSTrackingAreaOptions.MouseEnteredAndExited | NSTrackingAreaOptions.ActiveInKeyWindow | NSTrackingAreaOptions.MouseMoved | NSTrackingAreaOptions.CursorUpdate); trackingArea = Cocoa.SendIntPtr(Cocoa.SendIntPtr(Class.Get("NSTrackingArea"), Selector.Alloc), selInitWithRect, ownerBounds, options, owner, IntPtr.Zero); Cocoa.SendVoid(owner, selAddTrackingArea, trackingArea); } catch (Exception e) { Debug.Print(e.ToString()); } } public override void Close() { shouldClose = true; } private KeyModifiers GetModifiers(NSEventModifierMask mask) { OpenTK.Input.KeyModifiers modifiers = 0; if ((mask & NSEventModifierMask.ControlKeyMask) != 0) modifiers |= OpenTK.Input.KeyModifiers.Control; if ((mask & NSEventModifierMask.ShiftKeyMask) != 0) modifiers |= OpenTK.Input.KeyModifiers.Shift; if ((mask & NSEventModifierMask.AlternateKeyMask) != 0) modifiers |= OpenTK.Input.KeyModifiers.Alt; return modifiers; } private MouseButton GetMouseButton(int cocoaButtonIndex) { if (cocoaButtonIndex == 0) return MouseButton.Left; if (cocoaButtonIndex == 1) return MouseButton.Right; if (cocoaButtonIndex == 2) return MouseButton.Middle; if (cocoaButtonIndex >= (int)MouseButton.LastButton) return MouseButton.LastButton; return (MouseButton)cocoaButtonIndex; } public override void ProcessEvents() { base.ProcessEvents(); while (true) { var e = Cocoa.SendIntPtr(NSApplication.Handle, selNextEventMatchingMask, uint.MaxValue, IntPtr.Zero, NSDefaultRunLoopMode, true); if (e == IntPtr.Zero) break; var type = (NSEventType)Cocoa.SendInt(e, selType); switch (type) { case NSEventType.KeyDown: { MacOSKeyCode keyCode = (MacOSKeyCode)Cocoa.SendUshort(e, selKeyCode); var isARepeat = Cocoa.SendBool(e, selIsARepeat); Key key = MacOSKeyMap.GetKey(keyCode); OnKeyDown(key, isARepeat); var s = Cocoa.FromNSString(Cocoa.SendIntPtr(e, selCharactersIgnoringModifiers)); foreach (var c in s) { int intVal = (int)c; if (!Char.IsControl(c) && (intVal < 63232 || intVal > 63235)) { // For some reason, arrow keys (mapped 63232-63235) // are seen as non-control characters, so get rid of those. OnKeyPress(c); } } } break; case NSEventType.KeyUp: { MacOSKeyCode keyCode = (MacOSKeyCode)Cocoa.SendUshort(e, selKeyCode); Key key = MacOSKeyMap.GetKey(keyCode); OnKeyUp(key); } break; case NSEventType.FlagsChanged: { var modifierFlags = (NSEventModifierMask)Cocoa.SendUint(e, selModifierFlags); UpdateModifierFlags(GetModifiers(modifierFlags)); } break; case NSEventType.MouseEntered: { var eventTrackingArea = Cocoa.SendIntPtr(e, selTrackingArea); var trackingAreaOwner = Cocoa.SendIntPtr(eventTrackingArea, selOwner); if (trackingAreaOwner == windowInfo.ViewHandle) { if (selectedCursor != MouseCursor.Default) { //SetCursor(selectedCursor); } OnMouseEnter(EventArgs.Empty); } } break; case NSEventType.MouseExited: { var eventTrackingArea = Cocoa.SendIntPtr(e, selTrackingArea); var trackingAreaOwner = Cocoa.SendIntPtr(eventTrackingArea, selOwner); if (trackingAreaOwner == windowInfo.ViewHandle) { if (selectedCursor != MouseCursor.Default) { //SetCursor(MouseCursor.Default); } OnMouseLeave(EventArgs.Empty); } } break; case NSEventType.LeftMouseDragged: case NSEventType.RightMouseDragged: case NSEventType.OtherMouseDragged: case NSEventType.MouseMoved: { Point p = new Point(MouseState.X, MouseState.Y); if (CursorVisible) { // Use absolute coordinates var pf = Cocoa.SendPoint(e, selLocationInWindowOwner); // Convert from points to pixel coordinates var rf = Cocoa.SendRect(windowInfo.Handle, selConvertRectToBacking, new RectangleF(pf.X, pf.Y, 0, 0)); // See CocoaDrawingGuide under "Converting from Window to View Coordinates" p = new Point( MathHelper.Clamp((int)Math.Round(rf.X), 0, Width), MathHelper.Clamp((int)Math.Round(Height - rf.Y), 0, Height)); } else { // Mouse has been disassociated, // use relative coordinates var dx = Cocoa.SendFloat(e, selDeltaX); var dy = Cocoa.SendFloat(e, selDeltaY); p = new Point( MathHelper.Clamp((int)Math.Round(p.X + dx), 0, Width), MathHelper.Clamp((int)Math.Round(p.Y + dy), 0, Height)); } // Only raise events when the mouse has actually moved if (MouseState.X != p.X || MouseState.Y != p.Y) { OnMouseMove(p.X, p.Y); } } break; case NSEventType.CursorUpdate: break; case NSEventType.ScrollWheel: { float dx, dy; if (Cocoa.SendBool(e, selHasPreciseScrollingDeltas)) { dx = Cocoa.SendFloat(e, selScrollingDeltaX) * MacOSFactory.ScrollFactor; dy = Cocoa.SendFloat(e, selScrollingDeltaY) * MacOSFactory.ScrollFactor; } else { dx = Cocoa.SendFloat(e, selDeltaX); dy = Cocoa.SendFloat(e, selDeltaY); } // Only raise wheel events when the user has actually scrolled if (dx != 0 || dy != 0) { // Note: OpenTK follows the win32 convention, where // (+h, +v) = (right, up). MacOS reports (+h, +v) = (left, up) // so we need to flip the horizontal scroll direction. OnMouseWheel(-dx, dy); } } break; case NSEventType.LeftMouseDown: case NSEventType.RightMouseDown: case NSEventType.OtherMouseDown: { var buttonNumber = Cocoa.SendInt(e, selButtonNumber); OnMouseDown(GetMouseButton(buttonNumber)); } break; case NSEventType.LeftMouseUp: case NSEventType.RightMouseUp: case NSEventType.OtherMouseUp: { var buttonNumber = Cocoa.SendInt(e, selButtonNumber); OnMouseUp(GetMouseButton(buttonNumber)); } break; } Cocoa.SendVoid(NSApplication.Handle, selSendEvent, e); } // Handle closing if (shouldClose) { shouldClose = false; CloseWindow(false); } } public override System.Drawing.Point PointToClient(System.Drawing.Point point) { var r = Cocoa.SendRect(windowInfo.ViewHandle, selConvertRectToBacking, Cocoa.SendRect(windowInfo.Handle, selConvertRectFromScreen, new RectangleF(point.X, GetCurrentScreenFrame().Height - point.Y, 0, 0))); return new Point((int)r.X, (int)(Height - r.Y)); } public override System.Drawing.Point PointToScreen(System.Drawing.Point point) { var r = Cocoa.SendRect(windowInfo.Handle, selConvertRectToScreen, Cocoa.SendRect(windowInfo.ViewHandle, selConvertRectFromBacking, new RectangleF(point.X, Height - point.Y, 0, 0))); return new Point((int)r.X, (int)(GetCurrentScreenFrame().Height - r.Y)); } public override System.Drawing.Icon Icon { get { return icon; } set { if (value != null && value != icon) { // Create and set new icon IntPtr nsimg = IntPtr.Zero; using (Image img = value.ToBitmap()) { nsimg = Cocoa.ToNSImage(img); if (nsimg != IntPtr.Zero) { Cocoa.SendVoid(NSApplication.Handle, selSetApplicationIconImage, nsimg); } else { Debug.Print("[Mac] Failed to create NSImage for {0}", value); return; } } // Release previous icon if (current_icon_handle != IntPtr.Zero) { Cocoa.SendVoid(current_icon_handle, Selector.Release); } // Raise IconChanged event current_icon_handle = nsimg; icon = value; OnIconChanged(EventArgs.Empty); } } } public override string Title { get { return Cocoa.FromNSString(Cocoa.SendIntPtr(windowInfo.Handle, selTitle)); } set { SetTitle(value, true); } } public override bool Focused { get { return Cocoa.SendBool(windowInfo.Handle, selIsKeyWindow); } } public override bool Visible { get { return Cocoa.SendBool(windowInfo.Handle, selIsVisible); } set { Cocoa.SendVoid(windowInfo.Handle, selSetIsVisible, value); OnVisibleChanged(EventArgs.Empty); } } public override bool Exists { get { return exists; } } public override IWindowInfo WindowInfo { get { return windowInfo; } } private void RestoreWindowState() { suppressResize++; if (windowState == WindowState.Fullscreen) { SetMenuVisible(true); if (MacOSFactory.ExclusiveFullscreen) { CG.DisplayReleaseAll(); Cocoa.SendVoid(windowInfo.Handle, selSetLevel, normalLevel); } RestoreBorder(); InternalBounds = previousBounds; } else if (windowState == WindowState.Maximized) { RestoreBorder(); InternalBounds = previousBounds; } else if (windowState == WindowState.Minimized) { Cocoa.SendVoid(windowInfo.Handle, selDeminiaturize, windowInfo.Handle); } windowState = WindowState.Normal; suppressResize--; } private void HideBorder() { suppressResize++; SetWindowBorder(WindowBorder.Hidden); ProcessEvents(); suppressResize--; } private void RestoreBorder() { suppressResize++; SetWindowBorder( deferredWindowBorder.HasValue ? deferredWindowBorder.Value : previousWindowBorder.HasValue ? previousWindowBorder.Value : windowBorder); ProcessEvents(); suppressResize--; deferredWindowBorder = null; previousWindowBorder = null; } public override WindowState WindowState { get { return windowState; } set { var oldState = windowState; if (oldState == value) return; RestoreWindowState(); if (value == WindowState.Fullscreen) { if (MacOSFactory.ExclusiveFullscreen) { normalLevel = Cocoa.SendInt(windowInfo.Handle, selLevel); var windowLevel = CG.ShieldingWindowLevel(); CG.CaptureAllDisplays(); Cocoa.SendVoid(windowInfo.Handle, selSetLevel, windowLevel); } previousBounds = InternalBounds; previousWindowBorder = WindowBorder; SetMenuVisible(false); HideBorder(); InternalBounds = GetCurrentScreenFrame(); windowState = value; OnWindowStateChanged(EventArgs.Empty); } else if (value == WindowState.Maximized) { WindowShouldZoomToFrame(IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, GetCurrentScreenVisibleFrame()); } else if (value == WindowState.Minimized) { Cocoa.SendVoid(windowInfo.Handle, selMiniaturize, windowInfo.Handle); } else if (value == WindowState.Normal) { windowState = value; OnWindowStateChanged(EventArgs.Empty); OnResize(EventArgs.Empty); } } } public override WindowBorder WindowBorder { get { return windowBorder; } set { // Do not allow border changes during fullscreen mode. if (windowState == WindowState.Fullscreen || windowState == WindowState.Maximized) { deferredWindowBorder = value; return; } if (windowBorder == value) return; SetWindowBorder(value); OnWindowBorderChanged(EventArgs.Empty); } } private void SetWindowBorder(WindowBorder windowBorder) { this.windowBorder = windowBorder; UpdateWindowBorder(); } private static NSWindowStyle GetStyleMask(WindowBorder windowBorder) { switch (windowBorder) { case WindowBorder.Resizable: return NSWindowStyle.Closable | NSWindowStyle.Miniaturizable | NSWindowStyle.Titled | NSWindowStyle.Resizable; case WindowBorder.Fixed: return NSWindowStyle.Closable | NSWindowStyle.Miniaturizable | NSWindowStyle.Titled; case WindowBorder.Hidden: return NSWindowStyle.Borderless; } return (NSWindowStyle)0; } public override System.Drawing.Rectangle Bounds { get { var r = Cocoa.SendRect(windowInfo.Handle, selFrame); return new Rectangle( (int)r.X, (int)(GetCurrentScreenFrame().Height - r.Y - r.Height), (int)r.Width, (int)r.Height); } set { Cocoa.SendVoid(windowInfo.Handle, selSetFrame, new RectangleF( value.X, GetCurrentScreenFrame().Height - value.Y - value.Height, value.Width, value.Height), true); } } private System.Drawing.RectangleF InternalBounds { get { return Cocoa.SendRect(windowInfo.Handle, selFrame); } set { Cocoa.SendVoid(windowInfo.Handle, selSetFrame, value, true); } } public override System.Drawing.Size ClientSize { get { var contentViewBounds = Cocoa.SendRect(windowInfo.ViewHandle, selBounds); var bounds = Cocoa.SendRect(windowInfo.Handle, selConvertRectToBacking, contentViewBounds); return new Size((int)bounds.Width, (int)bounds.Height); } set { var r_scaled = Cocoa.SendRect(windowInfo.Handle, selConvertRectFromBacking, new RectangleF(PointF.Empty, value)); var r = Cocoa.SendRect(windowInfo.Handle, selFrameRectForContentRect, r_scaled); Size = new Size((int)r.Width, (int)r.Height); } } public override MouseCursor Cursor { get { return selectedCursor; } set { selectedCursor = value; InvalidateCursorRects(); } } static IntPtr ToNSCursor(MouseCursor cursor) { // We need to allocate a NSBitmapImageRep, fill it with pixels // and then convert it to a NSImage. // According to the documentation, alpha-enabled formats should // premultiply alpha, even though that "generally has negligible // effect on output quality." IntPtr imgdata = Cocoa.SendIntPtr( Cocoa.SendIntPtr(NSBitmapImageRep, Selector.Alloc), selInitWithBitmapDataPlanes, IntPtr.Zero, cursor.Width, cursor.Height, 8, 4, 1, 0, NSDeviceRGBColorSpace, NSBitmapFormat.AlphaFirst, 4 * cursor.Width, 32); if (imgdata == IntPtr.Zero) { Debug.Print("Failed to create NSBitmapImageRep with size ({0},{1]})", cursor.Width, cursor.Height); return IntPtr.Zero; } // Copy the cursor data int i = 0; IntPtr data = Cocoa.SendIntPtr(imgdata, selBitmapData); for (int y = 0; y < cursor.Height; y++) { for (int x = 0; x < cursor.Width; x++) { uint argb = unchecked((uint)BitConverter.ToInt32(cursor.Data, i)); if (BitConverter.IsLittleEndian) { argb = (argb & 0x000000FFu) << 24 | (argb & 0x0000FF00u) << 8 | (argb & 0x00FF0000u) >> 8 | (argb & 0xFF000000u) >> 24; } Marshal.WriteInt32(data, i, unchecked((int)argb)); i += 4; } } // Construct the actual NSImage IntPtr img = Cocoa.SendIntPtr( Cocoa.SendIntPtr(NSImage, Selector.Alloc), selInitWithSize, new SizeF(cursor.Width, cursor.Height)); if (img == IntPtr.Zero) { Debug.Print("Failed to construct NSImage from NSBitmapImageRep"); Cocoa.SendVoid(imgdata, Selector.Release); return IntPtr.Zero; } Cocoa.SendVoid(img, selAddRepresentation, imgdata); // Convert the NSImage to a NSCursor IntPtr nscursor = Cocoa.SendIntPtr( Cocoa.SendIntPtr(NSCursor, Selector.Alloc), selInitWithImageHotSpot, img, new PointF(cursor.X, cursor.Y)); Cocoa.SendVoid(imgdata, Selector.Release); Cocoa.SendVoid(img, Selector.Release); return nscursor; } void ResetCursorRects(IntPtr sender, IntPtr cmd) { // We will add a new cursor rectangle that covers the complete view var rect = Cocoa.SendRect(windowInfo.ViewHandle, selBounds); // Inside this rectangle, the following NSCursor will be used var cursor = IntPtr.Zero; if (selectedCursor == MouseCursor.Default) { cursor = Cocoa.SendIntPtr(NSCursor, selArrowCursor); } else { cursor = ToNSCursor(selectedCursor); } // Setup the cursor rectangle if (cursor != IntPtr.Zero) { Cocoa.SendVoid(sender, selAddCursorRect, rect, cursor); } } void InvalidateCursorRects() { Cocoa.SendVoid(windowInfo.Handle, selInvalidateCursorRectsForView, windowInfo.ViewHandle); } public override bool CursorVisible { get { return cursorVisible; } set { if (value && !cursorVisible) { SetCursorVisible(true); } else if (!value && cursorVisible) { SetCursorVisible(false); } cursorVisible = value; } } protected override void Dispose(bool disposing) { if (disposed) return; Debug.Print("Disposing of CocoaNativeWindow (disposing={0}).", disposing); if (!NSApplication.IsUIThread) return; NSApplication.Quit -= ApplicationQuit; if (disposing) { CursorVisible = true; if (exists) { CloseWindow(true); } if (trackingArea != IntPtr.Zero) { Cocoa.SendVoid(windowInfo.ViewHandle, selRemoveTrackingArea, trackingArea); Cocoa.SendVoid(trackingArea, Selector.Release); trackingArea = IntPtr.Zero; } Debug.Print("[Mac] Disposing {0}", windowInfo); windowInfo.Dispose(); } else { Debug.Print("{0} leaked, did you forget to call Dispose()?", GetType().FullName); } disposed = true; OnDisposed(EventArgs.Empty); } public static IntPtr GetView(IntPtr windowHandle) { return Cocoa.SendIntPtr(windowHandle, selContentView); } private RectangleF GetContentViewFrame() { return Cocoa.SendRect(windowInfo.ViewHandle, selFrame); } private IntPtr GetCurrentScreen() { return Cocoa.SendIntPtr(windowInfo.Handle, selScreen); } private RectangleF GetCurrentScreenFrame() { return Cocoa.SendRect(GetCurrentScreen(), selFrame); } private RectangleF GetCurrentScreenVisibleFrame() { return Cocoa.SendRect(GetCurrentScreen(), selVisibleFrame); } private void SetCursorVisible(bool visible) { // If the mouse is outside the window and we want to hide it, // move it inside the window first. // Otherwise, if we are making the cursor visible again, // we place it in the same spot as reported in the current // MouseState to avoid sudden jumps. if (!visible && !Bounds.Contains(new Point(MouseState.X, MouseState.Y))) { Mouse.SetPosition( (Bounds.Left + Bounds.Right) / 2, (Bounds.Top + Bounds.Bottom) / 2); } else if (visible) { var p = PointToScreen(new Point(MouseState.X, MouseState.Y)); Mouse.SetPosition((int)p.X, (int)p.Y); } CG.AssociateMouseAndMouseCursorPosition(visible); Cocoa.SendVoid(NSCursor, visible ? selUnhide : selHide); } private void SetMenuVisible(bool visible) { var options = (NSApplicationPresentationOptions)Cocoa.SendInt(NSApplication.Handle, selPresentationOptions); var changedOptions = NSApplicationPresentationOptions.HideMenuBar | NSApplicationPresentationOptions.HideDock; if (!visible) { options |= changedOptions; } else { options &= ~changedOptions; } Cocoa.SendVoid(NSApplication.Handle, selSetPresentationOptions, (int)options); } private void SetTitle(string newTitle, bool callEvent) { title = newTitle ?? ""; Cocoa.SendIntPtr(windowInfo.Handle, selSetTitle, Cocoa.ToNSString(title)); if (callEvent) { OnTitleChanged(EventArgs.Empty); } } private void UpdateWindowBorder() { Cocoa.SendVoid(windowInfo.Handle, selSetStyleMask, (uint)GetStyleMask(windowBorder)); SetTitle(title, false); // Title gets lost after going borderless } private NSWindowStyle GetStyleMask() { return (NSWindowStyle)Cocoa.SendUint(windowInfo.Handle, selStyleMask); } void CloseWindow(bool shutdown) { if (!Exists) return; exists = false; // PerformClose is equivalent to pressing the close-button, which // does not work in a borderless window. Handle this special case. if (GetStyleMask() == NSWindowStyle.Borderless || shutdown) { if (WindowShouldClose(IntPtr.Zero, IntPtr.Zero, IntPtr.Zero)) { Cocoa.SendVoid(windowInfo.Handle, selClose); } } else { Cocoa.SendVoid(windowInfo.Handle, selPerformClose, windowInfo.Handle); } } } }