Spaces:
Runtime error
Runtime error
import ctypes | |
from collections import defaultdict | |
import pyglet | |
from pyglet.input.base import DeviceOpenException | |
from pyglet.input.base import Tablet, TabletCanvas | |
from pyglet.libs.win32 import libwintab as wintab | |
from pyglet.util import debug_print | |
_debug = debug_print('debug_input') | |
lib = wintab.lib | |
def wtinfo(category, index, buffer): | |
size = lib.WTInfoW(category, index, None) | |
assert size <= ctypes.sizeof(buffer) | |
lib.WTInfoW(category, index, ctypes.byref(buffer)) | |
return buffer | |
def wtinfo_string(category, index): | |
size = lib.WTInfoW(category, index, None) | |
buffer = ctypes.create_unicode_buffer(size) | |
lib.WTInfoW(category, index, buffer) | |
return buffer.value | |
def wtinfo_uint(category, index): | |
buffer = wintab.UINT() | |
lib.WTInfoW(category, index, ctypes.byref(buffer)) | |
return buffer.value | |
def wtinfo_word(category, index): | |
buffer = wintab.WORD() | |
lib.WTInfoW(category, index, ctypes.byref(buffer)) | |
return buffer.value | |
def wtinfo_dword(category, index): | |
buffer = wintab.DWORD() | |
lib.WTInfoW(category, index, ctypes.byref(buffer)) | |
return buffer.value | |
def wtinfo_wtpkt(category, index): | |
buffer = wintab.WTPKT() | |
lib.WTInfoW(category, index, ctypes.byref(buffer)) | |
return buffer.value | |
def wtinfo_bool(category, index): | |
buffer = wintab.BOOL() | |
lib.WTInfoW(category, index, ctypes.byref(buffer)) | |
return bool(buffer.value) | |
class WintabTablet(Tablet): | |
def __init__(self, index): | |
self._device = wintab.WTI_DEVICES + index | |
self.name = wtinfo_string(self._device, wintab.DVC_NAME).strip() | |
self.id = wtinfo_string(self._device, wintab.DVC_PNPID) | |
hardware = wtinfo_uint(self._device, wintab.DVC_HARDWARE) | |
# phys_cursors = hardware & wintab.HWC_PHYSID_CURSORS | |
n_cursors = wtinfo_uint(self._device, wintab.DVC_NCSRTYPES) | |
first_cursor = wtinfo_uint(self._device, wintab.DVC_FIRSTCSR) | |
self.pressure_axis = wtinfo(self._device, wintab.DVC_NPRESSURE, wintab.AXIS()) | |
self.cursors = [] | |
self._cursor_map = {} | |
for i in range(n_cursors): | |
cursor = WintabTabletCursor(self, i + first_cursor) | |
if not cursor.bogus: | |
self.cursors.append(cursor) | |
self._cursor_map[i + first_cursor] = cursor | |
def open(self, window): | |
return WintabTabletCanvas(self, window) | |
class WintabTabletCanvas(TabletCanvas): | |
override_keys = False | |
def __init__(self, device, window, msg_base=wintab.WT_DEFBASE): | |
super(WintabTabletCanvas, self).__init__(window) | |
self.device = device | |
self.msg_base = msg_base | |
# Get the extension masks available. Only need to do this once. | |
global _extension_masks | |
if not _extension_masks: | |
_extension_masks = get_extension_masks() | |
# Just use system context, for similarity w/ os x and xinput. | |
# WTI_DEFCONTEXT detaches mouse from tablet, which is nice, but not | |
# possible on os x afiak. | |
self.context_info = context_info = wintab.LOGCONTEXT() | |
wtinfo(wintab.WTI_DEFSYSCTX, 0, context_info) | |
context_info.lcMsgBase = msg_base | |
context_info.lcOptions |= wintab.CXO_MESSAGES | |
# If you change this, change definition of PACKET also. | |
context_info.lcPktData = ( | |
wintab.PK_CHANGED | wintab.PK_CURSOR | wintab.PK_BUTTONS | | |
wintab.PK_X | wintab.PK_Y | wintab.PK_Z | | |
wintab.PK_NORMAL_PRESSURE | wintab.PK_TANGENT_PRESSURE | | |
wintab.PK_ORIENTATION) | _extension_masks | |
context_info.lcPktMode = 0 # All absolute (PACKETMODE) | |
self._context = lib.WTOpenW(window._hwnd, ctypes.byref(context_info), True) | |
if not self._context: | |
raise DeviceOpenException("Couldn't open tablet context") | |
window._event_handlers[msg_base + wintab.WT_PACKET] = self._event_wt_packet | |
window._event_handlers[msg_base + wintab.WT_PROXIMITY] = self._event_wt_proximity | |
if _extension_masks: | |
window._event_handlers[msg_base + wintab.WT_PACKETEXT] = self._event_wt_packetext | |
self._current_cursor = None | |
self._pressure_scale = device.pressure_axis.get_scale() | |
self._pressure_bias = device.pressure_axis.get_bias() | |
self.express_keys = defaultdict(lambda: defaultdict(bool)) # [control_id][location_id] | |
self.express_key_ct = 0 | |
self.touchrings = [] # Not currently implemented. | |
self.touchstrips = [] # Not currently implemented. | |
# Override test | |
for tablet_id in range(get_tablet_count()): | |
control_count = self.extension_get(wintab.WTX_EXPKEYS2, tablet_id, 0, 0, | |
wintab.TABLET_PROPERTY_CONTROLCOUNT) | |
self.express_key_ct = control_count | |
assert _debug(f"Controls Found: {control_count}") | |
if self.override_keys is True: | |
for control_id in range(control_count): | |
function_count = self.extension_get(wintab.WTX_EXPKEYS2, tablet_id, control_id, 0, | |
wintab.TABLET_PROPERTY_FUNCCOUNT) | |
for function_id in range(function_count): | |
self.extension_set(wintab.WTX_EXPKEYS2, tablet_id, control_id, function_id, | |
wintab.TABLET_PROPERTY_OVERRIDE, wintab.BOOL(True)) | |
def extension_get(self, extension, tablet_id, control_id, function_id, property_id, value_type=wintab.UINT): | |
prop = wintab.EXTPROPERTY() | |
prop.version = 0 | |
prop.tabletIndex = tablet_id | |
prop.controlIndex = control_id | |
prop.functionIndex = function_id | |
prop.propertyID = property_id | |
prop.reserved = 0 | |
prop.dataSize = ctypes.sizeof(value_type) | |
success = lib.WTExtGet(self._context, extension, ctypes.byref(prop)) | |
if success: | |
return ctypes.cast(prop.data, ctypes.POINTER(value_type)).contents.value | |
return 0 | |
def extension_set(self, extension, tablet_id, control_id, function_id, property_id, value): | |
prop = wintab.EXTPROPERTY() | |
prop.version = 0 | |
prop.tabletIndex = tablet_id | |
prop.controlIndex = control_id | |
prop.functionIndex = function_id | |
prop.propertyID = property_id | |
prop.reserved = 0 | |
prop.dataSize = ctypes.sizeof(value) | |
prop.data[0] = value.value | |
success = lib.WTExtSet(self._context, extension, ctypes.byref(prop)) | |
if success: | |
return True | |
return False | |
def close(self): | |
lib.WTClose(self._context) | |
self._context = None | |
del self.window._event_handlers[self.msg_base + wintab.WT_PACKET] | |
del self.window._event_handlers[self.msg_base + wintab.WT_PROXIMITY] | |
if _extension_masks: | |
del self.window._event_handlers[self.msg_base + wintab.WT_PACKETEXT] | |
def _set_current_cursor(self, cursor_type): | |
if self._current_cursor: | |
self.dispatch_event('on_leave', self._current_cursor) | |
self._current_cursor = self.device._cursor_map.get(cursor_type, None) | |
if self._current_cursor: | |
self.dispatch_event('on_enter', self._current_cursor) | |
def _event_wt_packet(self, msg, wParam, lParam): | |
if lParam != self._context: | |
return | |
packet = wintab.PACKET() | |
if lib.WTPacket(self._context, wParam, ctypes.byref(packet)) == 0: | |
return | |
if not packet.pkChanged: | |
return | |
window_x, window_y = self.window.get_location() # TODO cache on window | |
window_y = self.window.screen.height - window_y - self.window.height | |
x = packet.pkX - window_x | |
y = packet.pkY - window_y | |
pressure = (packet.pkNormalPressure + self._pressure_bias) * self._pressure_scale | |
if self._current_cursor is None: | |
self._set_current_cursor(packet.pkCursor) | |
self.dispatch_event('on_motion', self._current_cursor, x, y, pressure, 0., 0., packet.pkButtons) | |
def _event_wt_packetext(self, msg, wParam, lParam): | |
packet = wintab.PACKETEXT() | |
if lib.WTPacket(lParam, wParam, ctypes.byref(packet)) == 0: | |
return | |
# Proper context exists in the packet, not the lParam. | |
if packet.pkBase.nContext == self._context: | |
if packet.pkExpKeys.nControl < self.express_key_ct: | |
current_state = self.express_keys[packet.pkExpKeys.nControl][packet.pkExpKeys.nLocation] | |
new_state = bool(packet.pkExpKeys.nState) | |
if current_state != new_state: | |
event_type = "on_express_key_press" if new_state else "on_express_key_release" | |
self.express_keys[packet.pkExpKeys.nControl][packet.pkExpKeys.nLocation] = new_state | |
self.dispatch_event(event_type, packet.pkExpKeys.nControl, packet.pkExpKeys.nLocation) | |
def _event_wt_proximity(self, msg, wParam, lParam): | |
if wParam != self._context: | |
return | |
if not lParam & 0xffff0000: | |
# Not a hardware proximity event | |
return | |
if not lParam & 0xffff: | |
# Going out | |
self.dispatch_event('on_leave', self._current_cursor) | |
# If going in, proximity event will be generated by next event, which | |
# can actually grab a cursor id. | |
self._current_cursor = None | |
def on_express_key_press(self, control_id: int, location_id: int): | |
"""An event called when an ExpressKey is pressed down. | |
:Parameters: | |
`control_id` : int | |
Zero-based index number given to the assigned key by the driver. | |
The same control_id may exist in multiple locations, which the location_id is used to differentiate. | |
`location_id: int | |
Zero-based index indicating side of tablet where control id was found. | |
Some tablets may have clusters of ExpressKey's on various areas of the tablet. | |
(0 = left, 1 = right, 2 = top, 3 = bottom, 4 = transducer). | |
:event: | |
""" | |
def on_express_key_release(self, control_id: int, location_id: int): | |
"""An event called when an ExpressKey is released. | |
:Parameters: | |
`control_id` : int | |
Zero-based index number given to the assigned key by the driver. | |
The same control_id may exist in multiple locations, which the location_id is used to differentiate. | |
`location_id: int | |
Zero-based index indicating side of tablet where control id was found. | |
Some tablets may have clusters of ExpressKey's on various areas of the tablet. | |
(0 = left, 1 = right, 2 = top, 3 = bottom, 4 = transducer). | |
:event: | |
""" | |
WintabTabletCanvas.register_event_type('on_express_key_press') | |
WintabTabletCanvas.register_event_type('on_express_key_release') | |
class WintabTabletCursor: | |
def __init__(self, device, index): | |
self.device = device | |
self._cursor = wintab.WTI_CURSORS + index | |
self.name = wtinfo_string(self._cursor, wintab.CSR_NAME).strip() | |
self.active = wtinfo_bool(self._cursor, wintab.CSR_ACTIVE) | |
pktdata = wtinfo_wtpkt(self._cursor, wintab.CSR_PKTDATA) | |
# A whole bunch of cursors are reported by the driver, but most of | |
# them are hogwash. Make sure a cursor has at least X and Y data | |
# before adding it to the device. | |
self.bogus = not (pktdata & wintab.PK_X and pktdata & wintab.PK_Y) | |
if self.bogus: | |
return | |
self.id = (wtinfo_dword(self._cursor, wintab.CSR_TYPE) << 32) | \ | |
wtinfo_dword(self._cursor, wintab.CSR_PHYSID) | |
def __repr__(self): | |
return 'WintabCursor(%r)' % self.name | |
def get_spec_version(): | |
spec_version = wtinfo_word(wintab.WTI_INTERFACE, wintab.IFC_SPECVERSION) | |
return spec_version | |
def get_interface_name(): | |
interface_name = wtinfo_string(wintab.WTI_INTERFACE, wintab.IFC_WINTABID) | |
return interface_name | |
def get_implementation_version(): | |
impl_version = wtinfo_word(wintab.WTI_INTERFACE, wintab.IFC_IMPLVERSION) | |
return impl_version | |
def extension_index(ext): | |
"""Check if a particular extension exists within the driver.""" | |
exists = True | |
i = 0 | |
index = 0xFFFFFFFF | |
while exists: | |
tag = wintab.UINT() | |
exists = lib.WTInfoW(wintab.WTI_EXTENSIONS + i, wintab.EXT_TAG, ctypes.byref(tag)) | |
if tag.value == ext: | |
index = i | |
break | |
i += 1 | |
if index != 0xFFFFFFFF: | |
return index | |
return None | |
def get_extension_masks(): | |
"""Determine which extension support is available by getting the masks.""" | |
masks = 0 | |
tr_idx = extension_index(wintab.WTX_TOUCHRING) | |
if tr_idx is not None: | |
assert _debug("Touchring support found") | |
masks |= wtinfo_uint(wintab.WTI_EXTENSIONS + tr_idx, wintab.EXT_MASK) | |
else: | |
assert _debug("Touchring extension not found.") | |
ts_idx = extension_index(wintab.WTX_TOUCHSTRIP) | |
if ts_idx is not None: | |
assert _debug("Touchstrip support found.") | |
masks |= wtinfo_uint(wintab.WTI_EXTENSIONS + ts_idx, wintab.EXT_MASK) | |
else: | |
assert _debug("Touchstrip extension not found.") | |
expkeys_idx = extension_index(wintab.WTX_EXPKEYS2) | |
if expkeys_idx is not None: | |
assert _debug("ExpressKey support found.") | |
masks |= wtinfo_uint(wintab.WTI_EXTENSIONS + expkeys_idx, wintab.EXT_MASK) | |
else: | |
assert _debug("ExpressKey extension not found.") | |
return masks | |
def get_tablet_count(): | |
"""Return just the number of current devices.""" | |
spec_version = get_spec_version() | |
assert _debug(f"Wintab Version: {spec_version}") | |
if spec_version < 0x101: | |
return 0 | |
n_devices = wtinfo_uint(wintab.WTI_INTERFACE, wintab.IFC_NDEVICES) | |
return n_devices | |
_extension_masks = None | |
def get_tablets(display=None): | |
# Require spec version 1.1 or greater | |
n_devices = get_tablet_count() | |
if not n_devices: | |
return [] | |
devices = [WintabTablet(i) for i in range(n_devices)] | |
return devices | |