first commit
This commit is contained in:
commit
417e54da96
5696 changed files with 900003 additions and 0 deletions
|
@ -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')
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
35
kivy_venv/lib/python3.11/site-packages/kivy/input/factory.py
Normal file
35
kivy_venv/lib/python3.11/site-packages/kivy/input/factory.py
Normal 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]
|
587
kivy_venv/lib/python3.11/site-packages/kivy/input/motionevent.py
Normal file
587
kivy_venv/lib/python3.11/site-packages/kivy/input/motionevent.py
Normal 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
|
|
@ -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()
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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]
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
335
kivy_venv/lib/python3.11/site-packages/kivy/input/recorder.py
Normal file
335
kivy_venv/lib/python3.11/site-packages/kivy/input/recorder.py
Normal 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()
|
27
kivy_venv/lib/python3.11/site-packages/kivy/input/shape.py
Normal file
27
kivy_venv/lib/python3.11/site-packages/kivy/input/shape.py
Normal 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
|
Loading…
Add table
Add a link
Reference in a new issue