#region --- License --- /* Copyright (c) 2007 Stefanos Apostolopoulos * See license.txt for license info */ #endregion using System; using System.Collections.Generic; using System.Text; using System.Runtime.InteropServices; using System.Diagnostics; using System.Reflection; using OpenTK.OpenGL; using OpenTK.Input; using OpenTK.Platform.Windows; using OpenTK.Graphics; //using OpenTK.OpenGL; namespace OpenTK.Platform.X11 { /// /// Drives GameWindow on X11. /// This class supports OpenTK, and is not intended for use by OpenTK programs. /// internal sealed class X11GLNative : INativeGLWindow, IDisposable { // TODO: Disable screensaver. // TODO: What happens if we can't disable decorations through motif? // TODO: Mouse/keyboard grabbing/wrapping. // TODO: PointToWindow, PointToScreen #region --- Fields --- private WindowInfo window = new WindowInfo(); private DisplayMode mode = new DisplayMode(); private X11Input driver; // Window manager hints for fullscreen windows. private const string MOTIF_WM_ATOM = "_MOTIF_WM_HINTS"; private const string KDE_WM_ATOM = "KWM_WIN_DECORATION"; private const string KDE_NET_WM_ATOM = "_KDE_NET_WM_WINDOW_TYPE"; private const string ICCM_WM_ATOM = "_NET_WM_WINDOW_TYPE"; // Number of pending events. private int pending = 0; private int top, bottom, left, right; // C# ResizeEventArgs private ResizeEventArgs resizeEventArgs = new ResizeEventArgs(); // Used for event loop. private XEvent e = new XEvent(); private bool disposed; private bool exists; private bool isExiting; // XAtoms for window properties private static IntPtr WMTitle; // The title of the GameWindow. private static IntPtr UTF8String; // No idea. // Fields used for fullscreen mode changes. private int pre_fullscreen_width, pre_fullscreen_height; private bool fullscreen = false; #endregion #region --- Public Constructors --- /// /// Constructs and initializes a new X11GLNative window. /// Call CreateWindow to create the actual render window. /// public X11GLNative() { Debug.Print("Native window driver: {0}", this.ToString()); //Utilities.ThrowOnX11Error = true; // Not very reliable // Open the display to the X server, and obtain the screen and root window. //window.Display = API.OpenDisplay(null); // null == default display window.Display = API.DefaultDisplay; if (window.Display == IntPtr.Zero) throw new Exception("Could not open connection to X"); window.Screen = API.DefaultScreen;//Functions.XDefaultScreen(window.Display); window.RootWindow = API.RootWindow;//Functions.XRootWindow(window.Display, window.Screen); Debug.Print("Display: {0}, Screen {1}, Root window: {2}", window.Display, window.Screen, window.RootWindow); RegisterAtoms(window); } #endregion #region private static void RegisterAtoms() /// /// Not used yet. /// Registers the necessary atoms for GameWindow. /// private static void RegisterAtoms(WindowInfo window) { string[] atom_names = new string[] { "WM_TITLE", "UTF8_STRING" }; IntPtr[] atoms = new IntPtr[atom_names.Length]; //Functions.XInternAtoms(window.Display, atom_names, atom_names.Length, false, atoms); int offset = 0; WMTitle = atoms[offset++]; UTF8String = atoms[offset++]; } #endregion #region --- INativeGLWindow Members --- #region public void ProcessEvents() public void ProcessEvents() { // Process all pending events while (true) { //pending = Functions.XPending(window.Display); pending = API.Pending(window.Display); if (pending == 0) return; Functions.XNextEvent(window.Display, ref e); //Debug.Print("Event: {0} ({1} pending)", e.type, pending); // Respond to the event e switch (e.type) { case XEventName.MapNotify: Debug.WriteLine("Window mapped."); return; case XEventName.CreateNotify: // A child was was created - nothing to do break; case XEventName.ClientMessage: this.OnDestroy(EventArgs.Empty); break; case XEventName.DestroyNotify: exists = false; isExiting = true; Debug.Print("X11 window {0} destroyed.", e.DestroyWindowEvent.window); return; case XEventName.ConfigureNotify: // If the window size changed, raise the C# Resize event. if (e.ConfigureEvent.width != mode.Width || e.ConfigureEvent.height != mode.Height) { Debug.WriteLine( String.Format( "New res: {0}x{1}", e.ConfigureEvent.width, e.ConfigureEvent.height ) ); resizeEventArgs.Width = e.ConfigureEvent.width; resizeEventArgs.Height = e.ConfigureEvent.height; this.OnResize(resizeEventArgs); } break; case XEventName.KeyPress: case XEventName.KeyRelease: case XEventName.MotionNotify: case XEventName.ButtonPress: case XEventName.ButtonRelease: //Functions.XPutBackEvent(window.Display, ref e); driver.ProcessEvent(ref e); break; default: Debug.WriteLine(String.Format("{0} event was not handled", e.type)); break; } } } #endregion #region public IInputDriver InputDriver public IInputDriver InputDriver { get { return driver; } } #endregion #region public bool Exists /// /// Returns true if a render window/context exists. /// public bool Exists { get { return exists; } } #endregion #region public bool Quit public bool IsExiting { get { return isExiting; } } #endregion #region public bool IsIdle public bool IsIdle { get { throw new Exception("The method or operation is not implemented."); } } #endregion #region public bool Fullscreen public bool Fullscreen { get { return fullscreen; } set { if (value && !fullscreen) { Debug.Print("Going fullscreen"); Debug.Indent(); DisableWindowDecorations(); pre_fullscreen_height = this.Height; pre_fullscreen_width = this.Width; //Functions.XRaiseWindow(this.window.Display, this.Handle); Functions.XMoveResizeWindow(this.window.Display, this.Handle, 0, 0, DisplayDevice.PrimaryDisplay.Width, DisplayDevice.PrimaryDisplay.Height); Debug.Unindent(); fullscreen = true; } else if (!value && fullscreen) { Debug.Print("Going windowed"); Debug.Indent(); Functions.XMoveResizeWindow(this.window.Display, this.Handle, 0, 0, pre_fullscreen_width, pre_fullscreen_height); pre_fullscreen_height = pre_fullscreen_width = 0; EnableWindowDecorations(); Debug.Unindent(); fullscreen = false; } /* Debug.Print(value ? "Going fullscreen" : "Going windowed"); IntPtr state_atom = Functions.XInternAtom(this.window.Display, "_NET_WM_STATE", false); IntPtr fullscreen_atom = Functions.XInternAtom(this.window.Display, "_NET_WM_STATE_FULLSCREEN", false); XEvent xev = new XEvent(); xev.ClientMessageEvent.type = XEventName.ClientMessage; xev.ClientMessageEvent.serial = IntPtr.Zero; xev.ClientMessageEvent.send_event = true; xev.ClientMessageEvent.window = this.Handle; xev.ClientMessageEvent.message_type = state_atom; xev.ClientMessageEvent.format = 32; xev.ClientMessageEvent.ptr1 = (IntPtr)(value ? NetWindowManagerState.Add : NetWindowManagerState.Remove); xev.ClientMessageEvent.ptr2 = (IntPtr)(value ? 1 : 0); xev.ClientMessageEvent.ptr3 = IntPtr.Zero; Functions.XSendEvent(this.window.Display, API.RootWindow, false, (IntPtr)(EventMask.SubstructureRedirectMask | EventMask.SubstructureNotifyMask), ref xev); fullscreen = !fullscreen; */ } } #endregion #region public IntPtr Handle /// /// Gets the current window handle. /// public IntPtr Handle { get { return this.window.Handle; } } #endregion #region public string Title /// /// TODO: Use atoms for this property. /// Gets or sets the GameWindow title. /// public string Title { get { IntPtr name = IntPtr.Zero; Functions.XFetchName(window.Display, window.Handle, ref name); if (name != IntPtr.Zero) return Marshal.PtrToStringAnsi(name); return String.Empty; } set { /* XTextProperty name = new XTextProperty(); name.format = 8; //STRING if (value == null) name.value = String.Empty; else name.value = value; Functions.XSetWMName(window.Display, window.Handle, ref name); */ if (value != null) Functions.XStoreName(window.Display, window.Handle, value); } } #endregion #region public bool Visible bool mapped; public bool Visible { get { //return true; return mapped; } set { if (value && !mapped) { Functions.XMapWindow(window.Display, window.Handle); mapped = true; } else if (!value && mapped) { Functions.XUnmapWindow(window.Display, window.Handle); mapped = false; } } } #endregion /* #region public IInputDriver InputDriver public IInputDriver InputDriver { get { return driver; } } #endregion */ #region public IWindowInfo WindowInfo public IWindowInfo WindowInfo { get { return window; } } #endregion #region public void CreateWindow(DisplayMode mode, out IGLContext glContext) /// /// Opens a new render window with the given DisplayMode. /// /// The DisplayMode of the render window. /// /// Creates the window visual and colormap. Associates the colormap/visual /// with the window and raises the window on top of the window stack. /// /// Colormap creation is currently disabled. /// /// public void CreateWindow(int width, int height, DisplayMode mode, out IGLContext glContext) { if (exists) throw new ApplicationException("Render window already exists!"); Debug.Print("Creating GameWindow with mode: {0}", mode != null ? mode.ToString() : "default"); Debug.Indent(); glContext = new X11GLContext(); (glContext as IGLContextCreationHack).SelectDisplayMode(mode, window); if (glContext == null) throw new ApplicationException("Could not create GLContext"); Debug.Print("Created GLContext"); window.VisualInfo = ((X11.WindowInfo)((IGLContextInternal)glContext).Info).VisualInfo; //window.VisualInfo = Marshal.PtrToStructure(Glx.ChooseVisual(window.Display, window.Screen, // Create a window on this display using the visual above Debug.Write("Opening render window... "); XSetWindowAttributes attributes = new XSetWindowAttributes(); attributes.background_pixel = IntPtr.Zero; attributes.border_pixel = IntPtr.Zero; attributes.colormap = API.CreateColormap(window.Display, window.RootWindow, window.VisualInfo.visual, 0/*AllocNone*/); window.EventMask = EventMask.StructureNotifyMask | EventMask.SubstructureNotifyMask | EventMask.ExposureMask | EventMask.KeyReleaseMask | EventMask.KeyPressMask | EventMask.PointerMotionMask | /* Bad! EventMask.PointerMotionHintMask | */ EventMask.ButtonPressMask | EventMask.ButtonReleaseMask; attributes.event_mask = (IntPtr)window.EventMask; uint mask = (uint)SetWindowValuemask.ColorMap | (uint)SetWindowValuemask.EventMask | (uint)SetWindowValuemask.BackPixel | (uint)SetWindowValuemask.BorderPixel; window.Handle = Functions.XCreateWindow(window.Display, window.RootWindow, 0, 0, mode.Width, mode.Height, 0, window.VisualInfo.depth/*(int)CreateWindowArgs.CopyFromParent*/, (int)CreateWindowArgs.InputOutput, window.VisualInfo.visual, (UIntPtr)mask, ref attributes); if (window.Handle == IntPtr.Zero) throw new ApplicationException("XCreateWindow call failed (returned 0)."); // Set the window hints XSizeHints hints = new XSizeHints(); hints.x = 0; hints.y = 0; hints.width = mode.Width; hints.height = mode.Height; hints.flags = (IntPtr)(XSizeHintsFlags.USSize | XSizeHintsFlags.USPosition); Functions.XSetWMNormalHints(window.Display, window.Handle, ref hints); // Register for window destroy notification IntPtr wm_destroy_atom = Functions.XInternAtom(window.Display, "WM_DELETE_WINDOW", true); //XWMHints hint = new XWMHints(); Functions.XSetWMProtocols(window.Display, window.Handle, new IntPtr[] { wm_destroy_atom }, 1); Top = Left = 0; Right = Width; Bottom = Height; //XTextProperty text = new XTextProperty(); //text.value = "OpenTK Game Window"; //text.format = 8; //Functions.XSetWMName(window.Display, window.Handle, ref text); //Functions.XSetWMProperties(display, window, name, name, 0, /*None*/ null, 0, hints); Debug.Print("done! (id: {0})", window.Handle); (glContext as IGLContextCreationHack).SetWindowHandle(window.Handle); API.MapRaised(window.Display, window.Handle); mapped = true; glContext.CreateContext(true, null); driver = new X11Input(window); Debug.Unindent(); Debug.WriteLine("GameWindow creation completed successfully!"); exists = true; } #endregion #region OnCreate public event CreateEvent Create; private void OnCreate(EventArgs e) { if (this.Create != null) { Debug.Print("Create event fired from window: {0}", window.ToString()); this.Create(this, e); } } #endregion #region public void Exit() public void Exit() { this.DestroyWindow(); } #endregion #region public void DestroyWindow() public void DestroyWindow() { Debug.WriteLine("X11GLNative shutdown sequence initiated."); Functions.XDestroyWindow(window.Display, window.Handle); } #endregion #region OnDestroy public event DestroyEvent Destroy; private void OnDestroy(EventArgs e) { Debug.Print("Destroy event fired from window: {0}", window.ToString()); if (this.Destroy != null) this.Destroy(this, e); } #endregion #region PointToClient public void PointToClient(ref System.Drawing.Point p) { /* if (!Functions.ScreenToClient(this.Handle, p)) throw new InvalidOperationException(String.Format( "Could not convert point {0} from client to screen coordinates. Windows error: {1}", p.ToString(), Marshal.GetLastWin32Error())); */ } #endregion #region PointToScreen public void PointToScreen(ref System.Drawing.Point p) { throw new NotImplementedException(); } #endregion #endregion #region --- IResizable Members --- #region public int Width public int Width { get { return mode.Width; } set {/* // Clear event struct //Array.Clear(xresize.pad, 0, xresize.pad.Length); // Set requested parameters xresize.ResizeRequest.type = EventType.ResizeRequest; xresize.ResizeRequest.display = this.display; xresize.ResizeRequest.width = value; xresize.ResizeRequest.height = mode.Width; API.SendEvent( this.display, this.window, false, EventMask.StructureNotifyMask, ref xresize );*/ } } #endregion #region public int Height public int Height { get { return mode.Height; } set {/* // Clear event struct //Array.Clear(xresize.pad, 0, xresize.pad.Length); // Set requested parameters xresize.ResizeRequest.type = EventType.ResizeRequest; xresize.ResizeRequest.display = this.display; xresize.ResizeRequest.width = mode.Width; xresize.ResizeRequest.height = value; API.SendEvent( this.display, this.window, false, EventMask.StructureNotifyMask, ref xresize );*/ } } #endregion #region public event ResizeEvent Resize public event ResizeEvent Resize; private void OnResize(ResizeEventArgs e) { mode.Width = e.Width; mode.Height = e.Height; if (this.Resize != null) { this.Resize(this, e); } } #endregion public int Top { get { return top; } private set { top = value; } } public int Bottom { get { return bottom; } private set { bottom = value; } } public int Left { get { return left; } private set { left = value; } } public int Right { get { return right; } private set { right = value; } } #endregion #region --- IDisposable Members --- public void Dispose() { this.Dispose(true); GC.SuppressFinalize(this); } private void Dispose(bool manuallyCalled) { if (!disposed) { if (manuallyCalled) { //if (glContext != null) // glContext.Dispose(); // Kills connection to the X-Server. We don't want that, // 'cause it kills the ExampleLauncher too. //Functions.XCloseDisplay(window.Display); } Functions.XUnmapWindow(window.Display, window.Handle); Functions.XDestroyWindow(window.Display, window.Handle); disposed = true; } } ~X11GLNative() { this.Dispose(false); } #endregion #region --- Private Methods --- #region void DisableWindowDecorations() void DisableWindowDecorations() { bool removed = false; if (DisableMotifDecorations()) { Debug.Print("Removed decorations through motif."); removed = true; } if (DisableGnomeDecorations()) { Debug.Print("Removed decorations through gnome."); removed = true; } if (DisableIccmDecorations()) { Debug.Print("Removed decorations through ICCM."); removed = true; } if (removed) { Functions.XSetTransientForHint(this.window.Display, this.Handle, this.window.RootWindow); Functions.XUnmapWindow(this.window.Display, this.Handle); Functions.XMapWindow(this.window.Display, this.Handle); } } #region bool DisableMotifDecorations() bool DisableMotifDecorations() { IntPtr atom = Functions.XInternAtom(this.window.Display, MOTIF_WM_ATOM, true); if (atom != IntPtr.Zero) { MotifWmHints hints = new MotifWmHints(); hints.flags = (IntPtr)MotifFlags.Decorations; Functions.XChangeProperty(this.window.Display, this.Handle, atom, atom, 32, PropertyMode.Replace, ref hints, Marshal.SizeOf(hints) / 4); return true; } return false; } #endregion #region bool DisableGnomeDecorations() bool DisableGnomeDecorations() { // Attempt to cover gnome panels. //XEvent xev = new XEvent(); //xev.ClientMessageEvent.window = this.window.Handle; //xev.ClientMessageEvent.type = XEventName.ClientMessage; //xev.ClientMessageEvent.message_type = Functions.XInternAtom(this.window.Display, Constants.XA_WIN_LAYER, false); //xev.ClientMessageEvent.format = 32; //xev.ClientMessageEvent.ptr1 = (IntPtr)WindowLayer.AboveDock; //Functions.XSendEvent(this.window.Display, this.window.RootWindow, false, (IntPtr)EventMask.SubstructureNotifyMask, ref xev); //xev = new XEvent(); //xev.ClientMessageEvent.window = this.window.Handle; //xev.ClientMessageEvent.type = XEventName.ClientMessage; //xev.ClientMessageEvent.message_type = Functions.XInternAtom(this.window.Display, Constants.XA_WIN_STATE, false); //xev.ClientMessageEvent.format = 32; //xev.ClientMessageEvent.ptr1 = (IntPtr)WindowState.; //xev.ClientMessageEvent.ptr2 = (IntPtr)WindowLayer.AboveDock; //Functions.XSendEvent(this.window.Display, this.window.RootWindow, false, (IntPtr)EventMask.SubstructureNotifyMask, ref xev); IntPtr atom = Functions.XInternAtom(this.window.Display, Constants.XA_WIN_HINTS, true); if (atom != IntPtr.Zero) { IntPtr hints = IntPtr.Zero; Functions.XChangeProperty(this.window.Display, this.Handle, atom, atom, 32, PropertyMode.Replace, ref hints, Marshal.SizeOf(hints) / 4); return true; } return false; } #endregion #region bool DisableIccmDecorations() bool DisableIccmDecorations() { IntPtr atom = Functions.XInternAtom(this.window.Display, ICCM_WM_ATOM, true); if (atom != IntPtr.Zero) { IntPtr hints = Functions.XInternAtom(this.window.Display, "_NET_WM_STATE_FULLSCREEN", true); Functions.XChangeProperty(this.window.Display, this.Handle, atom, atom, 32, PropertyMode.Replace, ref hints, Marshal.SizeOf(hints) / 4); return true; } return false; } #endregion #endregion #region void EnableWindowDecorations() void EnableWindowDecorations() { bool activated = false; if (EnableMotifDecorations()) { Debug.Print("Activated decorations through motif."); activated = true; } if (EnableGnomeDecorations()) { Debug.Print("Activated decorations through gnome."); activated = true; } if (EnableIccmDecorations()) { Debug.Print("Activated decorations through ICCM."); activated = true; } if (activated) { Functions.XSetTransientForHint(this.window.Display, this.Handle, this.window.RootWindow); Functions.XUnmapWindow(this.window.Display, this.Handle); Functions.XMapWindow(this.window.Display, this.Handle); } } #region bool EnableMotifDecorations() bool EnableMotifDecorations() { IntPtr atom = Functions.XInternAtom(this.window.Display, MOTIF_WM_ATOM, true); if (atom != IntPtr.Zero) { Functions.XDeleteProperty(this.window.Display, this.Handle, atom); return true; } return false; } #endregion #region bool EnableGnomeDecorations() bool EnableGnomeDecorations() { // Restore window layer. //XEvent xev = new XEvent(); //xev.ClientMessageEvent.window = this.window.Handle; //xev.ClientMessageEvent.type = XEventName.ClientMessage; //xev.ClientMessageEvent.message_type = Functions.XInternAtom(this.window.Display, Constants.XA_WIN_LAYER, false); //xev.ClientMessageEvent.format = 32; //xev.ClientMessageEvent.ptr1 = (IntPtr)WindowLayer.AboveDock; //Functions.XSendEvent(this.window.Display, this.window.RootWindow, false, (IntPtr)EventMask.SubstructureNotifyMask, ref xev); IntPtr atom = Functions.XInternAtom(this.window.Display, Constants.XA_WIN_HINTS, true); if (atom != IntPtr.Zero) { Functions.XDeleteProperty(this.window.Display, this.Handle, atom); return true; } return false; } #endregion #region bool EnableIccmDecorations() bool EnableIccmDecorations() { IntPtr atom = Functions.XInternAtom(this.window.Display, ICCM_WM_ATOM, true); if (atom != IntPtr.Zero) { IntPtr hint = Functions.XInternAtom(this.window.Display, "_NET_WM_WINDOW_TYPE_NORMAL", true); if (hint != IntPtr.Zero) { Functions.XChangeProperty(this.window.Display, this.Handle, hint, /*XA_ATOM*/(IntPtr)4, 32, PropertyMode.Replace, ref hint, Marshal.SizeOf(hint) / 4); } return true; } return false; } #endregion #endregion #endregion } }