first commit

This commit is contained in:
Yura 2024-09-15 15:12:16 +03:00
commit 417e54da96
5696 changed files with 900003 additions and 0 deletions

View file

@ -0,0 +1,68 @@
# pylint: disable=W0611
'''
Providers
=========
'''
import os
from kivy.utils import platform as core_platform
from kivy.logger import Logger
from kivy.setupconfig import USE_SDL2
import kivy.input.providers.tuio
import kivy.input.providers.mouse
platform = core_platform
if platform == 'win' or 'KIVY_DOC' in os.environ:
try:
import kivy.input.providers.wm_touch
import kivy.input.providers.wm_pen
except:
err = 'Input: WM_Touch/WM_Pen not supported by your version of Windows'
Logger.warning(err)
if platform == 'macosx' or 'KIVY_DOC' in os.environ:
try:
import kivy.input.providers.mactouch
except:
err = 'Input: MacMultitouchSupport is not supported by your system'
Logger.exception(err)
if platform == 'linux' or 'KIVY_DOC' in os.environ:
try:
import kivy.input.providers.probesysfs
except:
err = 'Input: ProbeSysfs is not supported by your version of linux'
Logger.exception(err)
try:
import kivy.input.providers.mtdev
except:
err = 'Input: MTDev is not supported by your version of linux'
Logger.exception(err)
try:
import kivy.input.providers.hidinput
except:
err = 'Input: HIDInput is not supported by your version of linux'
Logger.exception(err)
try:
import kivy.input.providers.linuxwacom
except:
err = 'Input: LinuxWacom is not supported by your version of linux'
Logger.exception(err)
if (platform == 'android' and not USE_SDL2) or 'KIVY_DOC' in os.environ:
try:
import kivy.input.providers.androidjoystick
except:
err = 'Input: AndroidJoystick is not supported by your version ' \
'of linux'
Logger.exception(err)
try:
import kivy.input.providers.leapfinger # NOQA
except:
err = 'Input: LeapFinger is not available on your system'
Logger.exception(err)

View file

@ -0,0 +1,117 @@
# pylint: disable=W0611
'''
Android Joystick Input Provider
===============================
This module is based on the PyGame JoyStick Input Provider. For more
information, please refer to
`<http://www.pygame.org/docs/ref/joystick.html>`_
'''
__all__ = ('AndroidMotionEventProvider', )
import os
try:
import android # NOQA
except ImportError:
if 'KIVY_DOC' not in os.environ:
raise Exception('android lib not found.')
from kivy.logger import Logger
from kivy.input.provider import MotionEventProvider
from kivy.input.factory import MotionEventFactory
from kivy.input.shape import ShapeRect
from kivy.input.motionevent import MotionEvent
if 'KIVY_DOC' not in os.environ:
import pygame.joystick
class AndroidMotionEvent(MotionEvent):
def __init__(self, *args, **kwargs):
kwargs.setdefault('is_touch', True)
kwargs.setdefault('type_id', 'touch')
super().__init__(*args, **kwargs)
self.profile = ['pos', 'pressure', 'shape']
def depack(self, args):
self.sx, self.sy, self.pressure, radius = args
self.shape = ShapeRect()
self.shape.width = radius
self.shape.height = radius
super().depack(args)
class AndroidMotionEventProvider(MotionEventProvider):
def __init__(self, device, args):
super(AndroidMotionEventProvider, self).__init__(device, args)
self.joysticks = []
self.touches = {}
self.uid = 0
self.window = None
def create_joystick(self, index):
Logger.info('Android: create joystick <%d>' % index)
js = pygame.joystick.Joystick(index)
js.init()
if js.get_numbuttons() == 0:
Logger.info('Android: discard joystick <%d> cause no button' %
index)
return
self.joysticks.append(js)
def start(self):
pygame.joystick.init()
Logger.info('Android: found %d joystick' % pygame.joystick.get_count())
for i in range(pygame.joystick.get_count()):
self.create_joystick(i)
def stop(self):
self.joysticks = []
def update(self, dispatch_fn):
if not self.window:
from kivy.core.window import Window
self.window = Window
w, h = self.window.system_size
touches = self.touches
for joy in self.joysticks:
jid = joy.get_id()
pressed = joy.get_button(0)
if pressed or jid in touches:
x = joy.get_axis(0) * 32768. / w
y = 1. - (joy.get_axis(1) * 32768. / h)
# python for android do * 1000.
pressure = joy.get_axis(2) / 1000.
radius = joy.get_axis(3) / 1000.
# new touch ?
if pressed and jid not in touches:
self.uid += 1
touch = AndroidMotionEvent(self.device, self.uid,
[x, y, pressure, radius])
touches[jid] = touch
dispatch_fn('begin', touch)
# update touch
elif pressed:
touch = touches[jid]
# avoid same touch position
if (touch.sx == x and touch.sy == y and
touch.pressure == pressure):
continue
touch.move([x, y, pressure, radius])
dispatch_fn('update', touch)
# disappear
elif not pressed and jid in touches:
touch = touches[jid]
touch.move([x, y, pressure, radius])
touch.update_time_end()
dispatch_fn('end', touch)
touches.pop(jid)
MotionEventFactory.register('android', AndroidMotionEventProvider)

View file

@ -0,0 +1,778 @@
# coding utf-8
'''
Native support for HID input from the linux kernel
==================================================
Support starts from 2.6.32-ubuntu, or 2.6.34.
To configure HIDInput, add this to your configuration::
[input]
# devicename = hidinput,/dev/input/eventXX
# example with Stantum MTP4.3" screen
stantum = hidinput,/dev/input/event2
.. note::
You must have read access to the input event.
You can use a custom range for the X, Y and pressure values.
For some drivers, the range reported is invalid.
To fix that, you can add these options to the argument line:
* invert_x : 1 to invert X axis
* invert_y : 1 to invert Y axis
* min_position_x : X relative minimum
* max_position_x : X relative maximum
* min_position_y : Y relative minimum
* max_position_y : Y relative maximum
* min_abs_x : X absolute minimum
* min_abs_y : Y absolute minimum
* max_abs_x : X absolute maximum
* max_abs_y : Y absolute maximum
* min_pressure : pressure minimum
* max_pressure : pressure maximum
* rotation : rotate the input coordinate (0, 90, 180, 270)
For example, on the Asus T101M, the touchscreen reports a range from 0-4095 for
the X and Y values, but the real values are in a range from 0-32768. To correct
this, you can add the following to the configuration::
[input]
t101m = hidinput,/dev/input/event7,max_position_x=32768,\
max_position_y=32768
.. versionadded:: 1.9.1
`rotation` configuration token added.
'''
import os
from kivy.input.motionevent import MotionEvent
from kivy.input.shape import ShapeRect
__all__ = ('HIDInputMotionEventProvider', 'HIDMotionEvent')
# late imports
Window = None
Keyboard = None
class HIDMotionEvent(MotionEvent):
def __init__(self, *args, **kwargs):
kwargs.setdefault('is_touch', True)
kwargs.setdefault('type_id', 'touch')
super().__init__(*args, **kwargs)
def depack(self, args):
self.sx = args['x']
self.sy = args['y']
self.profile = ['pos']
if 'size_w' in args and 'size_h' in args:
self.shape = ShapeRect()
self.shape.width = args['size_w']
self.shape.height = args['size_h']
self.profile.append('shape')
if 'pressure' in args:
self.pressure = args['pressure']
self.profile.append('pressure')
if 'button' in args:
self.button = args['button']
self.profile.append('button')
super().depack(args)
def __str__(self):
return '<HIDMotionEvent id=%d pos=(%f, %f) device=%s>' \
% (self.id, self.sx, self.sy, self.device)
if 'KIVY_DOC' in os.environ:
# documentation hack
HIDInputMotionEventProvider = None
else:
import threading
import collections
import struct
import fcntl
from kivy.input.provider import MotionEventProvider
from kivy.input.factory import MotionEventFactory
from kivy.logger import Logger
#
# This part is taken from linux-source-2.6.32/include/linux/input.h
#
# Event types
EV_SYN = 0x00
EV_KEY = 0x01
EV_REL = 0x02
EV_ABS = 0x03
EV_MSC = 0x04
EV_SW = 0x05
EV_LED = 0x11
EV_SND = 0x12
EV_REP = 0x14
EV_FF = 0x15
EV_PWR = 0x16
EV_FF_STATUS = 0x17
EV_MAX = 0x1f
EV_CNT = (EV_MAX + 1)
KEY_MAX = 0x2ff
# Synchronization events
SYN_REPORT = 0
SYN_CONFIG = 1
SYN_MT_REPORT = 2
# Misc events
MSC_SERIAL = 0x00
MSC_PULSELED = 0x01
MSC_GESTURE = 0x02
MSC_RAW = 0x03
MSC_SCAN = 0x04
MSC_MAX = 0x07
MSC_CNT = (MSC_MAX + 1)
ABS_X = 0x00
ABS_Y = 0x01
ABS_PRESSURE = 0x18
ABS_MT_TOUCH_MAJOR = 0x30 # Major axis of touching ellipse
ABS_MT_TOUCH_MINOR = 0x31 # Minor axis (omit if circular)
ABS_MT_WIDTH_MAJOR = 0x32 # Major axis of approaching ellipse
ABS_MT_WIDTH_MINOR = 0x33 # Minor axis (omit if circular)
ABS_MT_ORIENTATION = 0x34 # Ellipse orientation
ABS_MT_POSITION_X = 0x35 # Center X ellipse position
ABS_MT_POSITION_Y = 0x36 # Center Y ellipse position
ABS_MT_TOOL_TYPE = 0x37 # Type of touching device
ABS_MT_BLOB_ID = 0x38 # Group a set of packets as a blob
ABS_MT_TRACKING_ID = 0x39 # Unique ID of initiated contact
ABS_MT_PRESSURE = 0x3a # Pressure on contact area
# some ioctl base (with 0 value)
EVIOCGNAME = 2147501318
EVIOCGBIT = 2147501344
EVIOCGABS = 2149074240
keyboard_keys = {
0x29: ('`', '~'),
0x02: ('1', '!'),
0x03: ('2', '@'),
0x04: ('3', '#'),
0x05: ('4', '$'),
0x06: ('5', '%'),
0x07: ('6', '^'),
0x08: ('7', '&'),
0x09: ('8', '*'),
0x0a: ('9', '('),
0x0b: ('0', ')'),
0x0c: ('-', '_'),
0x0d: ('=', '+'),
0x0e: ('backspace', ),
0x0f: ('tab', ),
0x10: ('q', 'Q'),
0x11: ('w', 'W'),
0x12: ('e', 'E'),
0x13: ('r', 'R'),
0x14: ('t', 'T'),
0x15: ('y', 'Y'),
0x16: ('u', 'U'),
0x17: ('i', 'I'),
0x18: ('o', 'O'),
0x19: ('p', 'P'),
0x1a: ('[', '{'),
0x1b: (']', '}'),
0x2b: ('\\', '|'),
0x3a: ('capslock', ),
0x1e: ('a', 'A'),
0x1f: ('s', 'S'),
0x20: ('d', 'D'),
0x21: ('f', 'F'),
0x22: ('g', 'G'),
0x23: ('h', 'H'),
0x24: ('j', 'J'),
0x25: ('k', 'K'),
0x26: ('l', 'L'),
0x27: (';', ':'),
0x28: ("'", '"'),
0xff: ('non-US-1', ),
0x1c: ('enter', ),
0x2a: ('shift', ),
0x2c: ('z', 'Z'),
0x2d: ('x', 'X'),
0x2e: ('c', 'C'),
0x2f: ('v', 'V'),
0x30: ('b', 'B'),
0x31: ('n', 'N'),
0x32: ('m', 'M'),
0x33: (',', '<'),
0x34: ('.', '>'),
0x35: ('/', '?'),
0x36: ('shift', ),
0x56: ('pipe', ),
0x1d: ('lctrl', ),
0x7D: ('super', ),
0x38: ('alt', ),
0x39: ('spacebar', ),
0x64: ('alt-gr', ),
0x7e: ('super', ),
0x7f: ('compose', ),
0x61: ('rctrl', ),
0x45: ('numlock', ),
0x47: ('numpad7', 'home'),
0x4b: ('numpad4', 'left'),
0x4f: ('numpad1', 'end'),
0x48: ('numpad8', 'up'),
0x4c: ('numpad5', ),
0x50: ('numpad2', 'down'),
0x52: ('numpad0', 'insert'),
0x37: ('numpadmul', ),
0x62: ('numpaddivide', ),
0x49: ('numpad9', 'pageup'),
0x4d: ('numpad6', 'right'),
0x51: ('numpad3', 'pagedown'),
0x53: ('numpaddecimal', 'delete'),
0x4a: ('numpadsubstract', ),
0x4e: ('numpadadd', ),
0x60: ('numpadenter', ),
0x01: ('escape', ),
0x3b: ('f1', ),
0x3c: ('f2', ),
0x3d: ('f3', ),
0x3e: ('f4', ),
0x3f: ('f5', ),
0x40: ('f6', ),
0x41: ('f7', ),
0x42: ('f8', ),
0x43: ('f9', ),
0x44: ('f10', ),
0x57: ('f11', ),
0x58: ('f12', ),
0x54: ('Alt+SysRq', ),
0x46: ('Screenlock', ),
0x67: ('up', ),
0x6c: ('down', ),
0x69: ('left', ),
0x6a: ('right', ),
0x6e: ('insert', ),
0x6f: ('delete', ),
0x66: ('home', ),
0x6b: ('end', ),
0x68: ('pageup', ),
0x6d: ('pagedown', ),
0x63: ('print', ),
0x77: ('pause', ),
# TODO combinations
# e0-37 PrtScr
# e0-46 Ctrl+Break
# e0-5b LWin (USB: LGUI)
# e0-5c RWin (USB: RGUI)
# e0-5d Menu
# e0-5f Sleep
# e0-5e Power
# e0-63 Wake
# e0-38 RAlt
# e0-1d RCtrl
# e0-52 Insert
# e0-53 Delete
# e0-47 Home
# e0-4f End
# e0-49 PgUp
# e0-51 PgDn
# e0-4b Left
# e0-48 Up
# e0-50 Down
# e0-4d Right
# e0-35 KP-/
# e0-1c KP-Enter
# e1-1d-45 77 Pause
}
keys_str = {
'spacebar': ' ',
'tab': ' ',
'shift': '',
'alt': '',
'ctrl': '',
'escape': '',
'numpad1': '1',
'numpad2': '2',
'numpad3': '3',
'numpad4': '4',
'numpad5': '5',
'numpad6': '6',
'numpad7': '7',
'numpad8': '8',
'numpad9': '9',
'numpad0': '0',
'numpadmul': '*',
'numpaddivide': '/',
'numpadadd': '+',
'numpaddecimal': '.',
'numpadsubstract': '-',
}
# sizeof(struct input_event)
struct_input_event_sz = struct.calcsize('LLHHi')
struct_input_absinfo_sz = struct.calcsize('iiiiii')
sz_l = struct.calcsize('Q')
class HIDInputMotionEventProvider(MotionEventProvider):
options = ('min_position_x', 'max_position_x',
'min_position_y', 'max_position_y',
'min_pressure', 'max_pressure',
'min_abs_x', 'max_abs_x',
'min_abs_y', 'max_abs_y',
'invert_x', 'invert_y', 'rotation')
def __init__(self, device, args):
super(HIDInputMotionEventProvider, self).__init__(device, args)
global Window, Keyboard
if Window is None:
from kivy.core.window import Window
if Keyboard is None:
from kivy.core.window import Keyboard
self.input_fn = None
self.default_ranges = dict()
# split arguments
args = args.split(',')
if not args:
Logger.error('HIDInput: Filename missing in configuration')
Logger.error('HIDInput: Use /dev/input/event0 for example')
return None
# read filename
self.input_fn = args[0]
Logger.info('HIDInput: Read event from <%s>' % self.input_fn)
# read parameters
for arg in args[1:]:
if arg == '':
continue
arg = arg.split('=')
# ensure it's a key = value
if len(arg) != 2:
Logger.error('HIDInput: invalid parameter '
'%s, not in key=value format.' % arg)
continue
# ensure the key exist
key, value = arg
if key not in HIDInputMotionEventProvider.options:
Logger.error('HIDInput: unknown %s option' % key)
continue
# ensure the value
try:
self.default_ranges[key] = int(value)
except ValueError:
err = 'HIDInput: invalid value "%s" for "%s"' % (
key, value)
Logger.error(err)
continue
# all good!
Logger.info('HIDInput: Set custom %s to %d' % (
key, int(value)))
if 'rotation' not in self.default_ranges:
self.default_ranges['rotation'] = 0
elif self.default_ranges['rotation'] not in (0, 90, 180, 270):
Logger.error('HIDInput: invalid rotation value ({})'.format(
self.default_ranges['rotation']))
self.default_ranges['rotation'] = 0
def start(self):
if self.input_fn is None:
return
self.uid = 0
self.queue = collections.deque()
self.dispatch_queue = []
self.thread = threading.Thread(
name=self.__class__.__name__,
target=self._thread_run,
kwargs=dict(
queue=self.queue,
input_fn=self.input_fn,
device=self.device,
default_ranges=self.default_ranges))
self.thread.daemon = True
self.thread.start()
def _thread_run(self, **kwargs):
input_fn = kwargs.get('input_fn')
queue = self.queue
dispatch_queue = self.dispatch_queue
device = kwargs.get('device')
drs = kwargs.get('default_ranges').get
touches = {}
touches_sent = []
point = {}
l_points = []
# prepare some vars to get limit of some component
range_min_position_x = 0
range_max_position_x = 2048
range_min_position_y = 0
range_max_position_y = 2048
range_min_pressure = 0
range_max_pressure = 255
range_min_abs_x = 0
range_max_abs_x = 255
range_min_abs_y = 0
range_max_abs_y = 255
range_min_abs_pressure = 0
range_max_abs_pressure = 255
invert_x = int(bool(drs('invert_x', 0)))
invert_y = int(bool(drs('invert_y', 1)))
rotation = drs('rotation', 0)
def assign_coord(point, value, invert, coords):
cx, cy = coords
if invert:
value = 1. - value
if rotation == 0:
point[cx] = value
elif rotation == 90:
point[cy] = value
elif rotation == 180:
point[cx] = 1. - value
elif rotation == 270:
point[cy] = 1. - value
def assign_rel_coord(point, value, invert, coords):
cx, cy = coords
if invert:
value = -1 * value
if rotation == 0:
point[cx] += value
elif rotation == 90:
point[cy] += value
elif rotation == 180:
point[cx] += -value
elif rotation == 270:
point[cy] += -value
# limit it to the screen area 0-1
point['x'] = min(1., max(0., point['x']))
point['y'] = min(1., max(0., point['y']))
def process_as_multitouch(tv_sec, tv_usec, ev_type,
ev_code, ev_value):
# sync event
if ev_type == EV_SYN:
if ev_code == SYN_MT_REPORT:
if 'id' not in point:
return
l_points.append(point.copy())
elif ev_code == SYN_REPORT:
process(l_points)
del l_points[:]
elif ev_type == EV_MSC and ev_code in (MSC_RAW, MSC_SCAN):
pass
else:
# compute multitouch track
if ev_code == ABS_MT_TRACKING_ID:
point.clear()
point['id'] = ev_value
elif ev_code == ABS_MT_POSITION_X:
val = normalize(ev_value,
range_min_position_x,
range_max_position_x)
assign_coord(point, val, invert_x, 'xy')
elif ev_code == ABS_MT_POSITION_Y:
val = 1. - normalize(ev_value,
range_min_position_y,
range_max_position_y)
assign_coord(point, val, invert_y, 'yx')
elif ev_code == ABS_MT_ORIENTATION:
point['orientation'] = ev_value
elif ev_code == ABS_MT_BLOB_ID:
point['blobid'] = ev_value
elif ev_code == ABS_MT_PRESSURE:
point['pressure'] = normalize(ev_value,
range_min_pressure,
range_max_pressure)
elif ev_code == ABS_MT_TOUCH_MAJOR:
point['size_w'] = ev_value
elif ev_code == ABS_MT_TOUCH_MINOR:
point['size_h'] = ev_value
def process_as_mouse_or_keyboard(
tv_sec, tv_usec, ev_type, ev_code, ev_value):
if ev_type == EV_SYN:
if ev_code == SYN_REPORT:
process([point])
if ('button' in point and
point['button'].startswith('scroll')):
# for scrolls we need to remove it as there is
# no up key
del point['button']
point['id'] += 1
point['_avoid'] = True
process([point])
elif ev_type == EV_REL:
if ev_code == 0:
assign_rel_coord(point,
min(1., max(-1., ev_value / 1000.)),
invert_x, 'xy')
elif ev_code == 1:
assign_rel_coord(point,
min(1., max(-1., ev_value / 1000.)),
invert_y, 'yx')
elif ev_code == 8: # Wheel
# translates the wheel move to a button
b = "scrollup" if ev_value < 0 else "scrolldown"
if 'button' not in point:
point['button'] = b
point['id'] += 1
if '_avoid' in point:
del point['_avoid']
elif ev_type != EV_KEY:
if ev_code == ABS_X:
val = normalize(ev_value,
range_min_abs_x,
range_max_abs_x)
assign_coord(point, val, invert_x, 'xy')
elif ev_code == ABS_Y:
val = 1. - normalize(ev_value,
range_min_abs_y,
range_max_abs_y)
assign_coord(point, val, invert_y, 'yx')
elif ev_code == ABS_PRESSURE:
point['pressure'] = normalize(ev_value,
range_min_abs_pressure,
range_max_abs_pressure)
else:
buttons = {
272: 'left',
273: 'right',
274: 'middle',
275: 'side',
276: 'extra',
277: 'forward',
278: 'back',
279: 'task',
330: 'touch',
320: 'pen'}
if ev_code in buttons.keys():
if ev_value:
if 'button' not in point:
point['button'] = buttons[ev_code]
point['id'] += 1
if '_avoid' in point:
del point['_avoid']
elif 'button' in point:
if point['button'] == buttons[ev_code]:
del point['button']
point['id'] += 1
point['_avoid'] = True
else:
if not 0 <= ev_value <= 1:
return
if ev_code not in keyboard_keys:
Logger.warn('HIDInput: unhandled HID code: {}'.
format(ev_code))
return
z = keyboard_keys[ev_code][-1 if 'shift' in
Window._modifiers else 0]
if z.lower() not in Keyboard.keycodes:
# or if it is not in this LUT
Logger.warn('HIDInput: unhandled character: {}'.
format(z))
return
keycode = Keyboard.keycodes[z.lower()]
if ev_value == 1:
if z == 'shift' or z == 'alt':
Window._modifiers.append(z)
elif z.endswith('ctrl'):
Window._modifiers.append('ctrl')
dispatch_queue.append(('key_down', (
keycode, ev_code,
keys_str.get(z, z), Window._modifiers)))
elif ev_value == 0:
dispatch_queue.append(('key_up', (
keycode, ev_code,
keys_str.get(z, z), Window._modifiers)))
if ((z == 'shift' or z == 'alt') and
(z in Window._modifiers)):
Window._modifiers.remove(z)
elif (z.endswith('ctrl') and
'ctrl' in Window._modifiers):
Window._modifiers.remove('ctrl')
def process(points):
if not is_multitouch:
dispatch_queue.append(('mouse_pos', (
points[0]['x'] * Window.width,
points[0]['y'] * Window.height)))
actives = [args['id']
for args in points
if 'id' in args and '_avoid' not in args]
for args in points:
tid = args['id']
try:
touch = touches[tid]
if touch.sx == args['x'] and touch.sy == args['y']:
continue
touch.move(args)
if tid not in touches_sent:
queue.append(('begin', touch))
touches_sent.append(tid)
queue.append(('update', touch))
except KeyError:
if '_avoid' not in args:
touch = HIDMotionEvent(device, tid, args)
touches[touch.id] = touch
if tid not in touches_sent:
queue.append(('begin', touch))
touches_sent.append(tid)
for tid in list(touches.keys())[:]:
if tid not in actives:
touch = touches[tid]
if tid in touches_sent:
touch.update_time_end()
queue.append(('end', touch))
touches_sent.remove(tid)
del touches[tid]
def normalize(value, vmin, vmax):
return (value - vmin) / float(vmax - vmin)
# open the input
fd = open(input_fn, 'rb')
# get the controller name (EVIOCGNAME)
device_name = fcntl.ioctl(fd, EVIOCGNAME + (256 << 16),
" " * 256).decode().strip()
Logger.info('HIDMotionEvent: using <%s>' % device_name)
# get abs infos
bit = fcntl.ioctl(fd, EVIOCGBIT + (EV_MAX << 16), ' ' * sz_l)
bit, = struct.unpack('Q', bit)
is_multitouch = False
for x in range(EV_MAX):
# preserve this, we may want other things than EV_ABS
if x != EV_ABS:
continue
# EV_ABS available for this device ?
if (bit & (1 << x)) == 0:
continue
# ask abs info keys to the devices
sbit = fcntl.ioctl(fd, EVIOCGBIT + x + (KEY_MAX << 16),
' ' * sz_l)
sbit, = struct.unpack('Q', sbit)
for y in range(KEY_MAX):
if (sbit & (1 << y)) == 0:
continue
absinfo = fcntl.ioctl(fd, EVIOCGABS + y +
(struct_input_absinfo_sz << 16),
' ' * struct_input_absinfo_sz)
abs_value, abs_min, abs_max, abs_fuzz, \
abs_flat, abs_res = struct.unpack('iiiiii', absinfo)
if y == ABS_MT_POSITION_X:
is_multitouch = True
range_min_position_x = drs('min_position_x', abs_min)
range_max_position_x = drs('max_position_x', abs_max)
Logger.info('HIDMotionEvent: ' +
'<%s> range position X is %d - %d' % (
device_name, abs_min, abs_max))
elif y == ABS_MT_POSITION_Y:
is_multitouch = True
range_min_position_y = drs('min_position_y', abs_min)
range_max_position_y = drs('max_position_y', abs_max)
Logger.info('HIDMotionEvent: ' +
'<%s> range position Y is %d - %d' % (
device_name, abs_min, abs_max))
elif y == ABS_MT_PRESSURE:
range_min_pressure = drs('min_pressure', abs_min)
range_max_pressure = drs('max_pressure', abs_max)
Logger.info('HIDMotionEvent: ' +
'<%s> range pressure is %d - %d' % (
device_name, abs_min, abs_max))
elif y == ABS_X:
range_min_abs_x = drs('min_abs_x', abs_min)
range_max_abs_x = drs('max_abs_x', abs_max)
Logger.info('HIDMotionEvent: ' +
'<%s> range ABS X position is %d - %d' % (
device_name, abs_min, abs_max))
elif y == ABS_Y:
range_min_abs_y = drs('min_abs_y', abs_min)
range_max_abs_y = drs('max_abs_y', abs_max)
Logger.info('HIDMotionEvent: ' +
'<%s> range ABS Y position is %d - %d' % (
device_name, abs_min, abs_max))
elif y == ABS_PRESSURE:
range_min_abs_pressure = drs(
'min_abs_pressure', abs_min)
range_max_abs_pressure = drs(
'max_abs_pressure', abs_max)
Logger.info('HIDMotionEvent: ' +
'<%s> range ABS pressure is %d - %d' % (
device_name, abs_min, abs_max))
# init the point
if not is_multitouch:
point = {'x': .5, 'y': .5, 'id': 0, '_avoid': True}
# read until the end
while fd:
data = fd.read(struct_input_event_sz)
if len(data) < struct_input_event_sz:
break
# extract each event
for i in range(int(len(data) / struct_input_event_sz)):
ev = data[i * struct_input_event_sz:]
# extract timeval + event infos
infos = struct.unpack('LLHHi', ev[:struct_input_event_sz])
if is_multitouch:
process_as_multitouch(*infos)
else:
process_as_mouse_or_keyboard(*infos)
def update(self, dispatch_fn):
# dispatch all events from threads
dispatch_queue = self.dispatch_queue
n = len(dispatch_queue)
for name, args in dispatch_queue[:n]:
if name == 'mouse_pos':
Window.mouse_pos = args
elif name == 'key_down':
if not Window.dispatch('on_key_down', *args):
Window.dispatch('on_keyboard', *args)
elif name == 'key_up':
Window.dispatch('on_key_up', *args)
del dispatch_queue[:n]
try:
while True:
event_type, touch = self.queue.popleft()
dispatch_fn(event_type, touch)
except:
pass
MotionEventFactory.register('hidinput', HIDInputMotionEventProvider)

View file

@ -0,0 +1,113 @@
'''
Leap Motion - finger only
=========================
'''
__all__ = ('LeapFingerEventProvider', 'LeapFingerEvent')
from collections import deque
from kivy.logger import Logger
from kivy.input.provider import MotionEventProvider
from kivy.input.factory import MotionEventFactory
from kivy.input.motionevent import MotionEvent
_LEAP_QUEUE = deque()
Leap = InteractionBox = None
def normalize(value, a, b):
return (value - a) / float(b - a)
class LeapFingerEvent(MotionEvent):
def __init__(self, *args, **kwargs):
kwargs.setdefault('is_touch', True)
kwargs.setdefault('type_id', 'touch')
super().__init__(*args, **kwargs)
self.profile = ('pos', 'pos3d',)
def depack(self, args):
super().depack(args)
if args[0] is None:
return
x, y, z = args
self.sx = normalize(x, -150, 150)
self.sy = normalize(y, 40, 460)
self.sz = normalize(z, -350, 350)
self.z = z
class LeapFingerEventProvider(MotionEventProvider):
__handlers__ = {}
def start(self):
# don't do the import at start, or the error will be always displayed
# for user who don't have Leap
global Leap, InteractionBox
import Leap
from Leap import InteractionBox
class LeapMotionListener(Leap.Listener):
def on_init(self, controller):
Logger.info('leapmotion: Initialized')
def on_connect(self, controller):
Logger.info('leapmotion: Connected')
def on_disconnect(self, controller):
Logger.info('leapmotion: Disconnected')
def on_frame(self, controller):
frame = controller.frame()
_LEAP_QUEUE.append(frame)
def on_exit(self, controller):
pass
self.uid = 0
self.touches = {}
self.listener = LeapMotionListener()
self.controller = Leap.Controller(self.listener)
def update(self, dispatch_fn):
try:
while True:
frame = _LEAP_QUEUE.popleft()
events = self.process_frame(frame)
for ev in events:
dispatch_fn(*ev)
except IndexError:
pass
def process_frame(self, frame):
events = []
touches = self.touches
available_uid = []
for hand in frame.hands:
for finger in hand.fingers:
# print(hand.id(), finger.id(), finger.tip())
uid = '{0}:{1}'.format(hand.id, finger.id)
available_uid.append(uid)
position = finger.tip_position
args = (position.x, position.y, position.z)
if uid not in touches:
touch = LeapFingerEvent(self.device, uid, args)
events.append(('begin', touch))
touches[uid] = touch
else:
touch = touches[uid]
touch.move(args)
events.append(('update', touch))
for key in list(touches.keys())[:]:
if key not in available_uid:
events.append(('end', touches[key]))
del touches[key]
return events
# registers
MotionEventFactory.register('leapfinger', LeapFingerEventProvider)

View file

@ -0,0 +1,396 @@
'''
Native support of Wacom tablet from linuxwacom driver
=====================================================
To configure LinuxWacom, add this to your configuration::
[input]
pen = linuxwacom,/dev/input/event2,mode=pen
finger = linuxwacom,/dev/input/event3,mode=touch
.. note::
You must have read access to the input event.
You can use a custom range for the X, Y and pressure values.
On some drivers, the range reported is invalid.
To fix that, you can add these options to the argument line:
* invert_x : 1 to invert X axis
* invert_y : 1 to invert Y axis
* min_position_x : X minimum
* max_position_x : X maximum
* min_position_y : Y minimum
* max_position_y : Y maximum
* min_pressure : pressure minimum
* max_pressure : pressure maximum
'''
__all__ = ('LinuxWacomMotionEventProvider', 'LinuxWacomMotionEvent')
import os
from kivy.input.motionevent import MotionEvent
from kivy.input.shape import ShapeRect
class LinuxWacomMotionEvent(MotionEvent):
def __init__(self, *args, **kwargs):
kwargs.setdefault('is_touch', True)
kwargs.setdefault('type_id', 'touch')
super().__init__(*args, **kwargs)
def depack(self, args):
self.sx = args['x']
self.sy = args['y']
self.profile = ['pos']
if 'size_w' in args and 'size_h' in args:
self.shape = ShapeRect()
self.shape.width = args['size_w']
self.shape.height = args['size_h']
self.profile.append('shape')
if 'pressure' in args:
self.pressure = args['pressure']
self.profile.append('pressure')
super().depack(args)
def __str__(self):
return '<LinuxWacomMotionEvent id=%d pos=(%f, %f) device=%s>' \
% (self.id, self.sx, self.sy, self.device)
if 'KIVY_DOC' in os.environ:
# documentation hack
LinuxWacomMotionEventProvider = None
else:
import threading
import collections
import struct
import fcntl
from kivy.input.provider import MotionEventProvider
from kivy.input.factory import MotionEventFactory
from kivy.logger import Logger
#
# This part is taken from linux-source-2.6.32/include/linux/input.h
#
# Event types
EV_SYN = 0x00
EV_KEY = 0x01
EV_REL = 0x02
EV_ABS = 0x03
EV_MSC = 0x04
EV_SW = 0x05
EV_LED = 0x11
EV_SND = 0x12
EV_REP = 0x14
EV_FF = 0x15
EV_PWR = 0x16
EV_FF_STATUS = 0x17
EV_MAX = 0x1f
EV_CNT = (EV_MAX + 1)
KEY_MAX = 0x2ff
# Synchronization events
SYN_REPORT = 0
SYN_CONFIG = 1
SYN_MT_REPORT = 2
# Misc events
MSC_SERIAL = 0x00
MSC_PULSELED = 0x01
MSC_GESTURE = 0x02
MSC_RAW = 0x03
MSC_SCAN = 0x04
MSC_MAX = 0x07
MSC_CNT = (MSC_MAX + 1)
ABS_X = 0x00
ABS_Y = 0x01
ABS_PRESSURE = 0x18
ABS_MISC = 0x28 # if 0, it's touch up
ABS_MT_TOUCH_MAJOR = 0x30 # Major axis of touching ellipse
ABS_MT_TOUCH_MINOR = 0x31 # Minor axis (omit if circular)
ABS_MT_WIDTH_MAJOR = 0x32 # Major axis of approaching ellipse
ABS_MT_WIDTH_MINOR = 0x33 # Minor axis (omit if circular)
ABS_MT_ORIENTATION = 0x34 # Ellipse orientation
ABS_MT_POSITION_X = 0x35 # Center X ellipse position
ABS_MT_POSITION_Y = 0x36 # Center Y ellipse position
ABS_MT_TOOL_TYPE = 0x37 # Type of touching device
ABS_MT_BLOB_ID = 0x38 # Group a set of packets as a blob
ABS_MT_TRACKING_ID = 0x39 # Unique ID of initiated contact
ABS_MT_PRESSURE = 0x3a # Pressure on contact area
# some ioctl base (with 0 value)
EVIOCGNAME = 2147501318
EVIOCGBIT = 2147501344
EVIOCGABS = 2149074240
# sizeof(struct input_event)
struct_input_event_sz = struct.calcsize('LLHHi')
struct_input_absinfo_sz = struct.calcsize('iiiiii')
sz_l = struct.calcsize('Q')
class LinuxWacomMotionEventProvider(MotionEventProvider):
options = ('min_position_x', 'max_position_x',
'min_position_y', 'max_position_y',
'min_pressure', 'max_pressure',
'invert_x', 'invert_y')
def __init__(self, device, args):
super(LinuxWacomMotionEventProvider, self).__init__(device, args)
self.input_fn = None
self.default_ranges = dict()
self.mode = 'touch'
# split arguments
args = args.split(',')
if not args:
Logger.error('LinuxWacom: No filename given in config')
Logger.error('LinuxWacom: Use /dev/input/event0 for example')
return
# read filename
self.input_fn = args[0]
Logger.info('LinuxWacom: Read event from <%s>' % self.input_fn)
# read parameters
for arg in args[1:]:
if arg == '':
continue
arg = arg.split('=')
# ensure it's a key = value
if len(arg) != 2:
err = 'LinuxWacom: Bad parameter' \
'%s: Not in key=value format.' % arg
Logger.error(err)
continue
# ensure the key exist
key, value = arg
if key == 'mode':
self.mode = value
continue
if key not in LinuxWacomMotionEventProvider.options:
Logger.error('LinuxWacom: unknown %s option' % key)
continue
# ensure the value
try:
self.default_ranges[key] = int(value)
except ValueError:
err = 'LinuxWacom: value %s invalid for %s' % (key, value)
Logger.error(err)
continue
# all good!
msg = 'LinuxWacom: Set custom %s to %d' % (key, int(value))
Logger.info(msg)
Logger.info('LinuxWacom: mode is <%s>' % self.mode)
def start(self):
if self.input_fn is None:
return
self.uid = 0
self.queue = collections.deque()
self.thread = threading.Thread(
target=self._thread_run,
kwargs=dict(
queue=self.queue,
input_fn=self.input_fn,
device=self.device,
default_ranges=self.default_ranges))
self.thread.daemon = True
self.thread.start()
def _thread_run(self, **kwargs):
input_fn = kwargs.get('input_fn')
queue = kwargs.get('queue')
device = kwargs.get('device')
drs = kwargs.get('default_ranges').get
touches = {}
touches_sent = []
l_points = {}
# prepare some vars to get limit of some component
range_min_position_x = 0
range_max_position_x = 2048
range_min_position_y = 0
range_max_position_y = 2048
range_min_pressure = 0
range_max_pressure = 255
invert_x = int(bool(drs('invert_x', 0)))
invert_y = int(bool(drs('invert_y', 0)))
reset_touch = False
def process(points):
actives = list(points.keys())
for args in points.values():
tid = args['id']
try:
touch = touches[tid]
except KeyError:
touch = LinuxWacomMotionEvent(device, tid, args)
touches[touch.id] = touch
if touch.sx == args['x'] \
and touch.sy == args['y'] \
and tid in touches_sent:
continue
touch.move(args)
if tid not in touches_sent:
queue.append(('begin', touch))
touches_sent.append(tid)
queue.append(('update', touch))
for tid in list(touches.keys())[:]:
if tid not in actives:
touch = touches[tid]
if tid in touches_sent:
touch.update_time_end()
queue.append(('end', touch))
touches_sent.remove(tid)
del touches[tid]
def normalize(value, vmin, vmax):
return (value - vmin) / float(vmax - vmin)
# open the input
try:
fd = open(input_fn, 'rb')
except IOError:
Logger.exception('Unable to open %s' % input_fn)
return
# get the controller name (EVIOCGNAME)
device_name = fcntl.ioctl(fd, EVIOCGNAME + (256 << 16),
" " * 256).split('\x00')[0]
Logger.info('LinuxWacom: using <%s>' % device_name)
# get abs infos
bit = fcntl.ioctl(fd, EVIOCGBIT + (EV_MAX << 16), ' ' * sz_l)
bit, = struct.unpack('Q', bit)
for x in range(EV_MAX):
# preserve this, we may want other things than EV_ABS
if x != EV_ABS:
continue
# EV_ABS available for this device ?
if (bit & (1 << x)) == 0:
continue
# ask abs info keys to the devices
sbit = fcntl.ioctl(fd, EVIOCGBIT + x + (KEY_MAX << 16),
' ' * sz_l)
sbit, = struct.unpack('Q', sbit)
for y in range(KEY_MAX):
if (sbit & (1 << y)) == 0:
continue
absinfo = fcntl.ioctl(fd, EVIOCGABS + y +
(struct_input_absinfo_sz << 16),
' ' * struct_input_absinfo_sz)
abs_value, abs_min, abs_max, abs_fuzz, \
abs_flat, abs_res = struct.unpack('iiiiii', absinfo)
if y == ABS_X:
range_min_position_x = drs('min_position_x', abs_min)
range_max_position_x = drs('max_position_x', abs_max)
Logger.info('LinuxWacom: ' +
'<%s> range position X is %d - %d' % (
device_name, abs_min, abs_max))
elif y == ABS_Y:
range_min_position_y = drs('min_position_y', abs_min)
range_max_position_y = drs('max_position_y', abs_max)
Logger.info('LinuxWacom: ' +
'<%s> range position Y is %d - %d' % (
device_name, abs_min, abs_max))
elif y == ABS_PRESSURE:
range_min_pressure = drs('min_pressure', abs_min)
range_max_pressure = drs('max_pressure', abs_max)
Logger.info('LinuxWacom: ' +
'<%s> range pressure is %d - %d' % (
device_name, abs_min, abs_max))
# read until the end
changed = False
touch_id = 0
touch_x = 0
touch_y = 0
touch_pressure = 0
while fd:
data = fd.read(struct_input_event_sz)
if len(data) < struct_input_event_sz:
break
# extract each event
for i in range(len(data) / struct_input_event_sz):
ev = data[i * struct_input_event_sz:]
# extract timeval + event infos
tv_sec, tv_usec, ev_type, ev_code, ev_value = \
struct.unpack('LLHHi', ev[:struct_input_event_sz])
if ev_type == EV_SYN and ev_code == SYN_REPORT:
if touch_id in l_points:
p = l_points[touch_id]
else:
p = dict()
l_points[touch_id] = p
p['id'] = touch_id
if not reset_touch:
p['x'] = touch_x
p['y'] = touch_y
p['pressure'] = touch_pressure
if self.mode == 'pen' \
and touch_pressure == 0 \
and not reset_touch:
del l_points[touch_id]
if changed:
if 'x' not in p:
reset_touch = False
continue
process(l_points)
changed = False
if reset_touch:
l_points.clear()
reset_touch = False
process(l_points)
elif ev_type == EV_MSC and ev_code == MSC_SERIAL:
touch_id = ev_value
elif ev_type == EV_ABS and ev_code == ABS_X:
val = normalize(ev_value,
range_min_position_x,
range_max_position_x)
if invert_x:
val = 1. - val
touch_x = val
changed = True
elif ev_type == EV_ABS and ev_code == ABS_Y:
val = 1. - normalize(ev_value,
range_min_position_y,
range_max_position_y)
if invert_y:
val = 1. - val
touch_y = val
changed = True
elif ev_type == EV_ABS and ev_code == ABS_PRESSURE:
touch_pressure = normalize(ev_value,
range_min_pressure,
range_max_pressure)
changed = True
elif ev_type == EV_ABS and ev_code == ABS_MISC:
if ev_value == 0:
reset_touch = True
def update(self, dispatch_fn):
# dispatch all event from threads
try:
while True:
event_type, touch = self.queue.popleft()
dispatch_fn(event_type, touch)
except:
pass
MotionEventFactory.register('linuxwacom', LinuxWacomMotionEventProvider)

View file

@ -0,0 +1,220 @@
'''
Native support of MultitouchSupport framework for MacBook (MaxOSX platform)
===========================================================================
'''
__all__ = ('MacMotionEventProvider', )
import ctypes
import threading
import collections
import os
from kivy.input.provider import MotionEventProvider
from kivy.input.factory import MotionEventFactory
from kivy.input.motionevent import MotionEvent
from kivy.input.shape import ShapeRect
if 'KIVY_DOC' not in os.environ:
CFArrayRef = ctypes.c_void_p
CFMutableArrayRef = ctypes.c_void_p
CFIndex = ctypes.c_long
dll = '/System/Library/PrivateFrameworks/' + \
'MultitouchSupport.framework/MultitouchSupport'
MultitouchSupport = ctypes.CDLL(dll)
CFArrayGetCount = MultitouchSupport.CFArrayGetCount
CFArrayGetCount.argtypes = [CFArrayRef]
CFArrayGetCount.restype = CFIndex
CFArrayGetValueAtIndex = MultitouchSupport.CFArrayGetValueAtIndex
CFArrayGetValueAtIndex.argtypes = [CFArrayRef, CFIndex]
CFArrayGetValueAtIndex.restype = ctypes.c_void_p
MTDeviceCreateList = MultitouchSupport.MTDeviceCreateList
MTDeviceCreateList.argtypes = []
MTDeviceCreateList.restype = CFMutableArrayRef
class MTPoint(ctypes.Structure):
_fields_ = [('x', ctypes.c_float),
('y', ctypes.c_float)]
class MTVector(ctypes.Structure):
_fields_ = [('position', MTPoint),
('velocity', MTPoint)]
class MTData(ctypes.Structure):
_fields_ = [
('frame', ctypes.c_int),
('timestamp', ctypes.c_double),
('identifier', ctypes.c_int),
# Current state (of unknown meaning).
('state', ctypes.c_int),
('unknown1', ctypes.c_int),
('unknown2', ctypes.c_int),
# Normalized position and vector of the touch (0 to 1)
('normalized', MTVector),
# The area of the touch.
('size', ctypes.c_float),
('unknown3', ctypes.c_int),
# The following three define the ellipsoid of a finger.
('angle', ctypes.c_float),
('major_axis', ctypes.c_float),
('minor_axis', ctypes.c_float),
('unknown4', MTVector),
('unknown5_1', ctypes.c_int),
('unknown5_2', ctypes.c_int),
('unknown6', ctypes.c_float), ]
MTDataRef = ctypes.POINTER(MTData)
MTContactCallbackFunction = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int,
MTDataRef, ctypes.c_int,
ctypes.c_double, ctypes.c_int)
MTDeviceRef = ctypes.c_void_p
MTRegisterContactFrameCallback = \
MultitouchSupport.MTRegisterContactFrameCallback
MTRegisterContactFrameCallback.argtypes = \
[MTDeviceRef, MTContactCallbackFunction]
MTRegisterContactFrameCallback.restype = None
MTDeviceStart = MultitouchSupport.MTDeviceStart
MTDeviceStart.argtypes = [MTDeviceRef, ctypes.c_int]
MTDeviceStart.restype = None
else:
MTContactCallbackFunction = lambda x: None
class MacMotionEvent(MotionEvent):
'''MotionEvent representing a contact point on the touchpad. Supports pos
and shape profiles.
'''
def __init__(self, *args, **kwargs):
kwargs.setdefault('is_touch', True)
kwargs.setdefault('type_id', 'touch')
super().__init__(*args, **kwargs)
self.profile = ('pos', 'shape')
def depack(self, args):
self.shape = ShapeRect()
self.sx, self.sy = args[0], args[1]
self.shape.width = args[2]
self.shape.height = args[2]
super().depack(args)
def __str__(self):
return '<MacMotionEvent id=%d pos=(%f, %f) device=%s>' \
% (self.id, self.sx, self.sy, self.device)
_instance = None
class MacMotionEventProvider(MotionEventProvider):
def __init__(self, *largs, **kwargs):
global _instance
if _instance is not None:
raise Exception('Only one MacMotionEvent provider is allowed.')
_instance = self
super(MacMotionEventProvider, self).__init__(*largs, **kwargs)
def start(self):
# global uid
self.uid = 0
# touches will be per devices
self.touches = {}
# lock needed to access on uid
self.lock = threading.Lock()
# event queue to dispatch in main thread
self.queue = collections.deque()
# ok, listing devices, and attach !
devices = MultitouchSupport.MTDeviceCreateList()
num_devices = CFArrayGetCount(devices)
for i in range(num_devices):
device = CFArrayGetValueAtIndex(devices, i)
# create touch dict for this device
data_id = str(device)
self.touches[data_id] = {}
# start !
MTRegisterContactFrameCallback(device, self._mts_callback)
MTDeviceStart(device, 0)
def update(self, dispatch_fn):
# dispatch all event from threads
try:
while True:
event_type, touch = self.queue.popleft()
dispatch_fn(event_type, touch)
except:
pass
def stop(self):
# i don't known how to stop it...
pass
@MTContactCallbackFunction
def _mts_callback(device, data_ptr, n_fingers, timestamp, frame):
global _instance
devid = str(device)
# XXX create live touch, we get one case that
# the device announced by macosx don't match the device
# in _mts_callback....
if devid not in _instance.touches:
_instance.touches[devid] = {}
touches = _instance.touches[devid]
actives = []
for i in range(n_fingers):
# get pointer on data
data = data_ptr[i]
# add this touch as an active touch
actives.append(data.identifier)
# extract identifier
data_id = data.identifier
# prepare argument position
norm_pos = data.normalized.position
args = (norm_pos.x, norm_pos.y, data.size)
if data_id not in touches:
# increment uid
_instance.lock.acquire()
_instance.uid += 1
# create a touch
touch = MacMotionEvent(_instance.device, _instance.uid, args)
_instance.lock.release()
# create event
_instance.queue.append(('begin', touch))
# store touch
touches[data_id] = touch
else:
touch = touches[data_id]
# check if he really moved
if data.normalized.position.x == touch.sx and \
data.normalized.position.y == touch.sy:
continue
touch.move(args)
_instance.queue.append(('update', touch))
# delete old touchs
for tid in list(touches.keys())[:]:
if tid not in actives:
touch = touches[tid]
touch.update_time_end()
_instance.queue.append(('end', touch))
del touches[tid]
return 0
MotionEventFactory.register('mactouch', MacMotionEventProvider)

View file

@ -0,0 +1,424 @@
'''
Mouse provider implementation
=============================
On linux systems, the mouse provider can be annoying when used with another
multitouch provider (hidinput or mtdev). The Mouse can conflict with them: a
single touch can generate one event from the mouse provider and another
from the multitouch provider.
To avoid this behavior, you can activate the "disable_on_activity" token in
the mouse configuration. Then, if any touches are created by another
provider, the mouse event will be discarded. Add this to your configuration::
[input]
mouse = mouse,disable_on_activity
Using multitouch interaction with the mouse
-------------------------------------------
.. versionadded:: 1.3.0
By default, the middle and right mouse buttons, as well as a combination of
ctrl + left mouse button are used for multitouch emulation.
If you want to use them for other purposes, you can disable this behavior by
activating the "disable_multitouch" token::
[input]
mouse = mouse,disable_multitouch
.. versionchanged:: 1.9.0
You can now selectively control whether a click initiated as described above
will emulate multi-touch. If the touch has been initiated in the above manner
(e.g. right mouse button), a `multitouch_sim` value will be added to the
touch's profile, and a `multitouch_sim` property will be added to the touch.
By default, `multitouch_sim` is True and multitouch will be emulated for that
touch. If, however, `multitouch_on_demand` is added to the config::
[input]
mouse = mouse,multitouch_on_demand
then `multitouch_sim` defaults to `False`. In that case, if `multitouch_sim`
is set to True before the mouse is released (e.g. in on_touch_down/move), the
touch will simulate a multi-touch event. For example::
if 'multitouch_sim' in touch.profile:
touch.multitouch_sim = True
.. versionchanged:: 2.1.0
Provider dispatches hover events by listening to properties/events in
:class:`~kivy.core.window.Window`. Dispatching can be disabled by setting
:attr:`MouseMotionEventProvider.disable_hover` to ``True`` or by adding
`disable_hover` in the config::
[input]
mouse = mouse,disable_hover
It's also possible to enable/disable hover events at runtime with
:attr:`MouseMotionEventProvider.disable_hover` property.
Following is a list of the supported values for the
:attr:`~kivy.input.motionevent.MotionEvent.profile` property list.
================ ==========================================================
Profile value Description
---------------- ----------------------------------------------------------
button Mouse button (one of `left`, `right`, `middle`, `scrollup`
or `scrolldown`). Accessed via the 'button' property.
pos 2D position. Also reflected in the
:attr:`~kivy.input.motionevent.MotionEvent.x`,
:attr:`~kivy.input.motionevent.MotionEvent.y`
and :attr:`~kivy.input.motionevent.MotionEvent.pos`
properties.
multitouch_sim Specifies whether multitouch is simulated or not. Accessed
via the 'multitouch_sim' property.
================ ==========================================================
'''
__all__ = ('MouseMotionEventProvider', )
from kivy.base import EventLoop
from collections import deque
from kivy.logger import Logger
from kivy.input.provider import MotionEventProvider
from kivy.input.factory import MotionEventFactory
from kivy.input.motionevent import MotionEvent
# late binding
Color = Ellipse = None
class MouseMotionEvent(MotionEvent):
def __init__(self, *args, **kwargs):
self.multitouch_sim = False
super().__init__(*args, **kwargs)
def depack(self, args):
self.sx, self.sy = args[:2]
profile = self.profile
if self.is_touch:
# don't overwrite previous profile
if not profile:
profile.extend(('pos', 'button'))
if len(args) >= 3:
self.button = args[2]
if len(args) == 4:
self.multitouch_sim = args[3]
profile.append('multitouch_sim')
else:
if not profile:
profile.append('pos')
super().depack(args)
#
# Create automatically touch on the surface.
#
def update_graphics(self, win, create=False):
global Color, Ellipse
de = self.ud.get('_drawelement', None)
if de is None and create:
if Color is None:
from kivy.graphics import Color, Ellipse
with win.canvas.after:
de = (
Color(.8, .2, .2, .7),
Ellipse(size=(20, 20), segments=15))
self.ud._drawelement = de
if de is not None:
self.push()
# use same logic as WindowBase.on_motion() so we get correct
# coordinates when _density != 1
w, h = win._get_effective_size()
self.scale_for_screen(w, h, rotation=win.rotation)
de[1].pos = self.x - 10, self.y - 10
self.pop()
def clear_graphics(self, win):
de = self.ud.pop('_drawelement', None)
if de is not None:
win.canvas.after.remove(de[0])
win.canvas.after.remove(de[1])
class MouseMotionEventProvider(MotionEventProvider):
__handlers__ = {}
def __init__(self, device, args):
super(MouseMotionEventProvider, self).__init__(device, args)
self.waiting_event = deque()
self.touches = {}
self.counter = 0
self.current_drag = None
self.alt_touch = None
self.disable_on_activity = False
self.disable_multitouch = False
self.multitouch_on_demand = False
self.hover_event = None
self._disable_hover = False
self._running = False
# split arguments
args = args.split(',')
for arg in args:
arg = arg.strip()
if arg == '':
continue
elif arg == 'disable_on_activity':
self.disable_on_activity = True
elif arg == 'disable_multitouch':
self.disable_multitouch = True
elif arg == 'disable_hover':
self.disable_hover = True
elif arg == 'multitouch_on_demand':
self.multitouch_on_demand = True
else:
Logger.error('Mouse: unknown parameter <%s>' % arg)
def _get_disable_hover(self):
return self._disable_hover
def _set_disable_hover(self, value):
if self._disable_hover != value:
if self._running:
if value:
self._stop_hover_events()
else:
self._start_hover_events()
self._disable_hover = value
disable_hover = property(_get_disable_hover, _set_disable_hover)
'''Disables dispatching of hover events if set to ``True``.
Hover events are enabled by default (`disable_hover` is ``False``). See
module documentation if you want to enable/disable hover events through
config file.
.. versionadded:: 2.1.0
'''
def start(self):
'''Start the mouse provider'''
if not EventLoop.window:
return
fbind = EventLoop.window.fbind
fbind('on_mouse_down', self.on_mouse_press)
fbind('on_mouse_move', self.on_mouse_motion)
fbind('on_mouse_up', self.on_mouse_release)
fbind('on_rotate', self.update_touch_graphics)
fbind('system_size', self.update_touch_graphics)
if not self.disable_hover:
self._start_hover_events()
self._running = True
def _start_hover_events(self):
fbind = EventLoop.window.fbind
fbind('mouse_pos', self.begin_or_update_hover_event)
fbind('system_size', self.update_hover_event)
fbind('on_cursor_enter', self.begin_hover_event)
fbind('on_cursor_leave', self.end_hover_event)
fbind('on_close', self.end_hover_event)
fbind('on_rotate', self.update_hover_event)
def stop(self):
'''Stop the mouse provider'''
if not EventLoop.window:
return
funbind = EventLoop.window.funbind
funbind('on_mouse_down', self.on_mouse_press)
funbind('on_mouse_move', self.on_mouse_motion)
funbind('on_mouse_up', self.on_mouse_release)
funbind('on_rotate', self.update_touch_graphics)
funbind('system_size', self.update_touch_graphics)
if not self.disable_hover:
self._stop_hover_events()
self._running = False
def _stop_hover_events(self):
funbind = EventLoop.window.funbind
funbind('mouse_pos', self.begin_or_update_hover_event)
funbind('system_size', self.update_hover_event)
funbind('on_cursor_enter', self.begin_hover_event)
funbind('on_cursor_leave', self.end_hover_event)
funbind('on_close', self.end_hover_event)
funbind('on_rotate', self.update_hover_event)
def test_activity(self):
if not self.disable_on_activity:
return False
# trying to get if we currently have other touch than us
# discard touches generated from kinetic
for touch in EventLoop.touches:
# discard all kinetic touch
if touch.__class__.__name__ == 'KineticMotionEvent':
continue
# not our instance, stop mouse
if touch.__class__ != MouseMotionEvent:
return True
return False
def find_touch(self, win, x, y):
factor = 10. / win.system_size[0]
for touch in self.touches.values():
if abs(x - touch.sx) < factor and abs(y - touch.sy) < factor:
return touch
return None
def create_event_id(self):
self.counter += 1
return self.device + str(self.counter)
def create_touch(self, win, nx, ny, is_double_tap, do_graphics, button):
event_id = self.create_event_id()
args = [nx, ny, button]
if do_graphics:
args += [not self.multitouch_on_demand]
self.current_drag = touch = MouseMotionEvent(
self.device, event_id, args,
is_touch=True,
type_id='touch'
)
touch.is_double_tap = is_double_tap
self.touches[event_id] = touch
if do_graphics:
# only draw red circle if multitouch is not disabled, and
# if the multitouch_on_demand feature is not enable
# (because in that case, we wait to see if multitouch_sim
# is True or not before doing the multitouch)
create_flag = (
not self.disable_multitouch
and not self.multitouch_on_demand
)
touch.update_graphics(win, create_flag)
self.waiting_event.append(('begin', touch))
return touch
def remove_touch(self, win, touch):
if touch.id in self.touches:
del self.touches[touch.id]
touch.update_time_end()
self.waiting_event.append(('end', touch))
touch.clear_graphics(win)
def create_hover(self, win, etype):
nx, ny = win.to_normalized_pos(*win.mouse_pos)
# Divide by density because it's used by mouse_pos
nx /= win._density
ny /= win._density
args = (nx, ny)
hover = self.hover_event
if hover:
hover.move(args)
else:
self.hover_event = hover = MouseMotionEvent(
self.device,
self.create_event_id(),
args,
type_id='hover'
)
if etype == 'end':
hover.update_time_end()
self.hover_event = None
self.waiting_event.append((etype, hover))
def on_mouse_motion(self, win, x, y, modifiers):
nx, ny = win.to_normalized_pos(x, y)
ny = 1.0 - ny
if self.current_drag:
touch = self.current_drag
touch.move([nx, ny])
touch.update_graphics(win)
self.waiting_event.append(('update', touch))
elif self.alt_touch is not None and 'alt' not in modifiers:
# alt just released ?
is_double_tap = 'shift' in modifiers
self.create_touch(win, nx, ny, is_double_tap, True, [])
def on_mouse_press(self, win, x, y, button, modifiers):
if self.test_activity():
return
nx, ny = win.to_normalized_pos(x, y)
ny = 1.0 - ny
found_touch = self.find_touch(win, nx, ny)
if found_touch:
self.current_drag = found_touch
else:
is_double_tap = 'shift' in modifiers
do_graphics = (
not self.disable_multitouch
and (button != 'left' or 'ctrl' in modifiers)
)
touch = self.create_touch(
win, nx, ny, is_double_tap, do_graphics, button
)
if 'alt' in modifiers:
self.alt_touch = touch
self.current_drag = None
def on_mouse_release(self, win, x, y, button, modifiers):
if button == 'all':
# Special case, if button is all,
# then remove all the current touches.
for touch in list(self.touches.values()):
self.remove_touch(win, touch)
self.current_drag = None
touch = self.current_drag
if touch:
not_right = button in (
'left',
'scrollup', 'scrolldown',
'scrollleft', 'scrollright'
)
not_ctrl = 'ctrl' not in modifiers
not_multi = (
self.disable_multitouch
or 'multitouch_sim' not in touch.profile
or not touch.multitouch_sim
)
if not_right and not_ctrl or not_multi:
self.remove_touch(win, touch)
self.current_drag = None
else:
touch.update_graphics(win, True)
if self.alt_touch:
self.remove_touch(win, self.alt_touch)
self.alt_touch = None
def update_touch_graphics(self, win, *args):
for touch in self.touches.values():
touch.update_graphics(win)
def begin_or_update_hover_event(self, win, *args):
etype = 'update' if self.hover_event else 'begin'
self.create_hover(win, etype)
def begin_hover_event(self, win, *args):
if not self.hover_event:
self.create_hover(win, 'begin')
def update_hover_event(self, win, *args):
if self.hover_event:
self.create_hover(win, 'update')
def end_hover_event(self, win, *args):
if self.hover_event:
self.create_hover(win, 'end')
def update(self, dispatch_fn):
'''Update the mouse provider (pop event from the queue)'''
try:
while True:
event = self.waiting_event.popleft()
dispatch_fn(*event)
except IndexError:
pass
# registers
MotionEventFactory.register('mouse', MouseMotionEventProvider)

View file

@ -0,0 +1,383 @@
'''
Native support for Multitouch devices on Linux, using libmtdev.
===============================================================
The Mtdev project is a part of the Ubuntu Maverick multitouch architecture.
You can read more on http://wiki.ubuntu.com/Multitouch
To configure MTDev, it's preferable to use probesysfs providers.
Check :py:class:`~kivy.input.providers.probesysfs` for more information.
Otherwise, add this to your configuration::
[input]
# devicename = hidinput,/dev/input/eventXX
acert230h = mtdev,/dev/input/event2
.. note::
You must have read access to the input event.
You can use a custom range for the X, Y and pressure values.
On some drivers, the range reported is invalid.
To fix that, you can add these options to the argument line:
* invert_x : 1 to invert X axis
* invert_y : 1 to invert Y axis
* min_position_x : X minimum
* max_position_x : X maximum
* min_position_y : Y minimum
* max_position_y : Y maximum
* min_pressure : pressure minimum
* max_pressure : pressure maximum
* min_touch_major : width shape minimum
* max_touch_major : width shape maximum
* min_touch_minor : width shape minimum
* max_touch_minor : height shape maximum
* rotation : 0,90,180 or 270 to rotate
An inverted display configuration will look like this::
[input]
# example for inverting touch events
display = mtdev,/dev/input/event0,invert_x=1,invert_y=1
'''
__all__ = ('MTDMotionEventProvider', 'MTDMotionEvent')
import os
import os.path
import time
from kivy.input.motionevent import MotionEvent
from kivy.input.shape import ShapeRect
class MTDMotionEvent(MotionEvent):
def __init__(self, *args, **kwargs):
kwargs.setdefault('is_touch', True)
kwargs.setdefault('type_id', 'touch')
super().__init__(*args, **kwargs)
def depack(self, args):
if 'x' in args:
self.sx = args['x']
else:
self.sx = -1
if 'y' in args:
self.sy = args['y']
else:
self.sy = -1
self.profile = ['pos']
if 'size_w' in args and 'size_h' in args:
self.shape = ShapeRect()
self.shape.width = args['size_w']
self.shape.height = args['size_h']
self.profile.append('shape')
if 'pressure' in args:
self.pressure = args['pressure']
self.profile.append('pressure')
super().depack(args)
def __str__(self):
i, sx, sy, d = (self.id, self.sx, self.sy, self.device)
return '<MTDMotionEvent id=%d pos=(%f, %f) device=%s>' % (i, sx, sy, d)
if 'KIVY_DOC' in os.environ:
# documentation hack
MTDMotionEventProvider = None
else:
import threading
import collections
from kivy.lib.mtdev import Device, \
MTDEV_TYPE_EV_ABS, MTDEV_CODE_SLOT, MTDEV_CODE_POSITION_X, \
MTDEV_CODE_POSITION_Y, MTDEV_CODE_PRESSURE, \
MTDEV_CODE_TOUCH_MAJOR, MTDEV_CODE_TOUCH_MINOR, \
MTDEV_CODE_TRACKING_ID, MTDEV_ABS_POSITION_X, \
MTDEV_ABS_POSITION_Y, MTDEV_ABS_TOUCH_MINOR, \
MTDEV_ABS_TOUCH_MAJOR
from kivy.input.provider import MotionEventProvider
from kivy.input.factory import MotionEventFactory
from kivy.logger import Logger
class MTDMotionEventProvider(MotionEventProvider):
options = ('min_position_x', 'max_position_x',
'min_position_y', 'max_position_y',
'min_pressure', 'max_pressure',
'min_touch_major', 'max_touch_major',
'min_touch_minor', 'max_touch_minor',
'invert_x', 'invert_y',
'rotation')
def __init__(self, device, args):
super(MTDMotionEventProvider, self).__init__(device, args)
self._device = None
self.input_fn = None
self.default_ranges = dict()
# split arguments
args = args.split(',')
if not args:
Logger.error('MTD: No filename pass to MTD configuration')
Logger.error('MTD: Use /dev/input/event0 for example')
return
# read filename
self.input_fn = args[0]
Logger.info('MTD: Read event from <%s>' % self.input_fn)
# read parameters
for arg in args[1:]:
if arg == '':
continue
arg = arg.split('=')
# ensure it's a key = value
if len(arg) != 2:
err = 'MTD: Bad parameter %s: Not in key=value format' %\
arg
Logger.error(err)
continue
# ensure the key exist
key, value = arg
if key not in MTDMotionEventProvider.options:
Logger.error('MTD: unknown %s option' % key)
continue
# ensure the value
try:
self.default_ranges[key] = int(value)
except ValueError:
err = 'MTD: invalid value %s for option %s' % (key, value)
Logger.error(err)
continue
# all good!
Logger.info('MTD: Set custom %s to %d' % (key, int(value)))
if 'rotation' not in self.default_ranges:
self.default_ranges['rotation'] = 0
elif self.default_ranges['rotation'] not in (0, 90, 180, 270):
Logger.error('HIDInput: invalid rotation value ({})'.format(
self.default_ranges['rotation']))
self.default_ranges['rotation'] = 0
def start(self):
if self.input_fn is None:
return
self.uid = 0
self.queue = collections.deque()
self.thread = threading.Thread(
name=self.__class__.__name__,
target=self._thread_run,
kwargs=dict(
queue=self.queue,
input_fn=self.input_fn,
device=self.device,
default_ranges=self.default_ranges))
self.thread.daemon = True
self.thread.start()
def _thread_run(self, **kwargs):
input_fn = kwargs.get('input_fn')
queue = kwargs.get('queue')
device = kwargs.get('device')
drs = kwargs.get('default_ranges').get
touches = {}
touches_sent = []
point = {}
l_points = {}
def assign_coord(point, value, invert, coords):
cx, cy = coords
if invert:
value = 1. - value
if rotation == 0:
point[cx] = value
elif rotation == 90:
point[cy] = value
elif rotation == 180:
point[cx] = 1. - value
elif rotation == 270:
point[cy] = 1. - value
def process(points):
for args in points:
# this can happen if we have a touch going on already at
# the start of the app
if 'id' not in args:
continue
tid = args['id']
try:
touch = touches[tid]
except KeyError:
touch = MTDMotionEvent(device, tid, args)
touches[touch.id] = touch
touch.move(args)
action = 'update'
if tid not in touches_sent:
action = 'begin'
touches_sent.append(tid)
if 'delete' in args:
action = 'end'
del args['delete']
del touches[touch.id]
touches_sent.remove(tid)
touch.update_time_end()
queue.append((action, touch))
def normalize(value, vmin, vmax):
try:
return (value - vmin) / float(vmax - vmin)
except ZeroDivisionError: # it's both in py2 and py3
return (value - vmin)
# open mtdev device
_fn = input_fn
_slot = 0
try:
_device = Device(_fn)
except OSError as e:
if e.errno == 13: # Permission denied
Logger.warn(
'MTD: Unable to open device "{0}". Please ensure you'
' have the appropriate permissions.'.format(_fn))
return
else:
raise
_changes = set()
# prepare some vars to get limit of some component
ab = _device.get_abs(MTDEV_ABS_POSITION_X)
range_min_position_x = drs('min_position_x', ab.minimum)
range_max_position_x = drs('max_position_x', ab.maximum)
Logger.info('MTD: <%s> range position X is %d - %d' %
(_fn, range_min_position_x, range_max_position_x))
ab = _device.get_abs(MTDEV_ABS_POSITION_Y)
range_min_position_y = drs('min_position_y', ab.minimum)
range_max_position_y = drs('max_position_y', ab.maximum)
Logger.info('MTD: <%s> range position Y is %d - %d' %
(_fn, range_min_position_y, range_max_position_y))
ab = _device.get_abs(MTDEV_ABS_TOUCH_MAJOR)
range_min_major = drs('min_touch_major', ab.minimum)
range_max_major = drs('max_touch_major', ab.maximum)
Logger.info('MTD: <%s> range touch major is %d - %d' %
(_fn, range_min_major, range_max_major))
ab = _device.get_abs(MTDEV_ABS_TOUCH_MINOR)
range_min_minor = drs('min_touch_minor', ab.minimum)
range_max_minor = drs('max_touch_minor', ab.maximum)
Logger.info('MTD: <%s> range touch minor is %d - %d' %
(_fn, range_min_minor, range_max_minor))
range_min_pressure = drs('min_pressure', 0)
range_max_pressure = drs('max_pressure', 255)
Logger.info('MTD: <%s> range pressure is %d - %d' %
(_fn, range_min_pressure, range_max_pressure))
invert_x = int(bool(drs('invert_x', 0)))
invert_y = int(bool(drs('invert_y', 0)))
Logger.info('MTD: <%s> axes inversion: X is %d, Y is %d' %
(_fn, invert_x, invert_y))
rotation = drs('rotation', 0)
Logger.info('MTD: <%s> rotation set to %d' %
(_fn, rotation))
failures = 0
while _device:
# if device have disconnected lets try to connect
if failures > 1000:
Logger.info('MTD: <%s> input device disconnected' % _fn)
while not os.path.exists(_fn):
time.sleep(0.05)
# input device is back online let's recreate device
_device.close()
_device = Device(_fn)
Logger.info('MTD: <%s> input device reconnected' % _fn)
failures = 0
continue
# idle as much as we can.
while _device.idle(1000):
continue
# got data, read all without redoing idle
while True:
data = _device.get()
if data is None:
failures += 1
break
failures = 0
# set the working slot
if data.type == MTDEV_TYPE_EV_ABS and \
data.code == MTDEV_CODE_SLOT:
_slot = data.value
continue
# fill the slot
if not (_slot in l_points):
l_points[_slot] = dict()
point = l_points[_slot]
ev_value = data.value
ev_code = data.code
if ev_code == MTDEV_CODE_POSITION_X:
val = normalize(ev_value,
range_min_position_x,
range_max_position_x)
assign_coord(point, val, invert_x, 'xy')
elif ev_code == MTDEV_CODE_POSITION_Y:
val = 1. - normalize(ev_value,
range_min_position_y,
range_max_position_y)
assign_coord(point, val, invert_y, 'yx')
elif ev_code == MTDEV_CODE_PRESSURE:
point['pressure'] = normalize(ev_value,
range_min_pressure,
range_max_pressure)
elif ev_code == MTDEV_CODE_TOUCH_MAJOR:
point['size_w'] = normalize(ev_value,
range_min_major,
range_max_major)
elif ev_code == MTDEV_CODE_TOUCH_MINOR:
point['size_h'] = normalize(ev_value,
range_min_minor,
range_max_minor)
elif ev_code == MTDEV_CODE_TRACKING_ID:
if ev_value == -1:
point['delete'] = True
# force process of changes here, as the slot can be
# reused.
_changes.add(_slot)
process([l_points[x] for x in _changes])
_changes.clear()
continue
else:
point['id'] = ev_value
else:
# unrecognized command, ignore.
continue
_changes.add(_slot)
# push all changes
if _changes:
process([l_points[x] for x in _changes])
_changes.clear()
def update(self, dispatch_fn):
# dispatch all event from threads
try:
while True:
event_type, touch = self.queue.popleft()
dispatch_fn(event_type, touch)
except:
pass
MotionEventFactory.register('mtdev', MTDMotionEventProvider)

View file

@ -0,0 +1,254 @@
'''
Auto Create Input Provider Config Entry for Available MT Hardware (linux only).
===============================================================================
Thanks to Marc Tardif for the probing code, taken from scan-for-mt-device.
The device discovery is done by this provider. However, the reading of
input can be performed by other providers like: hidinput, mtdev and
linuxwacom. mtdev is used prior to other providers. For more
information about mtdev, check :py:class:`~kivy.input.providers.mtdev`.
Here is an example of auto creation::
[input]
# using mtdev
device_%(name)s = probesysfs,provider=mtdev
# using hidinput
device_%(name)s = probesysfs,provider=hidinput
# using mtdev with a match on name
device_%(name)s = probesysfs,provider=mtdev,match=acer
# using hidinput with custom parameters to hidinput (all on one line)
%(name)s = probesysfs,
provider=hidinput,param=min_pressure=1,param=max_pressure=99
# you can also match your wacom touchscreen
touch = probesysfs,match=E3 Finger,provider=linuxwacom,
select_all=1,param=mode=touch
# and your wacom pen
pen = probesysfs,match=E3 Pen,provider=linuxwacom,
select_all=1,param=mode=pen
By default, ProbeSysfs module will enumerate hardware from the /sys/class/input
device, and configure hardware with ABS_MT_POSITION_X capability. But for
example, the wacom screen doesn't support this capability. You can prevent this
behavior by putting select_all=1 in your config line. Add use_mouse=1 to also
include touchscreen hardware that offers core pointer functionality.
'''
__all__ = ('ProbeSysfsHardwareProbe', )
import os
from os.path import sep
if 'KIVY_DOC' in os.environ:
ProbeSysfsHardwareProbe = None
else:
import ctypes
from re import match, IGNORECASE
from glob import glob
from subprocess import Popen, PIPE
from kivy.logger import Logger
from kivy.input.provider import MotionEventProvider
from kivy.input.providers.mouse import MouseMotionEventProvider
from kivy.input.factory import MotionEventFactory
from kivy.config import _is_rpi
EventLoop = None
# See linux/input.h
ABS_MT_POSITION_X = 0x35
_cache_input = None
_cache_xinput = None
class Input(object):
def __init__(self, path):
query_xinput()
self.path = path
@property
def device(self):
base = os.path.basename(self.path)
return os.path.join("/dev", "input", base)
@property
def name(self):
path = os.path.join(self.path, "device", "name")
return read_line(path)
def get_capabilities(self):
path = os.path.join(self.path, "device", "capabilities", "abs")
line = "0"
try:
line = read_line(path)
except (IOError, OSError):
return []
capabilities = []
long_bit = ctypes.sizeof(ctypes.c_long) * 8
for i, word in enumerate(line.split(" ")):
word = int(word, 16)
subcapabilities = [bool(word & 1 << i)
for i in range(long_bit)]
capabilities[:0] = subcapabilities
return capabilities
def has_capability(self, capability):
capabilities = self.get_capabilities()
return len(capabilities) > capability and capabilities[capability]
@property
def is_mouse(self):
return self.device in _cache_xinput
def getout(*args):
try:
return Popen(args, stdout=PIPE).communicate()[0]
except OSError:
return ''
def query_xinput():
global _cache_xinput
if _cache_xinput is None:
_cache_xinput = []
devids = getout('xinput', '--list', '--id-only')
for did in devids.splitlines():
devprops = getout('xinput', '--list-props', did)
evpath = None
for prop in devprops.splitlines():
prop = prop.strip()
if (prop.startswith(b'Device Enabled') and
prop.endswith(b'0')):
evpath = None
break
if prop.startswith(b'Device Node'):
try:
evpath = prop.split('"')[1]
except Exception:
evpath = None
if evpath:
_cache_xinput.append(evpath)
def get_inputs(path):
global _cache_input
if _cache_input is None:
event_glob = os.path.join(path, "event*")
_cache_input = [Input(x) for x in glob(event_glob)]
return _cache_input
def read_line(path):
f = open(path)
try:
return f.readline().strip()
finally:
f.close()
class ProbeSysfsHardwareProbe(MotionEventProvider):
def __new__(self, device, args):
# hack to not return an instance of this provider.
# :)
instance = super(ProbeSysfsHardwareProbe, self).__new__(self)
instance.__init__(device, args)
def __init__(self, device, args):
super(ProbeSysfsHardwareProbe, self).__init__(device, args)
self.provider = 'mtdev'
self.match = None
self.input_path = '/sys/class/input'
self.select_all = True if _is_rpi else False
self.use_mouse = False
self.use_regex = False
self.args = []
args = args.split(',')
for arg in args:
if arg == '':
continue
arg = arg.split('=', 1)
# ensure it's a key = value
if len(arg) != 2:
Logger.error('ProbeSysfs: invalid parameters %s, not'
' key=value format' % arg)
continue
key, value = arg
if key == 'match':
self.match = value
elif key == 'provider':
self.provider = value
elif key == 'use_regex':
self.use_regex = bool(int(value))
elif key == 'select_all':
self.select_all = bool(int(value))
elif key == 'use_mouse':
self.use_mouse = bool(int(value))
elif key == 'param':
self.args.append(value)
else:
Logger.error('ProbeSysfs: unknown %s option' % key)
continue
self.probe()
def should_use_mouse(self):
return (self.use_mouse or
not any(p for p in EventLoop.input_providers
if isinstance(p, MouseMotionEventProvider)))
def probe(self):
global EventLoop
from kivy.base import EventLoop
inputs = get_inputs(self.input_path)
Logger.debug('ProbeSysfs: using probesysfs!')
use_mouse = self.should_use_mouse()
if not self.select_all:
inputs = [x for x in inputs if
x.has_capability(ABS_MT_POSITION_X) and
(use_mouse or not x.is_mouse)]
for device in inputs:
Logger.debug('ProbeSysfs: found device: %s at %s' % (
device.name, device.device))
# must ignore ?
if self.match:
if self.use_regex:
if not match(self.match, device.name, IGNORECASE):
Logger.debug('ProbeSysfs: device not match the'
' rule in config, ignoring.')
continue
else:
if self.match not in device.name:
continue
Logger.info('ProbeSysfs: device match: %s' % device.device)
d = device.device
devicename = self.device % dict(name=d.split(sep)[-1])
provider = MotionEventFactory.get(self.provider)
if provider is None:
Logger.info('ProbeSysfs: Unable to find provider %s' %
self.provider)
Logger.info('ProbeSysfs: fallback on hidinput')
provider = MotionEventFactory.get('hidinput')
if provider is None:
Logger.critical('ProbeSysfs: no input provider found'
' to handle this device !')
continue
instance = provider(devicename, '%s,%s' % (
device.device, ','.join(self.args)))
if instance:
EventLoop.add_input_provider(instance)
MotionEventFactory.register('probesysfs', ProbeSysfsHardwareProbe)

View file

@ -0,0 +1,326 @@
'''
TUIO Input Provider
===================
TUIO is the de facto standard network protocol for the transmission of
touch and fiducial information between a server and a client. To learn
more about TUIO (which is itself based on the OSC protocol), please
refer to http://tuio.org -- The specification should be of special
interest.
Configure a TUIO provider in the config.ini
-------------------------------------------
The TUIO provider can be configured in the configuration file in the
``[input]`` section::
[input]
# name = tuio,<ip>:<port>
multitouchtable = tuio,192.168.0.1:3333
Configure a TUIO provider in the App
------------------------------------
You must add the provider before your application is run, like this::
from kivy.app import App
from kivy.config import Config
class TestApp(App):
def build(self):
Config.set('input', 'multitouchscreen1', 'tuio,0.0.0.0:3333')
# You can also add a second TUIO listener
# Config.set('input', 'source2', 'tuio,0.0.0.0:3334')
# Then do the usual things
# ...
return
'''
__all__ = ('TuioMotionEventProvider', 'Tuio2dCurMotionEvent',
'Tuio2dObjMotionEvent')
from kivy.logger import Logger
from functools import partial
from collections import deque
from kivy.input.provider import MotionEventProvider
from kivy.input.factory import MotionEventFactory
from kivy.input.motionevent import MotionEvent
from kivy.input.shape import ShapeRect
class TuioMotionEventProvider(MotionEventProvider):
'''The TUIO provider listens to a socket and handles some of the incoming
OSC messages:
* /tuio/2Dcur
* /tuio/2Dobj
You can easily extend the provider to handle new TUIO paths like so::
# Create a class to handle the new TUIO type/path
# Replace NEWPATH with the pathname you want to handle
class TuioNEWPATHMotionEvent(MotionEvent):
def depack(self, args):
# In this method, implement 'unpacking' for the received
# arguments. you basically translate from TUIO args to Kivy
# MotionEvent variables. If all you receive are x and y
# values, you can do it like this:
if len(args) == 2:
self.sx, self.sy = args
self.profile = ('pos', )
self.sy = 1 - self.sy
super().depack(args)
# Register it with the TUIO MotionEvent provider.
# You obviously need to replace the PATH placeholders appropriately.
TuioMotionEventProvider.register('/tuio/PATH', TuioNEWPATHMotionEvent)
.. note::
The class name is of no technical importance. Your class will be
associated with the path that you pass to the ``register()``
function. To keep things simple, you should name your class after the
path that it handles, though.
'''
__handlers__ = {}
def __init__(self, device, args):
super().__init__(device, args)
args = args.split(',')
if len(args) == 0:
Logger.error('Tuio: Invalid configuration for TUIO provider')
Logger.error('Tuio: Format must be ip:port (eg. 127.0.0.1:3333)')
err = 'Tuio: Current configuration is <%s>' % (str(','.join(args)))
Logger.error(err)
return
ipport = args[0].split(':')
if len(ipport) != 2:
Logger.error('Tuio: Invalid configuration for TUIO provider')
Logger.error('Tuio: Format must be ip:port (eg. 127.0.0.1:3333)')
err = 'Tuio: Current configuration is <%s>' % (str(','.join(args)))
Logger.error(err)
return
self.ip, self.port = args[0].split(':')
self.port = int(self.port)
self.handlers = {}
self.oscid = None
self.tuio_event_q = deque()
self.touches = {}
@staticmethod
def register(oscpath, classname):
'''Register a new path to handle in TUIO provider'''
TuioMotionEventProvider.__handlers__[oscpath] = classname
@staticmethod
def unregister(oscpath, classname):
'''Unregister a path to stop handling it in the TUIO provider'''
if oscpath in TuioMotionEventProvider.__handlers__:
del TuioMotionEventProvider.__handlers__[oscpath]
@staticmethod
def create(oscpath, **kwargs):
'''Create a touch event from a TUIO path'''
if oscpath not in TuioMotionEventProvider.__handlers__:
raise Exception('Unknown %s touch path' % oscpath)
return TuioMotionEventProvider.__handlers__[oscpath](**kwargs)
def start(self):
'''Start the TUIO provider'''
try:
from oscpy.server import OSCThreadServer
except ImportError:
Logger.info(
'Please install the oscpy python module to use the TUIO '
'provider.'
)
raise
self.oscid = osc = OSCThreadServer()
osc.listen(self.ip, self.port, default=True)
for oscpath in TuioMotionEventProvider.__handlers__:
self.touches[oscpath] = {}
osc.bind(oscpath, partial(self._osc_tuio_cb, oscpath))
def stop(self):
'''Stop the TUIO provider'''
self.oscid.stop_all()
def update(self, dispatch_fn):
'''Update the TUIO provider (pop events from the queue)'''
# read the Queue with event
while True:
try:
value = self.tuio_event_q.pop()
except IndexError:
# queue is empty, we're done for now
return
self._update(dispatch_fn, value)
def _osc_tuio_cb(self, oscpath, address, *args):
self.tuio_event_q.appendleft([oscpath, address, args])
def _update(self, dispatch_fn, value):
oscpath, command, args = value
# verify commands
if command not in [b'alive', b'set']:
return
# move or create a new touch
if command == b'set':
id = args[0]
if id not in self.touches[oscpath]:
# new touch
touch = TuioMotionEventProvider.__handlers__[oscpath](
self.device, id, args[1:])
self.touches[oscpath][id] = touch
dispatch_fn('begin', touch)
else:
# update a current touch
touch = self.touches[oscpath][id]
touch.move(args[1:])
dispatch_fn('update', touch)
# alive event, check for deleted touch
if command == b'alive':
alives = args
to_delete = []
for id in self.touches[oscpath]:
if id not in alives:
# touch up
touch = self.touches[oscpath][id]
if touch not in to_delete:
to_delete.append(touch)
for touch in to_delete:
dispatch_fn('end', touch)
del self.touches[oscpath][touch.id]
class TuioMotionEvent(MotionEvent):
'''Abstraction for TUIO touches/fiducials.
Depending on the tracking software you use (e.g. Movid, CCV, etc.) and its
TUIO implementation, the TuioMotionEvent object can support multiple
profiles such as:
* Fiducial ID: profile name 'markerid', attribute ``.fid``
* Position: profile name 'pos', attributes ``.x``, ``.y``
* Angle: profile name 'angle', attribute ``.a``
* Velocity vector: profile name 'mov', attributes ``.X``, ``.Y``
* Rotation velocity: profile name 'rot', attribute ``.A``
* Motion acceleration: profile name 'motacc', attribute ``.m``
* Rotation acceleration: profile name 'rotacc', attribute ``.r``
'''
__attrs__ = ('a', 'b', 'c', 'X', 'Y', 'Z', 'A', 'B', 'C', 'm', 'r')
def __init__(self, *args, **kwargs):
kwargs.setdefault('is_touch', True)
kwargs.setdefault('type_id', 'touch')
super().__init__(*args, **kwargs)
# Default argument for TUIO touches
self.a = 0.0
self.b = 0.0
self.c = 0.0
self.X = 0.0
self.Y = 0.0
self.Z = 0.0
self.A = 0.0
self.B = 0.0
self.C = 0.0
self.m = 0.0
self.r = 0.0
angle = property(lambda self: self.a)
mot_accel = property(lambda self: self.m)
rot_accel = property(lambda self: self.r)
xmot = property(lambda self: self.X)
ymot = property(lambda self: self.Y)
zmot = property(lambda self: self.Z)
class Tuio2dCurMotionEvent(TuioMotionEvent):
'''A 2dCur TUIO touch.'''
def depack(self, args):
if len(args) < 5:
self.sx, self.sy = list(map(float, args[0:2]))
self.profile = ('pos', )
elif len(args) == 5:
self.sx, self.sy, self.X, self.Y, self.m = list(map(float,
args[0:5]))
self.Y = -self.Y
self.profile = ('pos', 'mov', 'motacc')
else:
self.sx, self.sy, self.X, self.Y = list(map(float, args[0:4]))
self.m, width, height = list(map(float, args[4:7]))
self.Y = -self.Y
self.profile = ('pos', 'mov', 'motacc', 'shape')
if self.shape is None:
self.shape = ShapeRect()
self.shape.width = width
self.shape.height = height
self.sy = 1 - self.sy
super().depack(args)
class Tuio2dObjMotionEvent(TuioMotionEvent):
'''A 2dObj TUIO object.
'''
def depack(self, args):
if len(args) < 5:
self.sx, self.sy = args[0:2]
self.profile = ('pos', )
elif len(args) == 9:
self.fid, self.sx, self.sy, self.a, self.X, self.Y = args[:6]
self.A, self.m, self.r = args[6:9]
self.Y = -self.Y
self.profile = ('markerid', 'pos', 'angle', 'mov', 'rot',
'motacc', 'rotacc')
else:
self.fid, self.sx, self.sy, self.a, self.X, self.Y = args[:6]
self.A, self.m, self.r, width, height = args[6:11]
self.Y = -self.Y
self.profile = ('markerid', 'pos', 'angle', 'mov', 'rot', 'rotacc',
'acc', 'shape')
if self.shape is None:
self.shape = ShapeRect()
self.shape.width = width
self.shape.height = height
self.sy = 1 - self.sy
super().depack(args)
class Tuio2dBlbMotionEvent(TuioMotionEvent):
'''A 2dBlb TUIO object.
# FIXME 3d shape are not supported
/tuio/2Dobj set s i x y a X Y A m r
/tuio/2Dblb set s x y a w h f X Y A m r
'''
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.profile = ('pos', 'angle', 'mov', 'rot', 'rotacc', 'acc', 'shape')
def depack(self, args):
self.sx, self.sy, self.a, self.X, self.Y, sw, sh, sd, \
self.A, self.m, self.r = args
self.Y = -self.Y
if self.shape is None:
self.shape = ShapeRect()
self.shape.width = sw
self.shape.height = sh
self.sy = 1 - self.sy
super().depack(args)
# registers
TuioMotionEventProvider.register(b'/tuio/2Dcur', Tuio2dCurMotionEvent)
TuioMotionEventProvider.register(b'/tuio/2Dobj', Tuio2dObjMotionEvent)
TuioMotionEventProvider.register(b'/tuio/2Dblb', Tuio2dBlbMotionEvent)
MotionEventFactory.register('tuio', TuioMotionEventProvider)

View file

@ -0,0 +1,162 @@
'''
Common definitions for a Windows provider
=========================================
This file provides common definitions for constants used by WM_Touch / WM_Pen.
'''
import os
WM_MOUSEFIRST = 512
WM_MOUSEMOVE = 512
WM_LBUTTONDOWN = 513
WM_LBUTTONUP = 514
WM_LBUTTONDBLCLK = 515
WM_RBUTTONDOWN = 516
WM_RBUTTONUP = 517
WM_RBUTTONDBLCLK = 518
WM_MBUTTONDOWN = 519
WM_MBUTTONUP = 520
WM_MBUTTONDBLCLK = 521
WM_MOUSEWHEEL = 522
WM_MOUSELAST = 522
WM_DPICHANGED = 736
WM_GETDPISCALEDSIZE = 740
WM_NCCALCSIZE = 131
WM_TOUCH = 576
TOUCHEVENTF_MOVE = 1
TOUCHEVENTF_DOWN = 2
TOUCHEVENTF_UP = 4
PEN_OR_TOUCH_SIGNATURE = 0xFF515700
PEN_OR_TOUCH_MASK = 0xFFFFFF00
PEN_EVENT_TOUCH_MASK = 0x80
SM_CYCAPTION = 4
WM_TABLET_QUERYSYSTEMGESTURE = 0x000002CC
TABLET_DISABLE_PRESSANDHOLD = 0x00000001
TABLET_DISABLE_PENTAPFEEDBACK = 0x00000008
TABLET_DISABLE_PENBARRELFEEDBACK = 0x00000010
TABLET_DISABLE_TOUCHUIFORCEON = 0x00000100
TABLET_DISABLE_TOUCHUIFORCEOFF = 0x00000200
TABLET_DISABLE_TOUCHSWITCH = 0x00008000
TABLET_DISABLE_FLICKS = 0x00010000
TABLET_ENABLE_FLICKSONCONTEXT = 0x00020000
TABLET_ENABLE_FLICKLEARNINGMODE = 0x00040000
TABLET_DISABLE_SMOOTHSCROLLING = 0x00080000
TABLET_DISABLE_FLICKFALLBACKKEYS = 0x00100000
GWL_WNDPROC = -4
QUERYSYSTEMGESTURE_WNDPROC = (
TABLET_DISABLE_PRESSANDHOLD |
TABLET_DISABLE_PENTAPFEEDBACK |
TABLET_DISABLE_PENBARRELFEEDBACK |
TABLET_DISABLE_SMOOTHSCROLLING |
TABLET_DISABLE_FLICKFALLBACKKEYS |
TABLET_DISABLE_TOUCHSWITCH |
TABLET_DISABLE_FLICKS)
if 'KIVY_DOC' not in os.environ:
from ctypes.wintypes import (ULONG, HANDLE, DWORD, LONG, UINT,
WPARAM, LPARAM, BOOL, HWND, POINT,
RECT as RECT_BASE)
from ctypes import (windll, WINFUNCTYPE, POINTER,
c_int, c_longlong, c_void_p, Structure,
sizeof, byref, cast)
class RECT(RECT_BASE):
x = property(lambda self: self.left)
y = property(lambda self: self.top)
w = property(lambda self: self.right - self.left)
h = property(lambda self: self.bottom - self.top)
# check availability of RegisterTouchWindow
if not hasattr(windll.user32, 'RegisterTouchWindow'):
raise Exception('Unsupported Window version')
LRESULT = LPARAM
WNDPROC = WINFUNCTYPE(LRESULT, HWND, UINT, WPARAM, LPARAM)
class TOUCHINPUT(Structure):
_fields_ = [
('x', LONG),
('y', LONG),
('pSource', HANDLE),
('id', DWORD),
('flags', DWORD),
('mask', DWORD),
('time', DWORD),
('extraInfo', POINTER(ULONG)),
('size_x', DWORD),
('size_y', DWORD)]
def size(self):
return (self.size_x, self.size_y)
def screen_x(self):
return self.x / 100.0
def screen_y(self):
return self.y / 100.0
def _event_type(self):
if self.flags & TOUCHEVENTF_MOVE:
return 'update'
if self.flags & TOUCHEVENTF_DOWN:
return 'begin'
if self.flags & TOUCHEVENTF_UP:
return 'end'
event_type = property(_event_type)
def SetWindowLong_WndProc_wrapper_generator(func):
def _closure(hWnd, wndProc):
oldAddr = func(hWnd, GWL_WNDPROC, cast(wndProc, c_void_p).value)
return cast(c_void_p(oldAddr), WNDPROC)
return _closure
try:
LONG_PTR = c_longlong
windll.user32.SetWindowLongPtrW.restype = LONG_PTR
windll.user32.SetWindowLongPtrW.argtypes = [HWND, c_int, LONG_PTR]
SetWindowLong_WndProc_wrapper = \
SetWindowLong_WndProc_wrapper_generator(
windll.user32.SetWindowLongPtrW)
except AttributeError:
windll.user32.SetWindowLongW.restype = LONG
windll.user32.SetWindowLongW.argtypes = [HWND, c_int, LONG]
SetWindowLong_WndProc_wrapper = \
SetWindowLong_WndProc_wrapper_generator(
windll.user32.SetWindowLongW)
windll.user32.GetMessageExtraInfo.restype = LPARAM
windll.user32.GetMessageExtraInfo.argtypes = []
windll.user32.GetClientRect.restype = BOOL
windll.user32.GetClientRect.argtypes = [HANDLE, POINTER(RECT_BASE)]
windll.user32.GetWindowRect.restype = BOOL
windll.user32.GetWindowRect.argtypes = [HANDLE, POINTER(RECT_BASE)]
windll.user32.CallWindowProcW.restype = LRESULT
windll.user32.CallWindowProcW.argtypes = [WNDPROC, HWND, UINT, WPARAM,
LPARAM]
windll.user32.GetActiveWindow.restype = HWND
windll.user32.GetActiveWindow.argtypes = []
windll.user32.RegisterTouchWindow.restype = BOOL
windll.user32.RegisterTouchWindow.argtypes = [HWND, ULONG]
windll.user32.UnregisterTouchWindow.restype = BOOL
windll.user32.UnregisterTouchWindow.argtypes = [HWND]
windll.user32.GetTouchInputInfo.restype = BOOL
windll.user32.GetTouchInputInfo.argtypes = [HANDLE, UINT,
POINTER(TOUCHINPUT), c_int]
windll.user32.GetSystemMetrics.restype = c_int
windll.user32.GetSystemMetrics.argtypes = [c_int]
windll.user32.ClientToScreen.restype = BOOL
windll.user32.ClientToScreen.argtypes = [HWND, POINTER(POINT)]
try:
windll.user32.GetDpiForWindow.restype = UINT
windll.user32.GetDpiForWindow.argtypes = [HWND]
except AttributeError:
pass

View file

@ -0,0 +1,121 @@
'''
Support for WM_PEN messages (Windows platform)
==============================================
'''
__all__ = ('WM_PenProvider', 'WM_Pen')
import os
from kivy.input.providers.wm_common import RECT, PEN_OR_TOUCH_MASK, \
PEN_OR_TOUCH_SIGNATURE, PEN_EVENT_TOUCH_MASK, WM_LBUTTONDOWN, \
WM_MOUSEMOVE, WM_LBUTTONUP, WM_TABLET_QUERYSYSTEMGESTURE, \
QUERYSYSTEMGESTURE_WNDPROC, WNDPROC, SetWindowLong_WndProc_wrapper
from kivy.input.motionevent import MotionEvent
class WM_Pen(MotionEvent):
'''MotionEvent representing the WM_Pen event. Supports the pos profile.'''
def __init__(self, *args, **kwargs):
kwargs.setdefault('is_touch', True)
kwargs.setdefault('type_id', 'touch')
super().__init__(*args, **kwargs)
self.profile = ['pos']
def depack(self, args):
self.sx, self.sy = args[0], args[1]
super().depack(args)
def __str__(self):
i, u, s, d = (self.id, self.uid, str(self.spos), self.device)
return '<WMPen id:%d uid:%d pos:%s device:%s>' % (i, u, s, d)
if 'KIVY_DOC' in os.environ:
# documentation hack
WM_PenProvider = None
else:
from collections import deque
from ctypes import windll, byref, c_int16, c_int
from kivy.input.provider import MotionEventProvider
from kivy.input.factory import MotionEventFactory
win_rect = RECT()
class WM_PenProvider(MotionEventProvider):
def _is_pen_message(self, msg):
info = windll.user32.GetMessageExtraInfo()
# It's a touch or a pen
if (info & PEN_OR_TOUCH_MASK) == PEN_OR_TOUCH_SIGNATURE:
if not info & PEN_EVENT_TOUCH_MASK:
return True
def _pen_handler(self, msg, wParam, lParam):
if msg not in (WM_LBUTTONDOWN, WM_MOUSEMOVE, WM_LBUTTONUP):
return
windll.user32.GetClientRect(self.hwnd, byref(win_rect))
x = c_int16(lParam & 0xffff).value / float(win_rect.w)
y = c_int16(lParam >> 16).value / float(win_rect.h)
y = abs(1.0 - y)
if msg == WM_LBUTTONDOWN:
self.pen_events.appendleft(('begin', x, y))
self.pen_status = True
if msg == WM_MOUSEMOVE and self.pen_status:
self.pen_events.appendleft(('update', x, y))
if msg == WM_LBUTTONUP:
self.pen_events.appendleft(('end', x, y))
self.pen_status = False
def _pen_wndProc(self, hwnd, msg, wParam, lParam):
if msg == WM_TABLET_QUERYSYSTEMGESTURE:
return QUERYSYSTEMGESTURE_WNDPROC
if self._is_pen_message(msg):
self._pen_handler(msg, wParam, lParam)
return 1
else:
return windll.user32.CallWindowProcW(self.old_windProc,
hwnd, msg, wParam, lParam)
def start(self):
self.uid = 0
self.pen = None
self.pen_status = None
self.pen_events = deque()
self.hwnd = windll.user32.GetActiveWindow()
# inject our own wndProc to handle messages
# before window manager does
self.new_windProc = WNDPROC(self._pen_wndProc)
self.old_windProc = SetWindowLong_WndProc_wrapper(
self.hwnd, self.new_windProc)
def update(self, dispatch_fn):
while True:
try:
etype, x, y = self.pen_events.pop()
except:
break
if etype == 'begin':
self.uid += 1
self.pen = WM_Pen(self.device, self.uid, [x, y])
elif etype == 'update':
self.pen.move([x, y])
elif etype == 'end':
self.pen.update_time_end()
dispatch_fn(etype, self.pen)
def stop(self):
self.pen = None
SetWindowLong_WndProc_wrapper(self.hwnd, self.old_windProc)
MotionEventFactory.register('wm_pen', WM_PenProvider)

View file

@ -0,0 +1,157 @@
'''
Support for WM_TOUCH messages (Windows platform)
================================================
'''
__all__ = ('WM_MotionEventProvider', 'WM_MotionEvent')
import os
from kivy.input.providers.wm_common import WNDPROC, \
SetWindowLong_WndProc_wrapper, RECT, POINT, WM_TABLET_QUERYSYSTEMGESTURE, \
QUERYSYSTEMGESTURE_WNDPROC, WM_TOUCH, WM_MOUSEMOVE, WM_MOUSELAST, \
TOUCHINPUT, PEN_OR_TOUCH_MASK, PEN_OR_TOUCH_SIGNATURE, PEN_EVENT_TOUCH_MASK
from kivy.input.motionevent import MotionEvent
from kivy.input.shape import ShapeRect
Window = None
class WM_MotionEvent(MotionEvent):
'''MotionEvent representing the WM_MotionEvent event.
Supports pos, shape and size profiles.
'''
__attrs__ = ('size', )
def __init__(self, *args, **kwargs):
kwargs.setdefault('is_touch', True)
kwargs.setdefault('type_id', 'touch')
super().__init__(*args, **kwargs)
self.profile = ('pos', 'shape', 'size')
def depack(self, args):
self.shape = ShapeRect()
self.sx, self.sy = args[0], args[1]
self.shape.width = args[2][0]
self.shape.height = args[2][1]
self.size = self.shape.width * self.shape.height
super().depack(args)
def __str__(self):
args = (self.id, self.uid, str(self.spos), self.device)
return '<WMMotionEvent id:%d uid:%d pos:%s device:%s>' % args
if 'KIVY_DOC' in os.environ:
# documentation hack
WM_MotionEventProvider = None
else:
from ctypes.wintypes import HANDLE
from ctypes import (windll, sizeof, byref)
from collections import deque
from kivy.input.provider import MotionEventProvider
from kivy.input.factory import MotionEventFactory
class WM_MotionEventProvider(MotionEventProvider):
def start(self):
global Window
if not Window:
from kivy.core.window import Window
self.touch_events = deque()
self.touches = {}
self.uid = 0
# get window handle, and register to receive WM_TOUCH messages
self.hwnd = windll.user32.GetActiveWindow()
windll.user32.RegisterTouchWindow(self.hwnd, 1)
# inject our own wndProc to handle messages
# before window manager does
self.new_windProc = WNDPROC(self._touch_wndProc)
self.old_windProc = SetWindowLong_WndProc_wrapper(
self.hwnd, self.new_windProc)
def update(self, dispatch_fn):
c_rect = RECT()
windll.user32.GetClientRect(self.hwnd, byref(c_rect))
pt = POINT(x=0, y=0)
windll.user32.ClientToScreen(self.hwnd, byref(pt))
x_offset, y_offset = pt.x, pt.y
usable_w, usable_h = float(c_rect.w), float(c_rect.h)
while True:
try:
t = self.touch_events.pop()
except:
break
# adjust x,y to window coordinates (0.0 to 1.0)
x = (t.screen_x() - x_offset) / usable_w
y = 1.0 - (t.screen_y() - y_offset) / usable_h
# actually dispatch input
if t.event_type == 'begin':
self.uid += 1
self.touches[t.id] = WM_MotionEvent(
self.device, self.uid, [x, y, t.size()])
dispatch_fn('begin', self.touches[t.id])
if t.event_type == 'update' and t.id in self.touches:
self.touches[t.id].move([x, y, t.size()])
dispatch_fn('update', self.touches[t.id])
if t.event_type == 'end' and t.id in self.touches:
touch = self.touches[t.id]
touch.move([x, y, t.size()])
touch.update_time_end()
dispatch_fn('end', touch)
del self.touches[t.id]
def stop(self):
windll.user32.UnregisterTouchWindow(self.hwnd)
self.new_windProc = SetWindowLong_WndProc_wrapper(
self.hwnd, self.old_windProc)
# we inject this wndProc into our main window, to process
# WM_TOUCH and mouse messages before the window manager does
def _touch_wndProc(self, hwnd, msg, wParam, lParam):
done = False
if msg == WM_TABLET_QUERYSYSTEMGESTURE:
return QUERYSYSTEMGESTURE_WNDPROC
if msg == WM_TOUCH:
done = self._touch_handler(msg, wParam, lParam)
if msg >= WM_MOUSEMOVE and msg <= WM_MOUSELAST:
done = self._mouse_handler(msg, wParam, lParam)
if not done:
return windll.user32.CallWindowProcW(self.old_windProc,
hwnd, msg, wParam,
lParam)
return 1
# this on pushes WM_TOUCH messages onto our event stack
def _touch_handler(self, msg, wParam, lParam):
touches = (TOUCHINPUT * wParam)()
windll.user32.GetTouchInputInfo(HANDLE(lParam),
wParam,
touches,
sizeof(TOUCHINPUT))
for i in range(wParam):
self.touch_events.appendleft(touches[i])
windll.user32.CloseTouchInputHandle(HANDLE(lParam))
return True
# filter fake mouse events, because touch and stylus
# also make mouse events
def _mouse_handler(self, msg, wparam, lParam):
info = windll.user32.GetMessageExtraInfo()
# its a touch or a pen
if (info & PEN_OR_TOUCH_MASK) == PEN_OR_TOUCH_SIGNATURE:
if info & PEN_EVENT_TOUCH_MASK:
return True
MotionEventFactory.register('wm_touch', WM_MotionEventProvider)