#region License // // The Open Toolkit Library License // // Copyright (c) 2006 - 2009 the Open Toolkit library. // // 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.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Drawing; using System.Text; namespace OpenTK.Platform.MacOS { using Carbon; using Graphics; class CarbonGLNative : INativeWindow { #region Fields CarbonWindowInfo window; CarbonInput mInputDriver; GraphicsContext context; static MacOSKeyMap Keymap = new MacOSKeyMap(); IntPtr uppHandler; string title = "OpenTK Window"; Rectangle bounds, windowedBounds, clientRectangle; bool mIsDisposed = false; bool mExists = true; DisplayDevice mDisplayDevice; WindowAttributes mWindowAttrib; WindowClass mWindowClass; WindowPositionMethod mPositionMethod = WindowPositionMethod.CenterOnMainScreen; int mTitlebarHeight; private WindowBorder windowBorder = WindowBorder.Resizable; private WindowState windowState = WindowState.Normal; static Dictionary mWindows = new Dictionary(); #endregion #region AGL Device Hack static internal Dictionary WindowRefMap { get { return mWindows; } } internal DisplayDevice TargetDisplayDevice { get { return mDisplayDevice; } } #endregion #region Constructors static CarbonGLNative() { Application.Initialize(); } CarbonGLNative() : this(WindowClass.Document, WindowAttributes.StandardDocument | WindowAttributes.StandardHandler | WindowAttributes.InWindowMenu | WindowAttributes.LiveResize) { } CarbonGLNative(WindowClass @class, WindowAttributes attrib) { mWindowClass = @class; mWindowAttrib = attrib; } public CarbonGLNative(int x, int y, int width, int height, string title, GraphicsMode mode, GameWindowFlags options, DisplayDevice device) { CreateNativeWindow(WindowClass.Document, WindowAttributes.StandardDocument | WindowAttributes.StandardHandler | WindowAttributes.InWindowMenu | WindowAttributes.LiveResize, new Rect((short)x, (short)y, (short)width, (short)height)); mDisplayDevice = device; } #endregion #region IDisposable public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (mIsDisposed) return; Debug.Print("Disposing of CarbonGLNative window."); mIsDisposed = true; mExists = false; if (disposing) { mWindows.Remove(window.WindowRef); window.Dispose(); window = null; } DisposeUPP(); } ~CarbonGLNative() { Dispose(false); } #endregion #region Private Members void DisposeUPP() { if (uppHandler != IntPtr.Zero) { //API.RemoveEventHandler(uppHandler); //API.DisposeEventHandlerUPP(uppHandler); } uppHandler = IntPtr.Zero; } void CreateNativeWindow(WindowClass @class, WindowAttributes attrib, Rect r) { Debug.Print("Creating window..."); Debug.Indent(); IntPtr windowRef = API.CreateNewWindow(@class, attrib, r); API.SetWindowTitle(windowRef, title); window = new CarbonWindowInfo(windowRef, true, false); SetLocation(r.X, r.Y); SetSize(r.Width, r.Height); Debug.Unindent(); Debug.Print("Created window."); mWindows.Add(windowRef, new WeakReference(this)); LoadSize(); Rect titleSize = API.GetWindowBounds(window.WindowRef, WindowRegionCode.TitleBarRegion); mTitlebarHeight = titleSize.Height; Debug.Print("Titlebar size: {0}", titleSize); ConnectEvents(); System.Diagnostics.Debug.Print("Attached window events."); } void ConnectEvents() { mInputDriver = new CarbonInput(); EventTypeSpec[] eventTypes = new EventTypeSpec[] { new EventTypeSpec(EventClass.Window, WindowEventKind.WindowClose), new EventTypeSpec(EventClass.Window, WindowEventKind.WindowClosed), new EventTypeSpec(EventClass.Window, WindowEventKind.WindowBoundsChanged), //new EventTypeSpec(EventClass.Mouse, MouseEventKind.MouseDown), //new EventTypeSpec(EventClass.Mouse, MouseEventKind.MouseUp), //new EventTypeSpec(EventClass.Mouse, MouseEventKind.MouseMoved), //new EventTypeSpec(EventClass.Mouse, MouseEventKind.MouseDragged), //new EventTypeSpec(EventClass.Mouse, MouseEventKind.MouseEntered), //new EventTypeSpec(EventClass.Mouse, MouseEventKind.MouseExited), //new EventTypeSpec(EventClass.Mouse, MouseEventKind.WheelMoved), //new EventTypeSpec(EventClass.Keyboard, KeyboardEventKind.RawKeyDown), //new EventTypeSpec(EventClass.Keyboard, KeyboardEventKind.RawKeyRepeat), //new EventTypeSpec(EventClass.Keyboard, KeyboardEventKind.RawKeyUp), //new EventTypeSpec(EventClass.Keyboard, KeyboardEventKind.RawKeyModifiersChanged), }; MacOSEventHandler handler = EventHandler; uppHandler = API.NewEventHandlerUPP(handler); API.InstallWindowEventHandler(window.WindowRef, uppHandler, eventTypes, window.WindowRef, IntPtr.Zero); Application.WindowEventHandler = this; } void Activate() { API.SelectWindow(window.WindowRef); } void Show() { IntPtr parent = IntPtr.Zero; API.ShowWindow(window.WindowRef); API.RepositionWindow(window.WindowRef, parent, WindowPositionMethod); API.SelectWindow(window.WindowRef); } void Hide() { API.HideWindow(window.WindowRef); } void SetFullscreen() { windowedBounds = bounds; ((AglContext)(context as IGraphicsContextInternal).Implementation).SetFullScreen(window); Debug.Print("Prev Size: {0}, {1}", Width, Height); bounds = DisplayDevice.Default.Bounds; Debug.Print("New Size: {0}, {1}", Width, Height); } void UnsetFullscreen() { ((AglContext)(context as IGraphicsContextInternal).Implementation).UnsetFullScreen(window); SetSize((short)windowedBounds.Width, (short)windowedBounds.Height); } bool IsDisposed { get { return mIsDisposed; } } WindowPositionMethod WindowPositionMethod { get { return mPositionMethod; } set { mPositionMethod = value; } } internal OSStatus DispatchEvent(IntPtr inCaller, IntPtr inEvent, EventInfo evt, IntPtr userData) { switch (evt.EventClass) { case EventClass.Window: return ProcessWindowEvent(inCaller, inEvent, evt, userData); case EventClass.Mouse: return ProcessMouseEvent(inCaller, inEvent, evt, userData); case EventClass.Keyboard: return ProcessKeyboardEvent(inCaller, inEvent, evt, userData); default: return OSStatus.EventNotHandled; } } protected static OSStatus EventHandler(IntPtr inCaller, IntPtr inEvent, IntPtr userData) { // bail out if the window passed in is not actually our window. // I think this happens if using winforms with a GameWindow sometimes. if (mWindows.ContainsKey(userData) == false) return OSStatus.EventNotHandled; WeakReference reference = mWindows[userData]; // bail out if the CarbonGLNative window has been garbage collected. if (reference.IsAlive == false) { mWindows.Remove(userData); return OSStatus.EventNotHandled; } CarbonGLNative window = (CarbonGLNative)reference.Target; EventInfo evt = new EventInfo(inEvent); //Debug.Print("Processing {0} event for {1}.", evt, window.window); if (window == null) { Debug.WriteLine("Window for event not found."); return OSStatus.EventNotHandled; } switch (evt.EventClass) { case EventClass.Window: return window.ProcessWindowEvent(inCaller, inEvent, evt, userData); case EventClass.Mouse: return window.ProcessMouseEvent(inCaller, inEvent, evt, userData); case EventClass.Keyboard: return window.ProcessKeyboardEvent(inCaller, inEvent, evt, userData); default: return OSStatus.EventNotHandled; } } private OSStatus ProcessKeyboardEvent(IntPtr inCaller, IntPtr inEvent, EventInfo evt, IntPtr userData) { System.Diagnostics.Debug.Assert(evt.EventClass == EventClass.Keyboard); MacOSKeyCode code; char charCode; switch (evt.KeyboardEventKind) { case KeyboardEventKind.RawKeyRepeat: GetCharCodes(inEvent, out code, out charCode); InputDriver.Keyboard[0].KeyRepeat = true; goto case KeyboardEventKind.RawKeyDown; case KeyboardEventKind.RawKeyDown: GetCharCodes(inEvent, out code, out charCode); InputDriver.Keyboard[0][Keymap[code]] = true; return OSStatus.EventNotHandled; case KeyboardEventKind.RawKeyUp: GetCharCodes(inEvent, out code, out charCode); InputDriver.Keyboard[0][Keymap[code]] = false; return OSStatus.EventNotHandled; case KeyboardEventKind.RawKeyModifiersChanged: ProcessModifierKey(inEvent); return OSStatus.EventNotHandled; default: return OSStatus.EventNotHandled; } } private OSStatus ProcessWindowEvent(IntPtr inCaller, IntPtr inEvent, EventInfo evt, IntPtr userData) { System.Diagnostics.Debug.Assert(evt.EventClass == EventClass.Window); switch (evt.WindowEventKind) { case WindowEventKind.WindowClose: CancelEventArgs cancel = new CancelEventArgs(); OnClosing(cancel); if (cancel.Cancel) return OSStatus.NoError; else return OSStatus.EventNotHandled; case WindowEventKind.WindowClosed: mExists = false; OnClosed(); return OSStatus.NoError; case WindowEventKind.WindowBoundsChanged: int thisWidth = Width; int thisHeight = Height; LoadSize(); if (thisWidth != Width || thisHeight != Height) OnResize(); return OSStatus.EventNotHandled; default: Debug.Print("{0}", evt); return OSStatus.EventNotHandled; } } protected OSStatus ProcessMouseEvent(IntPtr inCaller, IntPtr inEvent, EventInfo evt, IntPtr userData) { System.Diagnostics.Debug.Assert(evt.EventClass == EventClass.Mouse); MouseButton button = MouseButton.Primary; HIPoint pt = new HIPoint(); OSStatus err; if (this.windowState == WindowState.Fullscreen) { err = API.GetEventMouseLocation(inEvent, out pt); } else { err = API.GetEventWindowMouseLocation(inEvent, out pt); } if (err != OSStatus.NoError) { // this error comes up from the application event handler. if (err != OSStatus.EventParameterNotFound) { throw new MacOSException(err); } } if (this.windowState == WindowState.Fullscreen) { InputDriver.Mouse[0].Position = new Point((int)pt.X, (int)pt.Y); } else { // ignore clicks in the title bar if (pt.Y < mTitlebarHeight) return OSStatus.EventNotHandled; InputDriver.Mouse[0].Position = new Point((int)pt.X, (int)(pt.Y - mTitlebarHeight)); } switch (evt.MouseEventKind) { case MouseEventKind.MouseDown: button = API.GetEventMouseButton(inEvent); switch (button) { case MouseButton.Primary: InputDriver.Mouse[0][OpenTK.Input.MouseButton.Left] = true; break; case MouseButton.Secondary: InputDriver.Mouse[0][OpenTK.Input.MouseButton.Right] = true; break; case MouseButton.Tertiary: InputDriver.Mouse[0][OpenTK.Input.MouseButton.Middle] = true; break; } break; case MouseEventKind.MouseUp: switch (button) { case MouseButton.Primary: InputDriver.Mouse[0][OpenTK.Input.MouseButton.Left] = false; break; case MouseButton.Secondary: InputDriver.Mouse[0][OpenTK.Input.MouseButton.Right] = false; break; case MouseButton.Tertiary: InputDriver.Mouse[0][OpenTK.Input.MouseButton.Middle] = false; break; } button = API.GetEventMouseButton(inEvent); break; case MouseEventKind.MouseMoved: case MouseEventKind.MouseDragged: //Debug.Print("MouseMoved: {0}", InputDriver.Mouse[0].Position); return OSStatus.EventNotHandled; default: Debug.Print("{0}", evt); return OSStatus.EventNotHandled; } return OSStatus.EventNotHandled; } private static void GetCharCodes(IntPtr inEvent, out MacOSKeyCode code, out char charCode) { code = API.GetEventKeyboardKeyCode(inEvent); charCode = API.GetEventKeyboardChar(inEvent); } private void ProcessModifierKey(IntPtr inEvent) { MacOSKeyModifiers modifiers = API.GetEventKeyModifiers(inEvent); bool caps = (modifiers & MacOSKeyModifiers.CapsLock) != 0 ? true : false; bool control = (modifiers & MacOSKeyModifiers.Control) != 0 ? true : false; bool command = (modifiers & MacOSKeyModifiers.Command) != 0 ? true : false; bool option = (modifiers & MacOSKeyModifiers.Option) != 0 ? true : false; bool shift = (modifiers & MacOSKeyModifiers.Shift) != 0 ? true : false; Debug.Print("Modifiers Changed: {0}", modifiers); Input.KeyboardDevice keyboard = InputDriver.Keyboard[0]; if (keyboard[OpenTK.Input.Key.AltLeft] ^ option) keyboard[OpenTK.Input.Key.AltLeft] = option; if (keyboard[OpenTK.Input.Key.ShiftLeft] ^ shift) keyboard[OpenTK.Input.Key.ShiftLeft] = shift; if (keyboard[OpenTK.Input.Key.WinLeft] ^ command) keyboard[OpenTK.Input.Key.WinLeft] = command; if (keyboard[OpenTK.Input.Key.ControlLeft] ^ control) keyboard[OpenTK.Input.Key.ControlLeft] = control; if (keyboard[OpenTK.Input.Key.CapsLock] ^ caps) keyboard[OpenTK.Input.Key.CapsLock] = caps; } Rect GetRegion() { Rect retval = API.GetWindowBounds(window.WindowRef, WindowRegionCode.ContentRegion); return retval; } void SetLocation(short x, short y) { if (windowState == WindowState.Fullscreen) return; API.MoveWindow(window.WindowRef, x, y, false); } void SetSize(short width, short height) { if (WindowState == WindowState.Fullscreen) return; // Todo: why call SizeWindow twice? API.SizeWindow(window.WindowRef, width, height, true); bounds.Width = (short)width; bounds.Height = (short)height; Rect contentBounds = API.GetWindowBounds(window.WindowRef, WindowRegionCode.ContentRegion); Rect newSize = new Rect(0, 0, (short)(2 * width - contentBounds.Width), (short)(2 * height - contentBounds.Height)); Debug.Print("Content region was: {0}", contentBounds); Debug.Print("Resizing window to: {0}", newSize); API.SizeWindow(window.WindowRef, newSize.Width, newSize.Height, true); contentBounds = API.GetWindowBounds(window.WindowRef, WindowRegionCode.ContentRegion); Debug.Print("New content region size: {0}", contentBounds); clientRectangle = contentBounds.ToRectangle(); } protected void OnResize() { LoadSize(); if (context != null && this.windowState != WindowState.Fullscreen) context.Update(window); if (Resize != null) { Resize(this, EventArgs.Empty); } } private void LoadSize() { if (WindowState == WindowState.Fullscreen) return; bounds = GetRegion().ToRectangle(); } protected virtual void OnClosing(CancelEventArgs e) { if (Closing != null) Closing(this, e); } protected virtual void OnClosed() { if (Closed != null) Closed(this, EventArgs.Empty); } #endregion #region INativeGLWindow Members public void CreateWindow(int width, int height, GraphicsMode mode, int major, int minor, GraphicsContextFlags flags, out IGraphicsContext context) { Rect r = new Rect(0, 0, (short)width, (short)height); CreateNativeWindow(mWindowClass, mWindowAttrib, r); Show(); this.context = new GraphicsContext(mode, window, major, minor, flags); this.context.MakeCurrent(window); context = this.context; } public void DestroyWindow() { Dispose(); } public void ProcessEvents() { Application.ProcessEvents(); } public Point PointToClient(Point point) { IntPtr handle = window.WindowRef; Rect r = Carbon.API.GetWindowBounds(window.WindowRef, WindowRegionCode.ContentRegion); Console.WriteLine("Rect: {0}", r); return new Point(point.X - r.X, point.Y - r.Y); } public Point PointToScreen(Point point) { throw new NotImplementedException(); } public bool Exists { get { return mExists; } } public IWindowInfo WindowInfo { get { return window; } } public bool IsIdle { get { return true; } } public OpenTK.Input.IInputDriver InputDriver { get { return mInputDriver; } } public bool Fullscreen { get { return false; } set { throw new NotImplementedException(); } } #endregion #region INativeWindow Members public Icon Icon { get { return null; } set { } } public string Title { get { return title; } set { API.SetWindowTitle(window.WindowRef, value); title = value; } } public bool Visible { get { return API.IsWindowVisible(window.WindowRef); } set { if (value && Visible == false) Show(); else Hide(); } } public bool Focused { get { throw new NotImplementedException(); } } public Rectangle Bounds { get { return bounds; } set { Location = value.Location; Size = value.Size; } } public Point Location { get { return bounds.Location; } set { SetLocation((short)value.X, (short)value.Y); } } public Size Size { get { return bounds.Size; } set { SetSize((short)value.Width, (short)value.Height); context.Update(WindowInfo); } } public int Width { get { return Bounds.Width; } set { Size = new Size(value, Height); } } public int Height { get { return Bounds.Height; } set { Size = new Size(Width, value); } } public int X { get { return bounds.X; } set { Location = new Point(value, Y); } } public int Y { get { return bounds.Y; } set { Location = new Point(X, value); } } public Rectangle ClientRectangle { get { return clientRectangle; } set { throw new NotImplementedException(); } } public Size ClientSize { get { return clientRectangle.Size; } set { throw new NotImplementedException(); } } public void Close() { throw new NotImplementedException(); } public WindowState WindowState { get { if (windowState == WindowState.Fullscreen) return WindowState.Fullscreen; if (Carbon.API.IsWindowCollapsed(window.WindowRef)) return WindowState.Minimized; if (Carbon.API.IsWindowInStandardState(window.WindowRef)) { return WindowState.Maximized; } return WindowState.Normal; } set { if (value == WindowState) return; Debug.Print("Switching window state from {0} to {1}", WindowState, value); if (WindowState == WindowState.Fullscreen) { UnsetFullscreen(); } if (WindowState == WindowState.Minimized) { API.CollapseWindow(window.WindowRef, false); } CarbonPoint idealSize; switch (value) { case WindowState.Fullscreen: SetFullscreen(); break; case WindowState.Maximized: // hack because mac os has no concept of maximized. Instead windows are "zoomed" // meaning they are maximized up to their reported ideal size. So we report a // large ideal size. idealSize = new CarbonPoint(9000, 9000); API.ZoomWindowIdeal(window.WindowRef, WindowPartCode.inZoomOut, ref idealSize); break; case WindowState.Normal: if (WindowState == WindowState.Maximized) { idealSize = new CarbonPoint(); API.ZoomWindowIdeal(window.WindowRef, WindowPartCode.inZoomIn, ref idealSize); } break; case WindowState.Minimized: API.CollapseWindow(window.WindowRef, true); break; } windowState = value; if (WindowStateChanged != null) WindowStateChanged(this, EventArgs.Empty); OnResize(); } } public WindowBorder WindowBorder { get { return windowBorder; } set { if (windowBorder != value) { windowBorder = value; if (windowBorder == WindowBorder.Resizable) { API.ChangeWindowAttributes(window.WindowRef, WindowAttributes.Resizable | WindowAttributes.FullZoom, WindowAttributes.NoAttributes); } else if (windowBorder == WindowBorder.Fixed) { API.ChangeWindowAttributes(window.WindowRef, WindowAttributes.NoAttributes, WindowAttributes.Resizable | WindowAttributes.FullZoom); } if (WindowBorderChanged != null) WindowBorderChanged(this, EventArgs.Empty); } } } public event EventHandler Idle; public event EventHandler Load; public event EventHandler Unload; public event EventHandler Move; public event EventHandler Resize; public event EventHandler Closing; public event EventHandler Closed; public event EventHandler Disposed; public event EventHandler IconChanged; public event EventHandler TitleChanged; public event EventHandler ClientSizeChanged; public event EventHandler VisibleChanged; public event EventHandler WindowInfoChanged; public event EventHandler FocusedChanged; public event EventHandler WindowBorderChanged; public event EventHandler WindowStateChanged; public event EventHandler KeyPress; public event EventHandler MouseEnter; public event EventHandler MouseLeave; #endregion } }