2021-04-16 23:21:06 +08:00
|
|
|
# ----------------------------------------------------------------------------
|
|
|
|
# pyglet
|
|
|
|
# Copyright (c) 2006-2008 Alex Holkner
|
2022-04-30 13:56:57 +08:00
|
|
|
# Copyright (c) 2008-2022 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
|
2022-07-16 20:20:23 +08:00
|
|
|
from collections import defaultdict
|
2021-04-16 23:21:06 +08:00
|
|
|
import pyglet
|
|
|
|
from pyglet.input.base import DeviceOpenException
|
2021-09-23 06:34:23 +08:00
|
|
|
from pyglet.input.base import Tablet, TabletCanvas
|
2022-06-20 21:08:57 +08:00
|
|
|
from pyglet.libs.win32 import libwintab as wintab
|
2022-07-16 20:20:23 +08:00
|
|
|
from pyglet.util import debug_print
|
|
|
|
|
|
|
|
_debug = debug_print('debug_input')
|
2021-04-16 23:21:06 +08:00
|
|
|
|
|
|
|
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):
|
2022-07-16 20:20:23 +08:00
|
|
|
override_keys = False
|
|
|
|
|
2021-04-16 23:21:06 +08:00
|
|
|
def __init__(self, device, window, msg_base=wintab.WT_DEFBASE):
|
|
|
|
super(WintabTabletCanvas, self).__init__(window)
|
|
|
|
|
|
|
|
self.device = device
|
|
|
|
self.msg_base = msg_base
|
|
|
|
|
2022-07-16 20:20:23 +08:00
|
|
|
# Get the extension masks available. Only need to do this once.
|
|
|
|
global _extension_masks
|
|
|
|
if not _extension_masks:
|
|
|
|
_extension_masks = get_extension_masks()
|
|
|
|
|
2021-04-16 23:21:06 +08:00
|
|
|
# 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 = (
|
2022-07-16 20:20:23 +08:00
|
|
|
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)
|
2021-04-16 23:21:06 +08:00
|
|
|
|
|
|
|
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
|
|
|
|
|
2022-07-16 20:20:23 +08:00
|
|
|
if _extension_masks:
|
|
|
|
window._event_handlers[msg_base + wintab.WT_PACKETEXT] = self._event_wt_packetext
|
|
|
|
|
2021-04-16 23:21:06 +08:00
|
|
|
self._current_cursor = None
|
|
|
|
self._pressure_scale = device.pressure_axis.get_scale()
|
|
|
|
self._pressure_bias = device.pressure_axis.get_bias()
|
|
|
|
|
2022-07-16 20:20:23 +08:00
|
|
|
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
|
|
|
|
|
2021-04-16 23:21:06 +08:00
|
|
|
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]
|
|
|
|
|
2022-07-16 20:20:23 +08:00
|
|
|
if _extension_masks:
|
|
|
|
del self.window._event_handlers[self.msg_base + wintab.WT_PACKETEXT]
|
|
|
|
|
2021-04-16 23:21:06 +08:00
|
|
|
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)
|
|
|
|
|
|
|
|
@pyglet.window.win32.Win32EventHandler(0)
|
|
|
|
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)
|
|
|
|
|
2022-07-16 20:20:23 +08:00
|
|
|
self.dispatch_event('on_motion', self._current_cursor, x, y, pressure, 0., 0., packet.pkButtons)
|
|
|
|
|
|
|
|
@pyglet.window.win32.Win32EventHandler(0)
|
|
|
|
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)
|
2021-04-16 23:21:06 +08:00
|
|
|
|
|
|
|
@pyglet.window.win32.Win32EventHandler(0)
|
|
|
|
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
|
|
|
|
|
2022-07-16 20:20:23 +08:00
|
|
|
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')
|
|
|
|
|
2021-04-16 23:21:06 +08:00
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
2022-07-16 20:20:23 +08:00
|
|
|
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
|
|
|
|
|
|
|
|
|
2021-04-16 23:21:06 +08:00
|
|
|
def get_tablets(display=None):
|
|
|
|
# Require spec version 1.1 or greater
|
2022-07-16 20:20:23 +08:00
|
|
|
n_devices = get_tablet_count()
|
|
|
|
if not n_devices:
|
2021-04-16 23:21:06 +08:00
|
|
|
return []
|
|
|
|
|
|
|
|
devices = [WintabTablet(i) for i in range(n_devices)]
|
|
|
|
return devices
|