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,39 @@
# pylint: disable=W0611
'''
Input management
================
Our input system is wide and simple at the same time. We are currently able to
natively support :
* Windows multitouch events (pencil and finger)
* OS X touchpads
* Linux multitouch events (kernel and mtdev)
* Linux wacom drivers (pencil and finger)
* TUIO
All the input management is configurable in the Kivy :mod:`~kivy.config`. You
can easily use many multitouch devices in one Kivy application.
When the events have been read from the devices, they are dispatched through
a post processing module before being sent to your application. We also have
several default modules for :
* Double tap detection
* Decreasing jittering
* Decreasing the inaccuracy of touch on "bad" DIY hardware
* Ignoring regions
'''
from kivy.input.motionevent import MotionEvent
from kivy.input.postproc import kivy_postproc_modules
from kivy.input.provider import MotionEventProvider
from kivy.input.factory import MotionEventFactory
import kivy.input.providers
__all__ = (
MotionEvent.__name__,
MotionEventProvider.__name__,
MotionEventFactory.__name__,
'kivy_postproc_modules')

View file

@ -0,0 +1,35 @@
'''
Motion Event Factory
====================
Factory of :class:`~kivy.input.motionevent.MotionEvent` providers.
'''
__all__ = ('MotionEventFactory', )
class MotionEventFactory:
'''MotionEvent factory is a class that registers all availables input
factories. If you create a new input factory, you need to register
it here::
MotionEventFactory.register('myproviderid', MyInputProvider)
'''
__providers__ = {}
@staticmethod
def register(name, classname):
'''Register a input provider in the database'''
MotionEventFactory.__providers__[name] = classname
@staticmethod
def list():
'''Get a list of all available providers'''
return MotionEventFactory.__providers__
@staticmethod
def get(name):
'''Get a provider class from the provider id'''
if name in MotionEventFactory.__providers__:
return MotionEventFactory.__providers__[name]

View file

@ -0,0 +1,587 @@
'''
.. _motionevent:
Motion Event
============
The :class:`MotionEvent` is the base class used for events provided by
pointing devices (touch and non-touch). This class defines all the properties
and methods needed to handle 2D and 3D movements but has many more
capabilities.
Usually you would never need to create the :class:`MotionEvent` yourself as
this is the role of the :mod:`~kivy.input.providers`.
Flow of the motion events
-------------------------
1. The :class:`MotionEvent` 's are gathered from input providers by
:class:`~kivy.base.EventLoopBase`.
2. Post processing is performed by registered processors
:mod:`~kivy.input.postproc`.
3. :class:`~kivy.base.EventLoopBase` dispatches all motion events using
`on_motion` event to all registered listeners including the
:class:`~kivy.core.window.WindowBase`.
4. Once received in :meth:`~kivy.core.window.WindowBase.on_motion` events
(touch or non-touch) are all registered managers. If a touch event is not
handled by at least one manager, then it is dispatched through
:meth:`~kivy.core.window.WindowBase.on_touch_down`,
:meth:`~kivy.core.window.WindowBase.on_touch_move` and
:meth:`~kivy.core.window.WindowBase.on_touch_up`.
5. Widgets receive events in :meth:`~kivy.uix.widget.Widget.on_motion` method
(if passed by a manager) or on `on_touch_xxx` methods.
Motion events and event managers
--------------------------------
A motion event is a touch event if its :attr:`MotionEvent.is_touch` is set to
`True`. Beside `is_touch` attribute, :attr:`MotionEvent.type_id` can be used to
check for event's general type. Currently two types are dispatched by
input providers: "touch" and "hover".
Event managers can be used to dispatch any motion event throughout the widget
tree and a manager uses `type_id` to specify which event types it want to
receive. See :mod:`~kivy.eventmanager` to learn how to define and register
an event manager.
A manager can also assign a new `type_id` to
:attr:`MotionEvent.type_id` before dispatching it to the widgets. This useful
when dispatching a specific event::
class MouseTouchManager(EventManagerBase):
type_ids = ('touch',)
def dispatch(self, etype, me):
accepted = False
if me.device == 'mouse':
me.push() # Save current type_id and other values
me.type_id = 'mouse_touch'
self.window.transform_motion_event_2d(me)
# Dispatch mouse touch event to widgets which registered
# to receive 'mouse_touch'
for widget in self.window.children[:]:
if widget.dispatch('on_motion', etype, me):
accepted = True
break
me.pop() # Restore
return accepted
Listening to a motion event
---------------------------
If you want to receive all motion events, touch or not, you can bind the
MotionEvent from the :class:`~kivy.core.window.Window` to your own callback::
def on_motion(self, etype, me):
# will receive all motion events.
pass
Window.bind(on_motion=on_motion)
You can also listen to changes of the mouse position by watching
:attr:`~kivy.core.window.WindowBase.mouse_pos`.
Profiles
--------
The :class:`MotionEvent` stores device specific information in various
properties listed in the :attr:`~MotionEvent.profile`.
For example, you can receive a MotionEvent that has an angle, a fiducial
ID, or even a shape. You can check the :attr:`~MotionEvent.profile`
attribute to see what is currently supported by the MotionEvent provider.
This is a short list of the profile values supported by default. Please check
the :attr:`MotionEvent.profile` property to see what profile values are
available.
============== ================================================================
Profile value Description
-------------- ----------------------------------------------------------------
angle 2D angle. Accessed via the `a` property.
button Mouse button ('left', 'right', 'middle', 'scrollup' or
'scrolldown'). Accessed via the `button` property.
markerid Marker or Fiducial ID. Accessed via the `fid` property.
pos 2D position. Accessed via the `x`, `y` or `pos` properties.
pos3d 3D position. Accessed via the `x`, `y` or `z` properties.
pressure Pressure of the contact. Accessed via the `pressure` property.
shape Contact shape. Accessed via the `shape` property .
============== ================================================================
If you want to know whether the current :class:`MotionEvent` has an angle::
def on_touch_move(self, touch):
if 'angle' in touch.profile:
print('The touch angle is', touch.a)
If you want to select only the fiducials::
def on_touch_move(self, touch):
if 'markerid' not in touch.profile:
return
'''
__all__ = ('MotionEvent', )
import weakref
from inspect import isroutine
from copy import copy
from time import time
from kivy.eventmanager import MODE_DEFAULT_DISPATCH
from kivy.vector import Vector
class EnhancedDictionary(dict):
def __getattr__(self, attr):
try:
return self.__getitem__(attr)
except KeyError:
return super(EnhancedDictionary, self).__getattr__(attr)
def __setattr__(self, attr, value):
self.__setitem__(attr, value)
class MotionEventMetaclass(type):
def __new__(mcs, name, bases, attrs):
__attrs__ = []
for base in bases:
if hasattr(base, '__attrs__'):
__attrs__.extend(base.__attrs__)
if '__attrs__' in attrs:
__attrs__.extend(attrs['__attrs__'])
attrs['__attrs__'] = tuple(__attrs__)
return super(MotionEventMetaclass, mcs).__new__(mcs, name,
bases, attrs)
MotionEventBase = MotionEventMetaclass('MotionEvent', (object, ), {})
class MotionEvent(MotionEventBase):
'''Abstract class that represents an input event.
:Parameters:
`id`: str
unique ID of the MotionEvent
`args`: list
list of parameters, passed to the depack() function
'''
__uniq_id = 0
__attrs__ = \
('device', 'push_attrs', 'push_attrs_stack',
'is_touch', 'type_id', 'id', 'dispatch_mode', 'shape', 'profile',
# current position, in 0-1 range
'sx', 'sy', 'sz',
# first position set, in 0-1 range
'osx', 'osy', 'osz',
# last position set, in 0-1 range
'psx', 'psy', 'psz',
# delta from the last position and current one, in 0-1 range
'dsx', 'dsy', 'dsz',
# current position, in screen range
'x', 'y', 'z',
# first position set, in screen range
'ox', 'oy', 'oz',
# last position set, in 0-1 range
'px', 'py', 'pz',
# delta from the last position and current one, in screen range
'dx', 'dy', 'dz',
'time_start',
'is_double_tap', 'double_tap_time',
'is_triple_tap', 'triple_tap_time',
'ud')
def __init__(self, device, id, args, is_touch=False, type_id=None):
if self.__class__ == MotionEvent:
raise NotImplementedError('class MotionEvent is abstract')
MotionEvent.__uniq_id += 1
#: True if the MotionEvent is a touch.
self.is_touch = is_touch
#: (Experimental) String to identify event type.
#:
#: .. versionadded:: 2.1.0
self.type_id = type_id
#: (Experimental) Used by a event manager or a widget to assign
#: the dispatching mode. Defaults to
#: :const:`~kivy.eventmanager.MODE_DEFAULT_DISPATCH`. See
#: :mod:`~kivy.eventmanager` for available modes.
#:
#: .. versionadded:: 2.1.0
self.dispatch_mode = MODE_DEFAULT_DISPATCH
#: Attributes to push by default, when we use :meth:`push` : x, y, z,
#: dx, dy, dz, ox, oy, oz, px, py, pz.
self.push_attrs_stack = []
self.push_attrs = ('x', 'y', 'z', 'dx', 'dy', 'dz', 'ox', 'oy', 'oz',
'px', 'py', 'pz', 'pos', 'type_id', 'dispatch_mode')
#: Uniq ID of the event. You can safely use this property, it will be
#: never the same across all existing events.
self.uid = MotionEvent.__uniq_id
#: Device used for creating this event.
self.device = device
# For grab
self.grab_list = []
self.grab_exclusive_class = None
self.grab_state = False
#: Used to determine which widget the event is being dispatched to.
#: Check the :meth:`grab` function for more information.
self.grab_current = None
#: Currently pressed button.
self.button = None
#: Profiles currently used in the event.
self.profile = []
#: Id of the event, not unique. This is generally the Id set by the
#: input provider, like ID in TUIO. If you have multiple TUIO sources,
#: then same id can be used. Prefer to use :attr:`uid` attribute
#: instead.
self.id = id
#: Shape of the touch event, subclass of
#: :class:`~kivy.input.shape.Shape`.
#: By default, the property is set to None.
self.shape = None
#: X position, in 0-1 range.
self.sx = 0.0
#: Y position, in 0-1 range.
self.sy = 0.0
#: Z position, in 0-1 range.
self.sz = 0.0
#: Origin X position, in 0-1 range.
self.osx = None
#: Origin Y position, in 0-1 range.
self.osy = None
#: Origin Z position, in 0-1 range.
self.osz = None
#: Previous X position, in 0-1 range.
self.psx = None
#: Previous Y position, in 0-1 range.
self.psy = None
#: Previous Z position, in 0-1 range.
self.psz = None
#: Delta between self.sx and self.psx, in 0-1 range.
self.dsx = None
#: Delta between self.sy and self.psy, in 0-1 range.
self.dsy = None
#: Delta between self.sz and self.psz, in 0-1 range.
self.dsz = None
#: X position, in window range.
self.x = 0.0
#: Y position, in window range.
self.y = 0.0
#: Z position, in window range.
self.z = 0.0
#: Origin X position, in window range.
self.ox = None
#: Origin Y position, in window range.
self.oy = None
#: Origin Z position, in window range.
self.oz = None
#: Previous X position, in window range.
self.px = None
#: Previous Y position, in window range.
self.py = None
#: Previous Z position, in window range.
self.pz = None
#: Delta between self.x and self.px, in window range.
self.dx = None
#: Delta between self.y and self.py, in window range.
self.dy = None
#: Delta between self.z and self.pz, in window range.
self.dz = None
#: Position (X, Y), in window range.
self.pos = (0.0, 0.0)
#: Initial time of the event creation.
self.time_start = time()
#: Time of the last update.
self.time_update = self.time_start
#: Time of the end event (last event usage).
self.time_end = -1
#: Indicate if the touch event is a double tap or not.
self.is_double_tap = False
#: Indicate if the touch event is a triple tap or not.
#:
#: .. versionadded:: 1.7.0
self.is_triple_tap = False
#: If the touch is a :attr:`is_double_tap`, this is the time
#: between the previous tap and the current touch.
self.double_tap_time = 0
#: If the touch is a :attr:`is_triple_tap`, this is the time
#: between the first tap and the current touch.
#:
#: .. versionadded:: 1.7.0
self.triple_tap_time = 0
#: User data dictionary. Use this dictionary to save your own data on
#: the event.
self.ud = EnhancedDictionary()
#: If set to `True` (default) keeps first previous position
#: (X, Y, Z in 0-1 range) and ignore all other until
#: :meth:`MotionEvent.dispatch_done` is called from the `EventLoop`.
#:
#: This attribute is needed because event provider can make many calls
#: to :meth:`MotionEvent.move`, but for all those calls event is
#: dispatched to the listeners only once. Assigning `False` will keep
#: latest previous position. See :meth:`MotionEvent.move`.
#:
#: .. versionadded:: 2.1.0
self.sync_with_dispatch = True
#: Keep first previous position if :attr:`sync_with_dispatch` is
#: `True`.
self._keep_prev_pos = True
#: Flag that first dispatch of this event is done.
self._first_dispatch_done = False
self.depack(args)
def depack(self, args):
'''Depack `args` into attributes of the class'''
if self.osx is None \
or self.sync_with_dispatch and not self._first_dispatch_done:
# Sync origin/previous/current positions until the first
# dispatch (etype == 'begin') is done.
self.osx = self.psx = self.sx
self.osy = self.psy = self.sy
self.osz = self.psz = self.sz
# update the delta
self.dsx = self.sx - self.psx
self.dsy = self.sy - self.psy
self.dsz = self.sz - self.psz
def grab(self, class_instance, exclusive=False):
'''Grab this motion event.
If this event is a touch you can grab it if you want to receive
subsequent :meth:`~kivy.uix.widget.Widget.on_touch_move` and
:meth:`~kivy.uix.widget.Widget.on_touch_up` events, even if the touch
is not dispatched by the parent:
.. code-block:: python
def on_touch_down(self, touch):
touch.grab(self)
def on_touch_move(self, touch):
if touch.grab_current is self:
# I received my grabbed touch
else:
# it's a normal touch
def on_touch_up(self, touch):
if touch.grab_current is self:
# I receive my grabbed touch, I must ungrab it!
touch.ungrab(self)
else:
# it's a normal touch
pass
.. versionchanged:: 2.1.0
Allowed grab for non-touch events.
'''
if self.grab_exclusive_class is not None:
raise Exception('Event is exclusive and cannot be grabbed')
class_instance = weakref.ref(class_instance.__self__)
if exclusive:
self.grab_exclusive_class = class_instance
self.grab_list.append(class_instance)
def ungrab(self, class_instance):
'''Ungrab a previously grabbed motion event.
'''
class_instance = weakref.ref(class_instance.__self__)
if self.grab_exclusive_class == class_instance:
self.grab_exclusive_class = None
if class_instance in self.grab_list:
self.grab_list.remove(class_instance)
def dispatch_done(self):
'''Notify that dispatch to the listeners is done.
Called by the :meth:`EventLoopBase.post_dispatch_input`.
.. versionadded:: 2.1.0
'''
self._keep_prev_pos = True
self._first_dispatch_done = True
def move(self, args):
'''Move to another position.
'''
if self.sync_with_dispatch:
if self._keep_prev_pos:
self.psx, self.psy, self.psz = self.sx, self.sy, self.sz
self._keep_prev_pos = False
else:
self.psx, self.psy, self.psz = self.sx, self.sy, self.sz
self.time_update = time()
self.depack(args)
def scale_for_screen(self, w, h, p=None, rotation=0,
smode='None', kheight=0):
'''Scale position for the screen.
.. versionchanged:: 2.1.0
Max value for `x`, `y` and `z` is changed respectively to `w` - 1,
`h` - 1 and `p` - 1.
'''
x_max, y_max = max(0, w - 1), max(0, h - 1)
absolute = self.to_absolute_pos
self.x, self.y = absolute(self.sx, self.sy, x_max, y_max, rotation)
self.ox, self.oy = absolute(self.osx, self.osy, x_max, y_max, rotation)
self.px, self.py = absolute(self.psx, self.psy, x_max, y_max, rotation)
z_max = 0 if p is None else max(0, p - 1)
self.z = self.sz * z_max
self.oz = self.osz * z_max
self.pz = self.psz * z_max
if smode:
# Adjust y for keyboard height
if smode == 'pan' or smode == 'below_target':
self.y -= kheight
self.oy -= kheight
self.py -= kheight
elif smode == 'scale':
offset = kheight * (self.y - h) / (h - kheight)
self.y += offset
self.oy += offset
self.py += offset
# Update delta values
self.dx = self.x - self.px
self.dy = self.y - self.py
self.dz = self.z - self.pz
# Cache position
self.pos = self.x, self.y
def to_absolute_pos(self, nx, ny, x_max, y_max, rotation):
'''Transforms normalized (0-1) coordinates `nx` and `ny` to absolute
coordinates using `x_max`, `y_max` and `rotation`.
:raises:
`ValueError`: If `rotation` is not one of: 0, 90, 180 or 270
.. versionadded:: 2.1.0
'''
if rotation == 0:
return nx * x_max, ny * y_max
elif rotation == 90:
return ny * y_max, (1 - nx) * x_max
elif rotation == 180:
return (1 - nx) * x_max, (1 - ny) * y_max
elif rotation == 270:
return (1 - ny) * y_max, nx * x_max
raise ValueError('Invalid rotation %s, '
'valid values are 0, 90, 180 or 270' % rotation)
def push(self, attrs=None):
'''Push attribute values in `attrs` onto the stack.
'''
if attrs is None:
attrs = self.push_attrs
values = [getattr(self, x) for x in attrs]
self.push_attrs_stack.append((attrs, values))
def pop(self):
'''Pop attributes values from the stack.
'''
attrs, values = self.push_attrs_stack.pop()
for i in range(len(attrs)):
setattr(self, attrs[i], values[i])
def apply_transform_2d(self, transform):
'''Apply a transformation on x, y, z, px, py, pz,
ox, oy, oz, dx, dy, dz.
'''
self.x, self.y = self.pos = transform(self.x, self.y)
self.px, self.py = transform(self.px, self.py)
self.ox, self.oy = transform(self.ox, self.oy)
self.dx = self.x - self.px
self.dy = self.y - self.py
def copy_to(self, to):
'''Copy some attribute to another motion event object.'''
for attr in self.__attrs__:
to.__setattr__(attr, copy(self.__getattribute__(attr)))
def distance(self, other_touch):
'''Return the distance between the two events.
'''
return Vector(self.pos).distance(other_touch.pos)
def update_time_end(self):
self.time_end = time()
# facilities
@property
def dpos(self):
'''Return delta between last position and current position, in the
screen coordinate system (self.dx, self.dy).'''
return self.dx, self.dy
@property
def opos(self):
'''Return the initial position of the motion event in the screen
coordinate system (self.ox, self.oy).'''
return self.ox, self.oy
@property
def ppos(self):
'''Return the previous position of the motion event in the screen
coordinate system (self.px, self.py).'''
return self.px, self.py
@property
def spos(self):
'''Return the position in the 0-1 coordinate system (self.sx, self.sy).
'''
return self.sx, self.sy
def __str__(self):
basename = str(self.__class__)
classname = basename.split('.')[-1].replace('>', '').replace('\'', '')
return '<%s spos=%s pos=%s>' % (classname, self.spos, self.pos)
def __repr__(self):
out = []
for x in dir(self):
v = getattr(self, x)
if x[0] == '_':
continue
if isroutine(v):
continue
out.append('%s="%s"' % (x, v))
return '<%s %s>' % (
self.__class__.__name__,
' '.join(out))
@property
def is_mouse_scrolling(self, *args):
'''Returns True if the touch event is a mousewheel scrolling
.. versionadded:: 1.6.0
'''
return 'button' in self.profile and 'scroll' in self.button

View file

@ -0,0 +1,27 @@
'''
Input Postprocessing
====================
'''
__all__ = ('kivy_postproc_modules', )
import os
from kivy.input.postproc.doubletap import InputPostprocDoubleTap
from kivy.input.postproc.tripletap import InputPostprocTripleTap
from kivy.input.postproc.ignorelist import InputPostprocIgnoreList
from kivy.input.postproc.retaintouch import InputPostprocRetainTouch
from kivy.input.postproc.dejitter import InputPostprocDejitter
from kivy.input.postproc.calibration import InputPostprocCalibration
# Mapping of ID to module
kivy_postproc_modules = {}
# Don't go further if we generate documentation
if 'KIVY_DOC' not in os.environ:
kivy_postproc_modules['calibration'] = InputPostprocCalibration()
kivy_postproc_modules['retaintouch'] = InputPostprocRetainTouch()
kivy_postproc_modules['ignorelist'] = InputPostprocIgnoreList()
kivy_postproc_modules['doubletap'] = InputPostprocDoubleTap()
kivy_postproc_modules['tripletap'] = InputPostprocTripleTap()
kivy_postproc_modules['dejitter'] = InputPostprocDejitter()

View file

@ -0,0 +1,200 @@
'''
Calibration
===========
.. versionadded:: 1.9.0
Recalibrate input device to a specific range / offset.
Let's say you have 3 1080p displays, the 2 firsts are multitouch. By default,
both will have mixed touch, the range will conflict with each others: the 0-1
range will goes to 0-5760 px (remember, 3 * 1920 = 5760.)
To fix it, you need to manually reference them. For example::
[input]
left = mtdev,/dev/input/event17
middle = mtdev,/dev/input/event15
# the right screen is just a display.
Then, you can use the calibration postproc module::
[postproc:calibration]
left = xratio=0.3333
middle = xratio=0.3333,xoffset=0.3333
Now, the touches from the left screen will be within 0-0.3333 range, and the
touches from the middle screen will be within 0.3333-0.6666 range.
You can also match calibration rules to devices based on their provider type.
This is useful when probesysfs is used to match devices. For example::
[input]
mtdev_%(name)s = probesysfs,provider=mtdev
Then to apply calibration to any mtdev device, you can assign rules to the
provider name enclosed by parentheses::
[postproc:calibration]
(mtdev) = xratio=0.3333,xoffset=0.3333
Calibrating devices like this means the device's path doesn't need to be
configured ahead of time. Note that with this method, all mtdev inputs will
have the same calibration applied to them. For this reason, matching by
provider will typically be useful when expecting only one input device.
'''
__all__ = ('InputPostprocCalibration', )
from kivy.config import Config
from kivy.logger import Logger
from kivy.input import providers
from kivy.input.factory import MotionEventFactory
from kivy.input.motionevent import MotionEvent
class InputPostprocCalibration(object):
'''Recalibrate the inputs.
The configuration must go within a section named `postproc:calibration`.
Within the section, you must have a line like::
devicename = param=value,param=value
If you wish to match by provider, you must have a line like::
(provider) = param=value,param=value
:Parameters:
`xratio`: float
Value to multiply X
`yratio`: float
Value to multiply Y
`xoffset`: float
Value to add to X
`yoffset`: float
Value to add to Y
`auto`: str
If set, then the touch is transformed from screen-relative
to window-relative The value is used as an indication of
screen size, e.g for fullHD:
auto=1920x1080
If present, this setting overrides all the others.
This assumes the input device exactly covers the display
area, if they are different, the computations will be wrong.
.. versionchanged:: 1.11.0
Added `auto` parameter
'''
def __init__(self):
super(InputPostprocCalibration, self).__init__()
self.devices = {}
self.frame = 0
self.provider_map = self._get_provider_map()
if not Config.has_section('postproc:calibration'):
return
default_params = {'xoffset': 0, 'yoffset': 0, 'xratio': 1, 'yratio': 1}
for device_key, params_str in Config.items('postproc:calibration'):
params = default_params.copy()
for param in params_str.split(','):
param = param.strip()
if not param:
continue
key, value = param.split('=', 1)
if key == 'auto':
width, height = [float(x) for x in value.split('x')]
params['auto'] = width, height
break
if key not in ('xoffset', 'yoffset', 'xratio', 'yratio'):
Logger.error(
'Calibration: invalid key provided: {}'.format(key))
params[key] = float(value)
self.devices[device_key] = params
def _get_provider_map(self):
"""Iterates through all registered input provider names and finds the
respective MotionEvent subclass for each. Returns a dict of MotionEvent
subclasses mapped to their provider name.
"""
provider_map = {}
for input_provider in MotionEventFactory.list():
if not hasattr(providers, input_provider):
continue
p = getattr(providers, input_provider)
for m in p.__all__:
event = getattr(p, m)
if issubclass(event, MotionEvent):
provider_map[event] = input_provider
return provider_map
def _get_provider_key(self, event):
"""Returns the provider key for the event if the provider is configured
for calibration.
"""
input_type = self.provider_map.get(event.__class__)
key = '({})'.format(input_type)
if input_type and key in self.devices:
return key
def process(self, events):
# avoid doing any processing if there is no device to calibrate at all.
if not self.devices:
return events
self.frame += 1
frame = self.frame
to_remove = []
for etype, event in events:
# frame-based logic below doesn't account for
# end events having been already processed
if etype == 'end':
continue
if event.device in self.devices:
dev = event.device
else:
dev = self._get_provider_key(event)
if not dev:
continue
# some providers use the same event to update and end
if 'calibration:frame' not in event.ud:
event.ud['calibration:frame'] = frame
elif event.ud['calibration:frame'] == frame:
continue
event.ud['calibration:frame'] = frame
params = self.devices[dev]
if 'auto' in params:
event.sx, event.sy = self.auto_calibrate(
event.sx, event.sy, params['auto'])
if not (0 <= event.sx <= 1 and 0 <= event.sy <= 1):
to_remove.append((etype, event))
else:
event.sx = event.sx * params['xratio'] + params['xoffset']
event.sy = event.sy * params['yratio'] + params['yoffset']
for event in to_remove:
events.remove(event)
return events
def auto_calibrate(self, sx, sy, size):
from kivy.core.window import Window as W
WIDTH, HEIGHT = size
xratio = WIDTH / W.width
yratio = HEIGHT / W.height
xoffset = - W.left / W.width
yoffset = - (HEIGHT - W.top - W.height) / W.height
sx = sx * xratio + xoffset
sy = sy * yratio + yoffset
return sx, sy

View file

@ -0,0 +1,74 @@
'''
Dejitter
========
Prevent blob jittering.
A problem that is often faced (esp. in optical MT setups) is that of
jitterish BLOBs caused by bad camera characteristics. With this module
you can get rid of that jitter. You just define a threshold
`jitter_distance` in your config, and all touch movements that move
the touch by less than the jitter distance are considered 'bad'
movements caused by jitter and will be discarded.
'''
__all__ = ('InputPostprocDejitter', )
from kivy.config import Config
class InputPostprocDejitter(object):
'''
Get rid of jitterish BLOBs.
Example::
[postproc]
jitter_distance = 0.004
jitter_ignore_devices = mouse,mactouch
:Configuration:
`jitter_distance`: float
A float in range 0-1.
`jitter_ignore_devices`: string
A comma-separated list of device identifiers that
should not be processed by dejitter (because they're
very precise already).
'''
def __init__(self):
self.jitterdist = Config.getfloat('postproc', 'jitter_distance')
ignore_devices = Config.get('postproc', 'jitter_ignore_devices')
self.ignore_devices = ignore_devices.split(',')
self.last_touches = {}
def taxicab_distance(self, p, q):
# Get the taxicab/manhattan/citiblock distance for efficiency reasons
return abs(p[0] - q[0]) + abs(p[1] - q[1])
def process(self, events):
if not self.jitterdist:
return events
processed = []
for etype, touch in events:
if not touch.is_touch:
continue
if touch.device in self.ignore_devices:
processed.append((etype, touch))
continue
if etype == 'begin':
self.last_touches[touch.id] = touch.spos
if etype == 'end':
if touch.id in self.last_touches:
del self.last_touches[touch.id]
if etype != 'update':
processed.append((etype, touch))
continue
# Check whether the touch moved more than the jitter distance
last_spos = self.last_touches[touch.id]
dist = self.taxicab_distance(last_spos, touch.spos)
if dist > self.jitterdist:
# Only if the touch has moved more than the jitter dist we take
# it into account and dispatch it. Otherwise suppress it.
self.last_touches[touch.id] = touch.spos
processed.append((etype, touch))
return processed

View file

@ -0,0 +1,101 @@
'''
Double Tap
==========
Search touch for a double tap
'''
__all__ = ('InputPostprocDoubleTap', )
from time import time
from kivy.config import Config
from kivy.vector import Vector
class InputPostprocDoubleTap(object):
'''
InputPostProcDoubleTap is a post-processor to check if
a touch is a double tap or not.
Double tap can be configured in the Kivy config file::
[postproc]
double_tap_time = 250
double_tap_distance = 20
Distance parameter is in the range 0-1000 and time is in milliseconds.
'''
def __init__(self):
dist = Config.getint('postproc', 'double_tap_distance')
self.double_tap_distance = dist / 1000.0
tap_time = Config.getint('postproc', 'double_tap_time')
self.double_tap_time = tap_time / 1000.0
self.touches = {}
def find_double_tap(self, ref):
'''Find a double tap touch within self.touches.
The touch must be not a previous double tap and the distance must be
within the specified threshold. Additionally, the touch profiles
must be the same kind of touch.
'''
ref_button = None
if 'button' in ref.profile:
ref_button = ref.button
for touchid in self.touches:
if ref.uid == touchid:
continue
etype, touch = self.touches[touchid]
if etype != 'end':
continue
if touch.is_double_tap:
continue
distance = Vector.distance(
Vector(ref.sx, ref.sy),
Vector(touch.osx, touch.osy))
if distance > self.double_tap_distance:
continue
if touch.is_mouse_scrolling or ref.is_mouse_scrolling:
continue
touch_button = None
if 'button' in touch.profile:
touch_button = touch.button
if touch_button != ref_button:
continue
touch.double_tap_distance = distance
return touch
def process(self, events):
if self.double_tap_distance == 0 or self.double_tap_time == 0:
return events
# first, check if a touch down have a double tap
for etype, touch in events:
if not touch.is_touch:
continue
if etype == 'begin':
double_tap = self.find_double_tap(touch)
if double_tap:
touch.is_double_tap = True
tap_time = touch.time_start - double_tap.time_start
touch.double_tap_time = tap_time
distance = double_tap.double_tap_distance
touch.double_tap_distance = distance
# add the touch internally
self.touches[touch.uid] = (etype, touch)
# second, check if up-touch is timeout for double tap
time_current = time()
to_delete = []
for touchid in self.touches.keys():
etype, touch = self.touches[touchid]
if etype != 'end':
continue
if time_current - touch.time_start < self.double_tap_time:
continue
to_delete.append(touchid)
for touchid in to_delete:
del self.touches[touchid]
return events

View file

@ -0,0 +1,47 @@
'''
Ignore list
===========
Ignore touch on some areas of the screen
'''
__all__ = ('InputPostprocIgnoreList', )
from kivy.config import Config
from kivy.utils import strtotuple
class InputPostprocIgnoreList(object):
'''
InputPostprocIgnoreList is a post-processor which removes touches in the
Ignore list. The Ignore list can be configured in the Kivy config file::
[postproc]
# Format: [(xmin, ymin, xmax, ymax), ...]
ignore = [(0.1, 0.1, 0.15, 0.15)]
The Ignore list coordinates are in the range 0-1, not in screen pixels.
'''
def __init__(self):
self.ignore_list = strtotuple(Config.get('postproc', 'ignore'))
def collide_ignore(self, touch):
x, y = touch.sx, touch.sy
for l in self.ignore_list:
xmin, ymin, xmax, ymax = l
if x > xmin and x < xmax and y > ymin and y < ymax:
return True
def process(self, events):
if not len(self.ignore_list):
return events
for etype, touch in events:
if not touch.is_touch:
continue
if etype != 'begin':
continue
if self.collide_ignore(touch):
touch.ud.__pp_ignore__ = True
return [(etype, touch) for etype, touch in events
if '__pp_ignore__' not in touch.ud]

View file

@ -0,0 +1,93 @@
'''
Retain Touch
============
Reuse touch to counter lost finger behavior
'''
__all__ = ('InputPostprocRetainTouch', )
from kivy.config import Config
from kivy.vector import Vector
import time
class InputPostprocRetainTouch(object):
'''
InputPostprocRetainTouch is a post-processor to delay the 'up' event of a
touch, to reuse it under certains conditions. This module is designed to
prevent lost finger touches on some hardware/setups.
Retain touch can be configured in the Kivy config file::
[postproc]
retain_time = 100
retain_distance = 50
The distance parameter is in the range 0-1000 and time is in milliseconds.
'''
def __init__(self):
self.timeout = Config.getint('postproc', 'retain_time') / 1000.0
self.distance = Config.getint('postproc', 'retain_distance') / 1000.0
self._available = []
self._links = {}
def process(self, events):
# check if module is disabled
if self.timeout == 0:
return events
d = time.time()
for etype, touch in events[:]:
if not touch.is_touch:
continue
if etype == 'end':
events.remove((etype, touch))
if touch.uid in self._links:
selection = self._links[touch.uid]
selection.ud.__pp_retain_time__ = d
self._available.append(selection)
del self._links[touch.uid]
else:
touch.ud.__pp_retain_time__ = d
self._available.append(touch)
elif etype == 'update':
if touch.uid in self._links:
selection = self._links[touch.uid]
selection.x = touch.x
selection.y = touch.y
selection.sx = touch.sx
selection.sy = touch.sy
events.remove((etype, touch))
events.append((etype, selection))
else:
pass
elif etype == 'begin':
# new touch, found the nearest one
selection = None
selection_distance = 99999
for touch2 in self._available:
touch_distance = Vector(touch2.spos).distance(touch.spos)
if touch_distance > self.distance:
continue
if touch2.__class__ != touch.__class__:
continue
if touch_distance < selection_distance:
# eligible for continuation
selection_distance = touch_distance
selection = touch2
if selection is None:
continue
self._links[touch.uid] = selection
self._available.remove(selection)
events.remove((etype, touch))
for touch in self._available[:]:
t = touch.ud.__pp_retain_time__
if d - t > self.timeout:
self._available.remove(touch)
events.append(('end', touch))
return events

View file

@ -0,0 +1,106 @@
'''
Triple Tap
==========
.. versionadded:: 1.7.0
Search touch for a triple tap
'''
__all__ = ('InputPostprocTripleTap', )
from time import time
from kivy.config import Config
from kivy.vector import Vector
class InputPostprocTripleTap(object):
'''
InputPostProcTripleTap is a post-processor to check if
a touch is a triple tap or not.
Triple tap can be configured in the Kivy config file::
[postproc]
triple_tap_time = 250
triple_tap_distance = 20
The distance parameter is in the range 0-1000 and time is in milliseconds.
'''
def __init__(self):
dist = Config.getint('postproc', 'triple_tap_distance')
self.triple_tap_distance = dist / 1000.0
time = Config.getint('postproc', 'triple_tap_time')
self.triple_tap_time = time / 1000.0
self.touches = {}
def find_triple_tap(self, ref):
'''Find a triple tap touch within *self.touches*.
The touch must be not be a previous triple tap and the distance
must be within the bounds specified. Additionally, the touch profile
must be the same kind of touch.
'''
ref_button = None
if 'button' in ref.profile:
ref_button = ref.button
for touchid in self.touches:
if ref.uid == touchid:
continue
etype, touch = self.touches[touchid]
if not touch.is_double_tap:
continue
if etype != 'end':
continue
if touch.is_triple_tap:
continue
distance = Vector.distance(
Vector(ref.sx, ref.sy),
Vector(touch.osx, touch.osy))
if distance > self.triple_tap_distance:
continue
if touch.is_mouse_scrolling or ref.is_mouse_scrolling:
continue
touch_button = None
if 'button' in touch.profile:
touch_button = touch.button
if touch_button != ref_button:
continue
touch.triple_tap_distance = distance
return touch
def process(self, events):
if self.triple_tap_distance == 0 or self.triple_tap_time == 0:
return events
# first, check if a touch down have a triple tap
for etype, touch in events:
if not touch.is_touch:
continue
if etype == 'begin':
triple_tap = self.find_triple_tap(touch)
if triple_tap:
touch.is_double_tap = False
touch.is_triple_tap = True
tap_time = touch.time_start - triple_tap.time_start
touch.triple_tap_time = tap_time
distance = triple_tap.triple_tap_distance
touch.triple_tap_distance = distance
# add the touch internally
self.touches[touch.uid] = (etype, touch)
# second, check if up-touch is timeout for triple tap
time_current = time()
to_delete = []
for touchid in self.touches.keys():
etype, touch = self.touches[touchid]
if etype != 'end':
continue
if time_current - touch.time_start < self.triple_tap_time:
continue
to_delete.append(touchid)
for touchid in to_delete:
del self.touches[touchid]
return events

View file

@ -0,0 +1,40 @@
'''
Motion Event Provider
=====================
Abstract class for the implementation of a
:class:`~kivy.input.motionevent.MotionEvent`
provider. The implementation must support the
:meth:`~MotionEventProvider.start`, :meth:`~MotionEventProvider.stop` and
:meth:`~MotionEventProvider.update` methods.
'''
__all__ = ('MotionEventProvider', )
class MotionEventProvider(object):
'''Base class for a provider.
'''
def __init__(self, device, args):
self.device = device
if self.__class__ == MotionEventProvider:
raise NotImplementedError('class MotionEventProvider is abstract')
def start(self):
'''Start the provider. This method is automatically called when the
application is started and if the configuration uses the current
provider.
'''
pass
def stop(self):
'''Stop the provider.
'''
pass
def update(self, dispatch_fn):
'''Update the provider and dispatch all the new touch events though the
`dispatch_fn` argument.
'''
pass

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)

View file

@ -0,0 +1,335 @@
'''
Input recorder
==============
.. versionadded:: 1.1.0
.. warning::
This part of Kivy is still experimental and this API is subject to
change in a future version.
This is a class that can record and replay some input events. This can
be used for test cases, screen savers etc.
Once activated, the recorder will listen for any input event and save its
properties in a file with the delta time. Later, you can play the input
file: it will generate fake touch events with the saved properties and
dispatch it to the event loop.
By default, only the position is saved ('pos' profile and 'sx', 'sy',
attributes). Change it only if you understand how input handling works.
Recording events
----------------
The best way is to use the "recorder" module. Check the :doc:`api-kivy.modules`
documentation to see how to activate a module.
Once activated, you can press F8 to start the recording. By default,
events will be written to `<currentpath>/recorder.kvi`. When you want to
stop recording, press F8 again.
You can replay the file by pressing F7.
Check the :doc:`api-kivy.modules.recorder` module for more information.
Manual play
-----------
You can manually open a recorder file, and play it by doing::
from kivy.input.recorder import Recorder
rec = Recorder(filename='myrecorder.kvi')
rec.play = True
If you want to loop over that file, you can do::
from kivy.input.recorder import Recorder
def recorder_loop(instance, value):
if value is False:
instance.play = True
rec = Recorder(filename='myrecorder.kvi')
rec.bind(play=recorder_loop)
rec.play = True
Recording more attributes
-------------------------
You can extend the attributes to save on one condition: attributes values must
be simple values, not instances of complex classes.
Let's say you want to save the angle and pressure of the touch, if available::
from kivy.input.recorder import Recorder
rec = Recorder(filename='myrecorder.kvi',
record_attrs=['is_touch', 'sx', 'sy', 'angle', 'pressure'],
record_profile_mask=['pos', 'angle', 'pressure'])
rec.record = True
Or with modules variables::
$ python main.py -m recorder,attrs=is_touch:sx:sy:angle:pressure, \
profile_mask=pos:angle:pressure
Known limitations
-----------------
- Unable to save attributes with instances of complex classes.
- Values that represent time will not be adjusted.
- Can replay only complete records. If a begin/update/end event is missing,
this could lead to ghost touches.
- Stopping the replay before the end can lead to ghost touches.
'''
__all__ = ('Recorder', )
from os.path import exists
from time import time
from kivy.event import EventDispatcher
from kivy.properties import ObjectProperty, BooleanProperty, StringProperty, \
NumericProperty, ListProperty
from kivy.input.motionevent import MotionEvent
from kivy.base import EventLoop
from kivy.logger import Logger
from ast import literal_eval
from functools import partial
class RecorderMotionEvent(MotionEvent):
def depack(self, args):
for key, value in list(args.items()):
setattr(self, key, value)
super(RecorderMotionEvent, self).depack(args)
class Recorder(EventDispatcher):
'''Recorder class. Please check module documentation for more information.
:Events:
`on_stop`:
Fired when the playing stops.
.. versionchanged:: 1.10.0
Event `on_stop` added.
'''
window = ObjectProperty(None)
'''Window instance to attach the recorder. If None, it will use the
default instance.
:attr:`window` is a :class:`~kivy.properties.ObjectProperty` and
defaults to None.
'''
counter = NumericProperty(0)
'''Number of events recorded in the last session.
:attr:`counter` is a :class:`~kivy.properties.NumericProperty` and defaults
to 0, read-only.
'''
play = BooleanProperty(False)
'''Boolean to start/stop the replay of the current file (if it exists).
:attr:`play` is a :class:`~kivy.properties.BooleanProperty` and defaults to
False.
'''
record = BooleanProperty(False)
'''Boolean to start/stop the recording of input events.
:attr:`record` is a :class:`~kivy.properties.BooleanProperty` and defaults
to False.
'''
filename = StringProperty('recorder.kvi')
'''Filename to save the output of the recorder.
:attr:`filename` is a :class:`~kivy.properties.StringProperty` and defaults
to 'recorder.kvi'.
'''
record_attrs = ListProperty(['is_touch', 'sx', 'sy'])
'''Attributes to record from the motion event.
:attr:`record_attrs` is a :class:`~kivy.properties.ListProperty` and
defaults to ['is_touch', 'sx', 'sy'].
'''
record_profile_mask = ListProperty(['pos'])
'''Profile to save in the fake motion event when replayed.
:attr:`record_profile_mask` is a :class:`~kivy.properties.ListProperty` and
defaults to ['pos'].
'''
# internals
record_fd = ObjectProperty(None)
record_time = NumericProperty(0.)
__events__ = ('on_stop',)
def __init__(self, **kwargs):
super(Recorder, self).__init__(**kwargs)
if self.window is None:
# manually set the current window
from kivy.core.window import Window
self.window = Window
self.window.bind(
on_motion=self.on_motion,
on_key_up=partial(self.on_keyboard, 'keyup'),
on_key_down=partial(self.on_keyboard, 'keydown'),
on_keyboard=partial(self.on_keyboard, 'keyboard'))
def on_motion(self, window, etype, motionevent):
if not self.record:
return
args = dict((arg, getattr(motionevent, arg))
for arg in self.record_attrs if hasattr(motionevent, arg))
args['profile'] = [x for x in motionevent.profile if x in
self.record_profile_mask]
self.record_fd.write('%r\n' % (
(time() - self.record_time, etype, motionevent.uid, args), ))
self.counter += 1
def on_keyboard(self, etype, window, key, *args, **kwargs):
if not self.record:
return
self.record_fd.write('%r\n' % (
(time() - self.record_time, etype, 0, {
'key': key,
'scancode': kwargs.get('scancode'),
'codepoint': kwargs.get('codepoint', kwargs.get('unicode')),
'modifier': kwargs.get('modifier'),
'is_touch': False}), ))
self.counter += 1
def release(self):
self.window.unbind(
on_motion=self.on_motion,
on_key_up=self.on_keyboard,
on_key_down=self.on_keyboard)
def on_record(self, instance, value):
if value:
# generate a record filename
self.counter = 0
self.record_time = time()
self.record_fd = open(self.filename, 'w')
self.record_fd.write('#RECORDER1.0\n')
Logger.info('Recorder: Recording inputs to %r' % self.filename)
else:
self.record_fd.close()
Logger.info('Recorder: Recorded %d events in %r' % (self.counter,
self.filename))
# needed for acting as an input provider
def stop(self):
pass
def start(self):
pass
def on_play(self, instance, value):
if not value:
Logger.info('Recorder: Stop playing %r' % self.filename)
EventLoop.remove_input_provider(self)
return
if not exists(self.filename):
Logger.error('Recorder: Unable to find %r file, play aborted.' % (
self.filename))
return
with open(self.filename, 'r') as fd:
data = fd.read().splitlines()
if len(data) < 2:
Logger.error('Recorder: Unable to play %r, file truncated.' % (
self.filename))
return
if data[0] != '#RECORDER1.0':
Logger.error('Recorder: Unable to play %r, invalid header.' % (
self.filename))
return
# decompile data
self.play_data = [literal_eval(x) for x in data[1:]]
self.play_time = time()
self.play_me = {}
Logger.info('Recorder: Start playing %d events from %r' %
(len(self.play_data), self.filename))
EventLoop.add_input_provider(self)
def on_stop(self):
pass
def update(self, dispatch_fn):
if not self.play_data:
Logger.info('Recorder: Playing finished.')
self.play = False
self.dispatch('on_stop')
dt = time() - self.play_time
while self.play_data:
event = self.play_data[0]
assert len(event) == 4
if event[0] > dt:
return
me = None
etype, uid, args = event[1:]
if etype == 'begin':
me = RecorderMotionEvent('recorder', uid, args)
self.play_me[uid] = me
elif etype == 'update':
me = self.play_me[uid]
me.depack(args)
elif etype == 'end':
me = self.play_me.pop(uid)
me.depack(args)
elif etype == 'keydown':
self.window.dispatch(
'on_key_down',
args['key'],
args['scancode'],
args['codepoint'],
args['modifier'])
elif etype == 'keyup':
self.window.dispatch(
'on_key_up',
args['key'],
args['scancode'],
args['codepoint'],
args['modifier'])
elif etype == 'keyboard':
self.window.dispatch(
'on_keyboard',
args['key'],
args['scancode'],
args['codepoint'],
args['modifier'])
if me:
dispatch_fn(etype, me)
self.play_data.pop(0)
def start(win, ctx):
ctx.recorder = Recorder(window=win)
def stop(win, ctx):
if hasattr(ctx, 'recorder'):
ctx.recorder.release()

View file

@ -0,0 +1,27 @@
'''
Motion Event Shape
==================
Represent the shape of the :class:`~kivy.input.motionevent.MotionEvent`
'''
__all__ = ('Shape', 'ShapeRect')
class Shape(object):
'''Abstract class for all implementations of a shape'''
pass
class ShapeRect(Shape):
'''Class for the representation of a rectangle.'''
__slots__ = ('width', 'height')
def __init__(self):
super(ShapeRect, self).__init__()
#: Width of the rect
self.width = 0
#: Height of the rect
self.height = 0