diff options
Diffstat (limited to 'CrowX/X11/XI2MouseKeyboard.cs')
| -rw-r--r-- | CrowX/X11/XI2MouseKeyboard.cs | 675 |
1 files changed, 675 insertions, 0 deletions
diff --git a/CrowX/X11/XI2MouseKeyboard.cs b/CrowX/X11/XI2MouseKeyboard.cs new file mode 100644 index 00000000..d099c869 --- /dev/null +++ b/CrowX/X11/XI2MouseKeyboard.cs @@ -0,0 +1,675 @@ + #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. + // +using Crow; + + + #endregion + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Threading; +using OpenTK.Input; + +namespace X11Sharp +{ + sealed class XI2MouseKeyboard : 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<XIMouse> devices = new List<XIMouse>(); // list of connected mice + Dictionary<int, int> rawids = new Dictionary<int, int>(); // maps hardware device ids to XIMouse ids + + List<XIKeyboard> keyboards = new List<XIKeyboard>(); // list of connected keybords + Dictionary<int, int> keyboard_ids = new Dictionary<int, int>(); // 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 + } +} + |