Difficult-Rocket/pyglet/input/x11_xinput.py
2021-05-24 22:28:11 +08:00

380 lines
13 KiB
Python

# ----------------------------------------------------------------------------
# pyglet
# Copyright (c) 2006-2008 Alex Holkner
# Copyright (c) 2008-2021 pyglet contributors
# 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