1# ---------------------------------------------------------------------------- 2# pyglet 3# Copyright (c) 2006-2008 Alex Holkner 4# Copyright (c) 2008-2020 pyglet contributors 5# All rights reserved. 6# 7# Redistribution and use in source and binary forms, with or without 8# modification, are permitted provided that the following conditions 9# are met: 10# 11# * Redistributions of source code must retain the above copyright 12# notice, this list of conditions and the following disclaimer. 13# * Redistributions in binary form must reproduce the above copyright 14# notice, this list of conditions and the following disclaimer in 15# the documentation and/or other materials provided with the 16# distribution. 17# * Neither the name of pyglet nor the names of its 18# contributors may be used to endorse or promote products 19# derived from this software without specific prior written 20# permission. 21# 22# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 25# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 26# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 27# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 28# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 29# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 30# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 31# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 32# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 33# POSSIBILITY OF SUCH DAMAGE. 34# ---------------------------------------------------------------------------- 35 36import ctypes 37import pyglet 38from pyglet.input.base import Device, DeviceException, DeviceOpenException 39from pyglet.input.base import Control, Button, RelativeAxis, AbsoluteAxis 40from pyglet.libs.x11 import xlib 41from pyglet.util import asstr 42 43try: 44 from pyglet.libs.x11 import xinput as xi 45 46 _have_xinput = True 47except: 48 _have_xinput = False 49 50 51def ptr_add(ptr, offset): 52 address = ctypes.addressof(ptr.contents) + offset 53 return ctypes.pointer(type(ptr.contents).from_address(address)) 54 55 56class DeviceResponder: 57 def _key_press(self, e): 58 pass 59 60 def _key_release(self, e): 61 pass 62 63 def _button_press(self, e): 64 pass 65 66 def _button_release(self, e): 67 pass 68 69 def _motion(self, e): 70 pass 71 72 def _proximity_in(self, e): 73 pass 74 75 def _proximity_out(self, e): 76 pass 77 78 79class XInputDevice(DeviceResponder, Device): 80 def __init__(self, display, device_info): 81 super(XInputDevice, self).__init__(display, asstr(device_info.name)) 82 83 self._device_id = device_info.id 84 self._device = None 85 86 # Read device info 87 self.buttons = [] 88 self.keys = [] 89 self.axes = [] 90 91 ptr = device_info.inputclassinfo 92 for i in range(device_info.num_classes): 93 cp = ctypes.cast(ptr, ctypes.POINTER(xi.XAnyClassInfo)) 94 cls_class = getattr(cp.contents, 'class') 95 96 if cls_class == xi.KeyClass: 97 cp = ctypes.cast(ptr, ctypes.POINTER(xi.XKeyInfo)) 98 self.min_keycode = cp.contents.min_keycode 99 num_keys = cp.contents.num_keys 100 for i in range(num_keys): 101 self.keys.append(Button('key%d' % i)) 102 103 elif cls_class == xi.ButtonClass: 104 cp = ctypes.cast(ptr, ctypes.POINTER(xi.XButtonInfo)) 105 num_buttons = cp.contents.num_buttons 106 # Pointer buttons start at index 1, with 0 as 'AnyButton' 107 for i in range(num_buttons + 1): 108 self.buttons.append(Button('button%d' % i)) 109 110 elif cls_class == xi.ValuatorClass: 111 cp = ctypes.cast(ptr, ctypes.POINTER(xi.XValuatorInfo)) 112 num_axes = cp.contents.num_axes 113 mode = cp.contents.mode 114 axes = ctypes.cast(cp.contents.axes, 115 ctypes.POINTER(xi.XAxisInfo)) 116 for i in range(num_axes): 117 axis = axes[i] 118 if mode == xi.Absolute: 119 self.axes.append(AbsoluteAxis('axis%d' % i, 120 min=axis.min_value, 121 max=axis.max_value)) 122 elif mode == xi.Relative: 123 self.axes.append(RelativeAxis('axis%d' % i)) 124 125 cls = cp.contents 126 ptr = ptr_add(ptr, cls.length) 127 128 self.controls = self.buttons + self.keys + self.axes 129 130 # Can't detect proximity class event without opening device. Just 131 # assume there is the possibility of a control if there are any axes. 132 if self.axes: 133 self.proximity_control = Button('proximity') 134 self.controls.append(self.proximity_control) 135 else: 136 self.proximity_control = None 137 138 def get_controls(self): 139 return self.controls 140 141 def open(self, window=None, exclusive=False): 142 # Checks for is_open and raises if already open. 143 # TODO allow opening on multiple windows. 144 super(XInputDevice, self).open(window, exclusive) 145 146 if window is None: 147 self.is_open = False 148 raise DeviceOpenException('XInput devices require a window') 149 150 if window.display._display != self.display._display: 151 self.is_open = False 152 raise DeviceOpenException('Window and device displays differ') 153 154 if exclusive: 155 self.is_open = False 156 raise DeviceOpenException('Cannot open XInput device exclusive') 157 158 self._device = xi.XOpenDevice(self.display._display, self._device_id) 159 if not self._device: 160 self.is_open = False 161 raise DeviceOpenException('Cannot open device') 162 163 self._install_events(window) 164 165 def close(self): 166 super(XInputDevice, self).close() 167 168 if not self._device: 169 return 170 171 # TODO: uninstall events 172 xi.XCloseDevice(self.display._display, self._device) 173 174 def _install_events(self, window): 175 dispatcher = XInputWindowEventDispatcher.get_dispatcher(window) 176 dispatcher.open_device(self._device_id, self._device, self) 177 178 # DeviceResponder interface 179 180 def _key_press(self, e): 181 self.keys[e.keycode - self.min_keycode].value = True 182 183 def _key_release(self, e): 184 self.keys[e.keycode - self.min_keycode].value = False 185 186 def _button_press(self, e): 187 self.buttons[e.button].value = True 188 189 def _button_release(self, e): 190 self.buttons[e.button].value = False 191 192 def _motion(self, e): 193 for i in range(e.axes_count): 194 self.axes[i].value = e.axis_data[i] 195 196 def _proximity_in(self, e): 197 if self.proximity_control: 198 self.proximity_control.value = True 199 200 def _proximity_out(self, e): 201 if self.proximity_control: 202 self.proximity_control.value = False 203 204 205class XInputWindowEventDispatcher: 206 def __init__(self, window): 207 self.window = window 208 self._responders = {} 209 210 @staticmethod 211 def get_dispatcher(window): 212 try: 213 dispatcher = window.__xinput_window_event_dispatcher 214 except AttributeError: 215 dispatcher = window.__xinput_window_event_dispatcher = \ 216 XInputWindowEventDispatcher(window) 217 return dispatcher 218 219 def set_responder(self, device_id, responder): 220 self._responders[device_id] = responder 221 222 def remove_responder(self, device_id): 223 del self._responders[device_id] 224 225 def open_device(self, device_id, device, responder): 226 self.set_responder(device_id, responder) 227 device = device.contents 228 if not device.num_classes: 229 return 230 231 # Bind matching extended window events to bound instance methods 232 # on this object. 233 # 234 # This is inspired by test.c of xinput package by Frederic 235 # Lepied available at x.org. 236 # 237 # In C, this stuff is normally handled by the macro DeviceKeyPress and 238 # friends. Since we don't have access to those macros here, we do it 239 # this way. 240 events = [] 241 242 def add(class_info, event, handler): 243 _type = class_info.event_type_base + event 244 _class = device_id << 8 | _type 245 events.append(_class) 246 self.window._event_handlers[_type] = handler 247 248 for i in range(device.num_classes): 249 class_info = device.classes[i] 250 if class_info.input_class == xi.KeyClass: 251 add(class_info, xi._deviceKeyPress, 252 self._event_xinput_key_press) 253 add(class_info, xi._deviceKeyRelease, 254 self._event_xinput_key_release) 255 256 elif class_info.input_class == xi.ButtonClass: 257 add(class_info, xi._deviceButtonPress, 258 self._event_xinput_button_press) 259 add(class_info, xi._deviceButtonRelease, 260 self._event_xinput_button_release) 261 262 elif class_info.input_class == xi.ValuatorClass: 263 add(class_info, xi._deviceMotionNotify, 264 self._event_xinput_motion) 265 266 elif class_info.input_class == xi.ProximityClass: 267 add(class_info, xi._proximityIn, 268 self._event_xinput_proximity_in) 269 add(class_info, xi._proximityOut, 270 self._event_xinput_proximity_out) 271 272 elif class_info.input_class == xi.FeedbackClass: 273 pass 274 275 elif class_info.input_class == xi.FocusClass: 276 pass 277 278 elif class_info.input_class == xi.OtherClass: 279 pass 280 281 array = (xi.XEventClass * len(events))(*events) 282 xi.XSelectExtensionEvent(self.window._x_display, 283 self.window._window, 284 array, 285 len(array)) 286 287 @pyglet.window.xlib.XlibEventHandler(0) 288 def _event_xinput_key_press(self, ev): 289 e = ctypes.cast(ctypes.byref(ev), 290 ctypes.POINTER(xi.XDeviceKeyEvent)).contents 291 292 device = self._responders.get(e.deviceid) 293 if device is not None: 294 device._key_press(e) 295 296 @pyglet.window.xlib.XlibEventHandler(0) 297 def _event_xinput_key_release(self, ev): 298 e = ctypes.cast(ctypes.byref(ev), 299 ctypes.POINTER(xi.XDeviceKeyEvent)).contents 300 301 device = self._responders.get(e.deviceid) 302 if device is not None: 303 device._key_release(e) 304 305 @pyglet.window.xlib.XlibEventHandler(0) 306 def _event_xinput_button_press(self, ev): 307 e = ctypes.cast(ctypes.byref(ev), 308 ctypes.POINTER(xi.XDeviceButtonEvent)).contents 309 310 device = self._responders.get(e.deviceid) 311 if device is not None: 312 device._button_press(e) 313 314 @pyglet.window.xlib.XlibEventHandler(0) 315 def _event_xinput_button_release(self, ev): 316 e = ctypes.cast(ctypes.byref(ev), 317 ctypes.POINTER(xi.XDeviceButtonEvent)).contents 318 319 device = self._responders.get(e.deviceid) 320 if device is not None: 321 device._button_release(e) 322 323 @pyglet.window.xlib.XlibEventHandler(0) 324 def _event_xinput_motion(self, ev): 325 e = ctypes.cast(ctypes.byref(ev), 326 ctypes.POINTER(xi.XDeviceMotionEvent)).contents 327 328 device = self._responders.get(e.deviceid) 329 if device is not None: 330 device._motion(e) 331 332 @pyglet.window.xlib.XlibEventHandler(0) 333 def _event_xinput_proximity_in(self, ev): 334 e = ctypes.cast(ctypes.byref(ev), 335 ctypes.POINTER(xi.XProximityNotifyEvent)).contents 336 337 device = self._responders.get(e.deviceid) 338 if device is not None: 339 device._proximity_in(e) 340 341 @pyglet.window.xlib.XlibEventHandler(-1) 342 def _event_xinput_proximity_out(self, ev): 343 e = ctypes.cast(ctypes.byref(ev), 344 ctypes.POINTER(xi.XProximityNotifyEvent)).contents 345 346 device = self._responders.get(e.deviceid) 347 if device is not None: 348 device._proximity_out(e) 349 350 351def _check_extension(display): 352 major_opcode = ctypes.c_int() 353 first_event = ctypes.c_int() 354 first_error = ctypes.c_int() 355 xlib.XQueryExtension(display._display, b'XInputExtension', 356 ctypes.byref(major_opcode), 357 ctypes.byref(first_event), 358 ctypes.byref(first_error)) 359 return bool(major_opcode.value) 360 361 362def get_devices(display=None): 363 if display is None: 364 display = pyglet.canvas.get_display() 365 366 if not _have_xinput or not _check_extension(display): 367 return [] 368 369 devices = [] 370 count = ctypes.c_int(0) 371 device_list = xi.XListInputDevices(display._display, count) 372 373 for i in range(count.value): 374 device_info = device_list[i] 375 devices.append(XInputDevice(display, device_info)) 376 377 xi.XFreeDeviceList(device_list) 378 379 return devices 380