2021-04-16 23:21:06 +08:00
|
|
|
# ----------------------------------------------------------------------------
|
|
|
|
# pyglet
|
|
|
|
# Copyright (c) 2006-2008 Alex Holkner
|
2021-04-17 01:14:38 +08:00
|
|
|
# Copyright (c) 2008-2021 pyglet contributors
|
2021-04-16 23:21:06 +08:00
|
|
|
# All rights reserved.
|
|
|
|
#
|
|
|
|
# Redistribution and use in source and binary forms, with or without
|
|
|
|
# modification, are permitted provided that the following conditions
|
|
|
|
# are met:
|
|
|
|
#
|
|
|
|
# * Redistributions of source code must retain the above copyright
|
|
|
|
# notice, this list of conditions and the following disclaimer.
|
|
|
|
# * Redistributions in binary form must reproduce the above copyright
|
|
|
|
# notice, this list of conditions and the following disclaimer in
|
|
|
|
# the documentation and/or other materials provided with the
|
|
|
|
# distribution.
|
|
|
|
# * Neither the name of pyglet nor the names of its
|
|
|
|
# contributors may be used to endorse or promote products
|
|
|
|
# derived from this software without specific prior written
|
|
|
|
# permission.
|
|
|
|
#
|
|
|
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
|
|
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
|
|
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
|
|
|
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
|
|
|
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
|
|
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
|
|
|
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
|
|
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
|
|
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
|
|
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
|
|
|
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
|
|
# POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
# ----------------------------------------------------------------------------
|
|
|
|
|
|
|
|
import ctypes
|
|
|
|
import pyglet
|
|
|
|
from pyglet.input.base import Device, DeviceException, DeviceOpenException
|
|
|
|
from pyglet.input.base import Control, Button, RelativeAxis, AbsoluteAxis
|
|
|
|
from pyglet.libs.x11 import xlib
|
|
|
|
from pyglet.util import asstr
|
|
|
|
|
|
|
|
try:
|
|
|
|
from pyglet.libs.x11 import xinput as xi
|
|
|
|
|
|
|
|
_have_xinput = True
|
|
|
|
except:
|
|
|
|
_have_xinput = False
|
|
|
|
|
|
|
|
|
|
|
|
def ptr_add(ptr, offset):
|
|
|
|
address = ctypes.addressof(ptr.contents) + offset
|
|
|
|
return ctypes.pointer(type(ptr.contents).from_address(address))
|
|
|
|
|
|
|
|
|
|
|
|
class DeviceResponder:
|
|
|
|
def _key_press(self, e):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def _key_release(self, e):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def _button_press(self, e):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def _button_release(self, e):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def _motion(self, e):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def _proximity_in(self, e):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def _proximity_out(self, e):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class XInputDevice(DeviceResponder, Device):
|
|
|
|
def __init__(self, display, device_info):
|
|
|
|
super(XInputDevice, self).__init__(display, asstr(device_info.name))
|
|
|
|
|
|
|
|
self._device_id = device_info.id
|
|
|
|
self._device = None
|
|
|
|
|
|
|
|
# Read device info
|
|
|
|
self.buttons = []
|
|
|
|
self.keys = []
|
|
|
|
self.axes = []
|
|
|
|
|
|
|
|
ptr = device_info.inputclassinfo
|
|
|
|
for i in range(device_info.num_classes):
|
|
|
|
cp = ctypes.cast(ptr, ctypes.POINTER(xi.XAnyClassInfo))
|
|
|
|
cls_class = getattr(cp.contents, 'class')
|
|
|
|
|
|
|
|
if cls_class == xi.KeyClass:
|
|
|
|
cp = ctypes.cast(ptr, ctypes.POINTER(xi.XKeyInfo))
|
|
|
|
self.min_keycode = cp.contents.min_keycode
|
|
|
|
num_keys = cp.contents.num_keys
|
|
|
|
for i in range(num_keys):
|
|
|
|
self.keys.append(Button('key%d' % i))
|
|
|
|
|
|
|
|
elif cls_class == xi.ButtonClass:
|
|
|
|
cp = ctypes.cast(ptr, ctypes.POINTER(xi.XButtonInfo))
|
|
|
|
num_buttons = cp.contents.num_buttons
|
|
|
|
# Pointer buttons start at index 1, with 0 as 'AnyButton'
|
|
|
|
for i in range(num_buttons + 1):
|
|
|
|
self.buttons.append(Button('button%d' % i))
|
|
|
|
|
|
|
|
elif cls_class == xi.ValuatorClass:
|
|
|
|
cp = ctypes.cast(ptr, ctypes.POINTER(xi.XValuatorInfo))
|
|
|
|
num_axes = cp.contents.num_axes
|
|
|
|
mode = cp.contents.mode
|
|
|
|
axes = ctypes.cast(cp.contents.axes,
|
|
|
|
ctypes.POINTER(xi.XAxisInfo))
|
|
|
|
for i in range(num_axes):
|
|
|
|
axis = axes[i]
|
|
|
|
if mode == xi.Absolute:
|
|
|
|
self.axes.append(AbsoluteAxis('axis%d' % i,
|
|
|
|
min=axis.min_value,
|
|
|
|
max=axis.max_value))
|
|
|
|
elif mode == xi.Relative:
|
|
|
|
self.axes.append(RelativeAxis('axis%d' % i))
|
|
|
|
|
|
|
|
cls = cp.contents
|
|
|
|
ptr = ptr_add(ptr, cls.length)
|
|
|
|
|
|
|
|
self.controls = self.buttons + self.keys + self.axes
|
|
|
|
|
|
|
|
# Can't detect proximity class event without opening device. Just
|
|
|
|
# assume there is the possibility of a control if there are any axes.
|
|
|
|
if self.axes:
|
|
|
|
self.proximity_control = Button('proximity')
|
|
|
|
self.controls.append(self.proximity_control)
|
|
|
|
else:
|
|
|
|
self.proximity_control = None
|
|
|
|
|
|
|
|
def get_controls(self):
|
|
|
|
return self.controls
|
|
|
|
|
|
|
|
def open(self, window=None, exclusive=False):
|
|
|
|
# Checks for is_open and raises if already open.
|
|
|
|
# TODO allow opening on multiple windows.
|
|
|
|
super(XInputDevice, self).open(window, exclusive)
|
|
|
|
|
|
|
|
if window is None:
|
|
|
|
self.is_open = False
|
|
|
|
raise DeviceOpenException('XInput devices require a window')
|
|
|
|
|
|
|
|
if window.display._display != self.display._display:
|
|
|
|
self.is_open = False
|
|
|
|
raise DeviceOpenException('Window and device displays differ')
|
|
|
|
|
|
|
|
if exclusive:
|
|
|
|
self.is_open = False
|
|
|
|
raise DeviceOpenException('Cannot open XInput device exclusive')
|
|
|
|
|
|
|
|
self._device = xi.XOpenDevice(self.display._display, self._device_id)
|
|
|
|
if not self._device:
|
|
|
|
self.is_open = False
|
|
|
|
raise DeviceOpenException('Cannot open device')
|
|
|
|
|
|
|
|
self._install_events(window)
|
|
|
|
|
|
|
|
def close(self):
|
|
|
|
super(XInputDevice, self).close()
|
|
|
|
|
|
|
|
if not self._device:
|
|
|
|
return
|
|
|
|
|
|
|
|
# TODO: uninstall events
|
|
|
|
xi.XCloseDevice(self.display._display, self._device)
|
|
|
|
|
|
|
|
def _install_events(self, window):
|
|
|
|
dispatcher = XInputWindowEventDispatcher.get_dispatcher(window)
|
|
|
|
dispatcher.open_device(self._device_id, self._device, self)
|
|
|
|
|
|
|
|
# DeviceResponder interface
|
|
|
|
|
|
|
|
def _key_press(self, e):
|
|
|
|
self.keys[e.keycode - self.min_keycode].value = True
|
|
|
|
|
|
|
|
def _key_release(self, e):
|
|
|
|
self.keys[e.keycode - self.min_keycode].value = False
|
|
|
|
|
|
|
|
def _button_press(self, e):
|
|
|
|
self.buttons[e.button].value = True
|
|
|
|
|
|
|
|
def _button_release(self, e):
|
|
|
|
self.buttons[e.button].value = False
|
|
|
|
|
|
|
|
def _motion(self, e):
|
|
|
|
for i in range(e.axes_count):
|
|
|
|
self.axes[i].value = e.axis_data[i]
|
|
|
|
|
|
|
|
def _proximity_in(self, e):
|
|
|
|
if self.proximity_control:
|
|
|
|
self.proximity_control.value = True
|
|
|
|
|
|
|
|
def _proximity_out(self, e):
|
|
|
|
if self.proximity_control:
|
|
|
|
self.proximity_control.value = False
|
|
|
|
|
|
|
|
|
|
|
|
class XInputWindowEventDispatcher:
|
|
|
|
def __init__(self, window):
|
|
|
|
self.window = window
|
|
|
|
self._responders = {}
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def get_dispatcher(window):
|
|
|
|
try:
|
|
|
|
dispatcher = window.__xinput_window_event_dispatcher
|
|
|
|
except AttributeError:
|
|
|
|
dispatcher = window.__xinput_window_event_dispatcher = \
|
|
|
|
XInputWindowEventDispatcher(window)
|
|
|
|
return dispatcher
|
|
|
|
|
|
|
|
def set_responder(self, device_id, responder):
|
|
|
|
self._responders[device_id] = responder
|
|
|
|
|
|
|
|
def remove_responder(self, device_id):
|
|
|
|
del self._responders[device_id]
|
|
|
|
|
|
|
|
def open_device(self, device_id, device, responder):
|
|
|
|
self.set_responder(device_id, responder)
|
|
|
|
device = device.contents
|
|
|
|
if not device.num_classes:
|
|
|
|
return
|
|
|
|
|
|
|
|
# Bind matching extended window events to bound instance methods
|
|
|
|
# on this object.
|
|
|
|
#
|
|
|
|
# This is inspired by test.c of xinput package by Frederic
|
|
|
|
# Lepied available at x.org.
|
|
|
|
#
|
|
|
|
# In C, this stuff is normally handled by the macro DeviceKeyPress and
|
|
|
|
# friends. Since we don't have access to those macros here, we do it
|
|
|
|
# this way.
|
|
|
|
events = []
|
|
|
|
|
|
|
|
def add(class_info, event, handler):
|
|
|
|
_type = class_info.event_type_base + event
|
|
|
|
_class = device_id << 8 | _type
|
|
|
|
events.append(_class)
|
|
|
|
self.window._event_handlers[_type] = handler
|
|
|
|
|
|
|
|
for i in range(device.num_classes):
|
|
|
|
class_info = device.classes[i]
|
|
|
|
if class_info.input_class == xi.KeyClass:
|
|
|
|
add(class_info, xi._deviceKeyPress,
|
|
|
|
self._event_xinput_key_press)
|
|
|
|
add(class_info, xi._deviceKeyRelease,
|
|
|
|
self._event_xinput_key_release)
|
|
|
|
|
|
|
|
elif class_info.input_class == xi.ButtonClass:
|
|
|
|
add(class_info, xi._deviceButtonPress,
|
|
|
|
self._event_xinput_button_press)
|
|
|
|
add(class_info, xi._deviceButtonRelease,
|
|
|
|
self._event_xinput_button_release)
|
|
|
|
|
|
|
|
elif class_info.input_class == xi.ValuatorClass:
|
|
|
|
add(class_info, xi._deviceMotionNotify,
|
|
|
|
self._event_xinput_motion)
|
|
|
|
|
|
|
|
elif class_info.input_class == xi.ProximityClass:
|
|
|
|
add(class_info, xi._proximityIn,
|
|
|
|
self._event_xinput_proximity_in)
|
|
|
|
add(class_info, xi._proximityOut,
|
|
|
|
self._event_xinput_proximity_out)
|
|
|
|
|
|
|
|
elif class_info.input_class == xi.FeedbackClass:
|
|
|
|
pass
|
|
|
|
|
|
|
|
elif class_info.input_class == xi.FocusClass:
|
|
|
|
pass
|
|
|
|
|
|
|
|
elif class_info.input_class == xi.OtherClass:
|
|
|
|
pass
|
|
|
|
|
|
|
|
array = (xi.XEventClass * len(events))(*events)
|
|
|
|
xi.XSelectExtensionEvent(self.window._x_display,
|
|
|
|
self.window._window,
|
|
|
|
array,
|
|
|
|
len(array))
|
|
|
|
|
|
|
|
@pyglet.window.xlib.XlibEventHandler(0)
|
|
|
|
def _event_xinput_key_press(self, ev):
|
|
|
|
e = ctypes.cast(ctypes.byref(ev),
|
|
|
|
ctypes.POINTER(xi.XDeviceKeyEvent)).contents
|
|
|
|
|
|
|
|
device = self._responders.get(e.deviceid)
|
|
|
|
if device is not None:
|
|
|
|
device._key_press(e)
|
|
|
|
|
|
|
|
@pyglet.window.xlib.XlibEventHandler(0)
|
|
|
|
def _event_xinput_key_release(self, ev):
|
|
|
|
e = ctypes.cast(ctypes.byref(ev),
|
|
|
|
ctypes.POINTER(xi.XDeviceKeyEvent)).contents
|
|
|
|
|
|
|
|
device = self._responders.get(e.deviceid)
|
|
|
|
if device is not None:
|
|
|
|
device._key_release(e)
|
|
|
|
|
|
|
|
@pyglet.window.xlib.XlibEventHandler(0)
|
|
|
|
def _event_xinput_button_press(self, ev):
|
|
|
|
e = ctypes.cast(ctypes.byref(ev),
|
|
|
|
ctypes.POINTER(xi.XDeviceButtonEvent)).contents
|
|
|
|
|
|
|
|
device = self._responders.get(e.deviceid)
|
|
|
|
if device is not None:
|
|
|
|
device._button_press(e)
|
|
|
|
|
|
|
|
@pyglet.window.xlib.XlibEventHandler(0)
|
|
|
|
def _event_xinput_button_release(self, ev):
|
|
|
|
e = ctypes.cast(ctypes.byref(ev),
|
|
|
|
ctypes.POINTER(xi.XDeviceButtonEvent)).contents
|
|
|
|
|
|
|
|
device = self._responders.get(e.deviceid)
|
|
|
|
if device is not None:
|
|
|
|
device._button_release(e)
|
|
|
|
|
|
|
|
@pyglet.window.xlib.XlibEventHandler(0)
|
|
|
|
def _event_xinput_motion(self, ev):
|
|
|
|
e = ctypes.cast(ctypes.byref(ev),
|
|
|
|
ctypes.POINTER(xi.XDeviceMotionEvent)).contents
|
|
|
|
|
|
|
|
device = self._responders.get(e.deviceid)
|
|
|
|
if device is not None:
|
|
|
|
device._motion(e)
|
|
|
|
|
|
|
|
@pyglet.window.xlib.XlibEventHandler(0)
|
|
|
|
def _event_xinput_proximity_in(self, ev):
|
|
|
|
e = ctypes.cast(ctypes.byref(ev),
|
|
|
|
ctypes.POINTER(xi.XProximityNotifyEvent)).contents
|
|
|
|
|
|
|
|
device = self._responders.get(e.deviceid)
|
|
|
|
if device is not None:
|
|
|
|
device._proximity_in(e)
|
|
|
|
|
|
|
|
@pyglet.window.xlib.XlibEventHandler(-1)
|
|
|
|
def _event_xinput_proximity_out(self, ev):
|
|
|
|
e = ctypes.cast(ctypes.byref(ev),
|
|
|
|
ctypes.POINTER(xi.XProximityNotifyEvent)).contents
|
|
|
|
|
|
|
|
device = self._responders.get(e.deviceid)
|
|
|
|
if device is not None:
|
|
|
|
device._proximity_out(e)
|
|
|
|
|
|
|
|
|
|
|
|
def _check_extension(display):
|
|
|
|
major_opcode = ctypes.c_int()
|
|
|
|
first_event = ctypes.c_int()
|
|
|
|
first_error = ctypes.c_int()
|
|
|
|
xlib.XQueryExtension(display._display, b'XInputExtension',
|
|
|
|
ctypes.byref(major_opcode),
|
|
|
|
ctypes.byref(first_event),
|
|
|
|
ctypes.byref(first_error))
|
|
|
|
return bool(major_opcode.value)
|
|
|
|
|
|
|
|
|
|
|
|
def get_devices(display=None):
|
|
|
|
if display is None:
|
|
|
|
display = pyglet.canvas.get_display()
|
|
|
|
|
|
|
|
if not _have_xinput or not _check_extension(display):
|
|
|
|
return []
|
|
|
|
|
|
|
|
devices = []
|
|
|
|
count = ctypes.c_int(0)
|
|
|
|
device_list = xi.XListInputDevices(display._display, count)
|
|
|
|
|
|
|
|
for i in range(count.value):
|
|
|
|
device_info = device_list[i]
|
|
|
|
devices.append(XInputDevice(display, device_info))
|
|
|
|
|
|
|
|
xi.XFreeDeviceList(device_list)
|
|
|
|
|
|
|
|
return devices
|