#region License // // The Open Toolkit Library License // // Copyright (c) 2006 - 2010 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.Diagnostics; using System.Runtime.InteropServices; using System.Threading; using OpenTK.Input; namespace OpenTK.Platform.X11 { sealed class XI2MouseKeyboard : IKeyboardDriver2, IMouseDriver2, IDisposable { static readonly IntPtr ExitAtom; readonly object Sync = new object(); readonly Thread ProcessingThread; readonly X11KeyMap KeyMap; bool disposed; class XIMouse { public MouseState State; public XIDeviceInfo DeviceInfo; public XIScrollClassInfo ScrollX = new XIScrollClassInfo { number = -1 }; public XIScrollClassInfo ScrollY = new XIScrollClassInfo { number = -1 }; public XIValuatorClassInfo MotionX = new XIValuatorClassInfo { number = -1 }; public XIValuatorClassInfo MotionY = new XIValuatorClassInfo { number = -1 }; public string Name; } class XIKeyboard { public KeyboardState State; public XIDeviceInfo DeviceInfo; public string Name; } long cursor_x, cursor_y; // For GetCursorState() List devices = new List(); // list of connected mice Dictionary rawids = new Dictionary(); // maps hardware device ids to XIMouse ids List keyboards = new List(); // list of connected keybords Dictionary keyboard_ids = new Dictionary(); // maps hardware device ids to XIKeyboard ids internal readonly X11WindowInfo window; internal static int XIOpCode { get; private set; } internal static int XIVersion { get; private set; } static readonly Functions.EventPredicate PredicateImpl = IsEventValid; readonly IntPtr Predicate = Marshal.GetFunctionPointerForDelegate(PredicateImpl); static XI2MouseKeyboard() { using (new XLock(API.DefaultDisplay)) { ExitAtom = Functions.XInternAtom(API.DefaultDisplay, "Exit Input Thread Message", false); } } public XI2MouseKeyboard() { window = new X11WindowInfo(); window.Display = Functions.XOpenDisplay(IntPtr.Zero); using (new XLock(window.Display)) { XSetWindowAttributes attr = new XSetWindowAttributes(); window.Screen = Functions.XDefaultScreen(window.Display); window.RootWindow = Functions.XRootWindow(window.Display, window.Screen); window.Handle = Functions.XCreateWindow(window.Display, window.RootWindow, 0, 0, 1, 1, 0, 0, CreateWindowArgs.InputOnly, IntPtr.Zero, SetWindowValuemask.Nothing, attr); KeyMap = new X11KeyMap(window.Display); } if (!IsSupported(window.Display)) throw new NotSupportedException("XInput2 not supported."); // Enable XI2 mouse/keyboard events // Note: the input event loop blocks waiting for these events // *or* a custom ClientMessage event that instructs us to exit. // See SendExitEvent() below. using (new XLock(window.Display)) using (XIEventMask mask = new XIEventMask(1, XIEventMasks.RawKeyPressMask | XIEventMasks.RawKeyReleaseMask | XIEventMasks.RawButtonPressMask | XIEventMasks.RawButtonReleaseMask | XIEventMasks.RawMotionMask | XIEventMasks.MotionMask | XIEventMasks.DeviceChangedMask)) { XI.SelectEvents(window.Display, window.RootWindow, mask); UpdateDevices(); } ProcessingThread = new Thread(ProcessEvents); ProcessingThread.IsBackground = true; ProcessingThread.Start(); } // Checks whether XInput2 is supported on the specified display. // If a display is not specified, the default display is used. internal static bool IsSupported(IntPtr display) { try { if (display == IntPtr.Zero) { display = API.DefaultDisplay; } using (new XLock(display)) { int major, ev, error; if (Functions.XQueryExtension(display, "XInputExtension", out major, out ev, out error) != 0) { XIOpCode = major; int minor = 2; while (minor >= 0) { if (XI.QueryVersion(display, ref major, ref minor) == ErrorCodes.Success) { XIVersion = major * 100 + minor * 10; return true; } minor--; } } } } catch (DllNotFoundException e) { Debug.Print(e.ToString()); Debug.Print("XInput2 extension not supported. Mouse support will suffer."); } return false; } #region IKeyboardDriver2 Members KeyboardState IKeyboardDriver2.GetState() { lock (Sync) { KeyboardState state = new KeyboardState(); foreach (XIKeyboard k in keyboards) { state.MergeBits(k.State); } return state; } } KeyboardState IKeyboardDriver2.GetState(int index) { lock (Sync) { if (index >= 0 && index < keyboards.Count) { return keyboards[index].State; } return new KeyboardState(); } } string IKeyboardDriver2.GetDeviceName(int index) { lock (Sync) { if (index >= 0 && index < keyboards.Count) { return keyboards[index].Name; } return String.Empty; } } #endregion #region IMouseDriver2 Members MouseState IMouseDriver2.GetState() { lock (Sync) { MouseState master = new MouseState(); foreach (var d in devices) { master.MergeBits(d.State); } return master; } } MouseState IMouseDriver2.GetState(int index) { lock (Sync) { if (index >= 0 && index < devices.Count) { return devices[index].State; } return new MouseState(); } } MouseState IMouseDriver2.GetCursorState() { lock (Sync) { MouseState master = (this as IMouseDriver2).GetState(); master.X = (int)Interlocked.Read(ref cursor_x); master.Y = (int)Interlocked.Read(ref cursor_y); return master; } } void IMouseDriver2.SetPosition(double x, double y) { // Note: we cannot use window.Display here, because // that will deadlock the input thread, which is // blocking inside XIfEvent using (new XLock(API.DefaultDisplay)) { Functions.XWarpPointer(API.DefaultDisplay, IntPtr.Zero, window.RootWindow, 0, 0, 0, 0, (int)x, (int)y); Functions.XFlush(API.DefaultDisplay); Interlocked.Exchange(ref cursor_x, (long)x); Interlocked.Exchange(ref cursor_y, (long)y); } } #endregion #region Private Members void UpdateDevices() { lock (Sync) { devices.Clear(); keyboards.Clear(); int count; unsafe { XIDeviceInfo* list = (XIDeviceInfo*)XI.QueryDevice(window.Display, XI.XIAllDevices, out count); Debug.Print("Refreshing input device list"); Debug.Print("{0} input devices detected", count); for (int i = 0; i < count; i++) { switch ((list + i)->use) { case XIDeviceType.MasterKeyboard: //case XIDeviceType.SlaveKeyboard: { XIKeyboard k = new XIKeyboard(); k.DeviceInfo = *(list + i); k.State.SetIsConnected(k.DeviceInfo.enabled); k.Name = Marshal.PtrToStringAnsi(k.DeviceInfo.name); int id = k.DeviceInfo.deviceid; if (!keyboard_ids.ContainsKey(id)) { keyboard_ids.Add(k.DeviceInfo.deviceid, 0); } keyboard_ids[id] = keyboards.Count; keyboards.Add(k); } break; case XIDeviceType.MasterPointer: //case XIDeviceType.SlavePointer: case XIDeviceType.FloatingSlave: { XIMouse d = new XIMouse(); d.DeviceInfo = *(list + i); d.State.SetIsConnected(d.DeviceInfo.enabled); d.Name = Marshal.PtrToStringAnsi(d.DeviceInfo.name); Debug.Print("Device {0} \"{1}\" is {2} and has:", i, d.Name, d.DeviceInfo.enabled ? "enabled" : "disabled"); // Decode the XIDeviceInfo to axes, buttons and scroll types for (int j = 0; j < d.DeviceInfo.num_classes; j++) { XIAnyClassInfo* class_info = *((XIAnyClassInfo**)d.DeviceInfo.classes + j); switch (class_info->type) { case XIClassType.Button: { XIButtonClassInfo* button = (XIButtonClassInfo*)class_info; Debug.Print("\t{0} buttons", button->num_buttons); } break; case XIClassType.Scroll: { XIScrollClassInfo* scroll = (XIScrollClassInfo*)class_info; switch (scroll->scroll_type) { case XIScrollType.Vertical: Debug.WriteLine("\tSmooth vertical scrolling"); d.ScrollY = *scroll; break; case XIScrollType.Horizontal: Debug.WriteLine("\tSmooth horizontal scrolling"); d.ScrollX = *scroll; break; default: Debug.Print("\tUnknown scrolling type {0}", scroll->scroll_type); break; } } break; case XIClassType.Valuator: { // We use relative x/y valuators for mouse movement. // Iff these are not available, we fall back to // absolute x/y valuators. XIValuatorClassInfo* valuator = (XIValuatorClassInfo*)class_info; if (valuator->label == XI.RelativeX) { Debug.WriteLine("\tRelative X movement"); d.MotionX = *valuator; } else if (valuator->label == XI.RelativeY) { Debug.WriteLine("\tRelative Y movement"); d.MotionY = *valuator; } else if (valuator->label == XI.AbsoluteX) { Debug.WriteLine("\tAbsolute X movement"); if (d.MotionX.number == -1) d.MotionX = *valuator; } else if (valuator->label == XI.AbsoluteY) { Debug.WriteLine("\tAbsolute X movement"); if (d.MotionY.number == -1) d.MotionY = *valuator; } else { IntPtr label = Functions.XGetAtomName(window.Display, valuator->label); Debug.Print("\tUnknown valuator {0}", Marshal.PtrToStringAnsi(label)); Functions.XFree(label); } } break; } } // Map the hardware device id to the current XIMouse id int id = d.DeviceInfo.deviceid; if (!rawids.ContainsKey(id)) { rawids.Add(id, 0); } rawids[id] = devices.Count; devices.Add(d); } break; } } XI.FreeDeviceInfo((IntPtr)list); } } } void ProcessEvents() { while (!disposed) { XEvent e = new XEvent(); XGenericEventCookie cookie; using (new XLock(window.Display)) { Functions.XIfEvent(window.Display, ref e, Predicate, new IntPtr(XIOpCode)); if (e.type == XEventName.ClientMessage && e.ClientMessageEvent.ptr1 == ExitAtom) { Functions.XDestroyWindow(window.Display, window.Handle); window.Handle = IntPtr.Zero; break; } if (e.type == XEventName.GenericEvent && e.GenericEvent.extension == XIOpCode) { IntPtr dummy; int x, y, dummy2; Functions.XQueryPointer(window.Display, window.RootWindow, out dummy, out dummy, out x, out y, out dummy2, out dummy2, out dummy2); Interlocked.Exchange(ref cursor_x, (long)x); Interlocked.Exchange(ref cursor_y, (long)y); cookie = e.GenericEventCookie; if (Functions.XGetEventData(window.Display, ref cookie) != 0) { switch ((XIEventType)cookie.evtype) { case XIEventType.Motion: // Nothing to do break; case XIEventType.RawKeyPress: case XIEventType.RawKeyRelease: case XIEventType.RawMotion: case XIEventType.RawButtonPress: case XIEventType.RawButtonRelease: // Delivered to all XIMouse instances ProcessRawEvent(ref cookie); break; case XIEventType.DeviceChanged: UpdateDevices(); break; } } Functions.XFreeEventData(window.Display, ref cookie); } } } Debug.WriteLine("[X11] Exiting input event loop."); } void ProcessRawEvent(ref XGenericEventCookie cookie) { lock (Sync) { unsafe { XIRawEvent raw = *(XIRawEvent*)cookie.data; XIMouse mouse; XIKeyboard keyboard; switch (raw.evtype) { case XIEventType.RawMotion: if (GetMouseDevice(raw.deviceid, out mouse)) { ProcessRawMotion(mouse, ref raw); } break; case XIEventType.RawButtonPress: case XIEventType.RawButtonRelease: if (GetMouseDevice(raw.deviceid, out mouse)) { float dx, dy; MouseButton button = X11KeyMap.TranslateButton(raw.detail, out dx, out dy); mouse.State[button] = raw.evtype == XIEventType.RawButtonPress; if (mouse.ScrollX.number == -1 && mouse.ScrollY.number == -1) mouse.State.SetScrollRelative(dx, dy); } break; case XIEventType.RawKeyPress: case XIEventType.RawKeyRelease: if (GetKeyboardDevice(raw.deviceid, out keyboard)) { Key key; if (KeyMap.TranslateKey(raw.detail, out key)) { keyboard.State[key] = raw.evtype == XIEventType.RawKeyPress; } } break; } } } } bool GetMouseDevice(int deviceid, out XIMouse mouse) { if (!rawids.ContainsKey(deviceid)) { Debug.Print("Unknown mouse device {0} encountered, ignoring.", deviceid); mouse = null; return false; } mouse = devices[rawids[deviceid]]; return true; } bool GetKeyboardDevice(int deviceid, out XIKeyboard keyboard) { if (!keyboard_ids.ContainsKey(deviceid)) { Debug.Print("Unknown keyboard device {0} encountered, ignoring.", deviceid); keyboard = null; return false; } keyboard = keyboards[keyboard_ids[deviceid]]; return true; } unsafe static void ProcessRawMotion(XIMouse d, ref XIRawEvent raw) { // Note: we use the raw values here, without pointer // ballistics and any other modification. double x = 0; double y = 0; double h = 0; double v = 0; if (d.MotionX.number != -1) x = ReadRawValue(ref raw, d.MotionX.number); if (d.MotionY.number != -1) y = ReadRawValue(ref raw, d.MotionY.number); if (d.ScrollX.number != -1) h = ReadRawValue(ref raw, d.ScrollX.number) / d.ScrollX.increment; if (d.ScrollY.number != -1) v = ReadRawValue(ref raw, d.ScrollY.number) / d.ScrollY.increment; if (d.MotionX.mode == XIMode.Relative) d.State.X += (int)Math.Round(x); else d.State.X = (int)Math.Round(x); if (d.MotionY.mode == XIMode.Relative) d.State.Y += (int)Math.Round(y); else d.State.Y = (int)Math.Round(y); // Note: OpenTK follows the windows scrolling convention where // (+h, +v) = (right, up). XI2 uses (+h, +v) = (right, down) // instead, so we need to flip the vertical offset. d.State.SetScrollRelative((float)h, (float)(-v)); } unsafe static double ReadRawValue(ref XIRawEvent raw, int bit) { double value = 0; if (IsBitSet(raw.valuators.mask, bit)) { // Find the offset where this value is stored. // The offset is equal to the number of bits // set in raw.valuators.mask between [0, bit) int offset = 0; for (int i = 0; i < bit; i++) { if (IsBitSet(raw.valuators.mask, i)) { offset++; } } value = *((double*)raw.raw_values + offset); } return value; } static bool IsEventValid(IntPtr display, ref XEvent e, IntPtr arg) { bool valid = false; switch (e.type) { case XEventName.GenericEvent: { long extension = (long)e.GenericEventCookie.extension; valid = extension == arg.ToInt64() && (e.GenericEventCookie.evtype == (int)XIEventType.RawKeyPress || e.GenericEventCookie.evtype == (int)XIEventType.RawKeyRelease || e.GenericEventCookie.evtype == (int)XIEventType.RawMotion || e.GenericEventCookie.evtype == (int)XIEventType.RawButtonPress || e.GenericEventCookie.evtype == (int)XIEventType.RawButtonRelease || e.GenericEventCookie.evtype == (int)XIEventType.DeviceChanged); valid |= extension == 0; break; } case XEventName.ClientMessage: valid = e.ClientMessageEvent.ptr1 == ExitAtom; break; } return valid; } static bool IsBitSet(IntPtr mask, int bit) { unsafe { return bit >= 0 && (*((byte*)mask + (bit >> 3)) & (1 << (bit & 7))) != 0; } } void SendExitEvent() { // Send a ClientMessage event to unblock XIfEvent // and exit the input event loop. using (new XLock(API.DefaultDisplay)) { XEvent ev = new XEvent(); ev.type = XEventName.ClientMessage; ev.ClientMessageEvent.display = window.Display; ev.ClientMessageEvent.window = window.Handle; ev.ClientMessageEvent.format = 32; ev.ClientMessageEvent.ptr1 = ExitAtom; Functions.XSendEvent(API.DefaultDisplay, window.Handle, false, 0, ref ev); Functions.XFlush(API.DefaultDisplay); } } #endregion #region IDisposable Members public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } void Dispose(bool disposing) { if (!disposed) { disposed = true; SendExitEvent(); } } ~XI2MouseKeyboard() { Dispose(false); } #endregion } }