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