Ajout du GUI
This commit is contained in:
39
kivy/input/__init__.py
Normal file
39
kivy/input/__init__.py
Normal file
@@ -0,0 +1,39 @@
|
||||
# pylint: disable=W0611
|
||||
'''
|
||||
Input management
|
||||
================
|
||||
|
||||
Our input system is wide and simple at the same time. We are currently able to
|
||||
natively support :
|
||||
|
||||
* Windows multitouch events (pencil and finger)
|
||||
* OS X touchpads
|
||||
* Linux multitouch events (kernel and mtdev)
|
||||
* Linux wacom drivers (pencil and finger)
|
||||
* TUIO
|
||||
|
||||
All the input management is configurable in the Kivy :mod:`~kivy.config`. You
|
||||
can easily use many multitouch devices in one Kivy application.
|
||||
|
||||
When the events have been read from the devices, they are dispatched through
|
||||
a post processing module before being sent to your application. We also have
|
||||
several default modules for :
|
||||
|
||||
* Double tap detection
|
||||
* Decreasing jittering
|
||||
* Decreasing the inaccuracy of touch on "bad" DIY hardware
|
||||
* Ignoring regions
|
||||
'''
|
||||
|
||||
|
||||
from kivy.input.motionevent import MotionEvent
|
||||
from kivy.input.postproc import kivy_postproc_modules
|
||||
from kivy.input.provider import MotionEventProvider
|
||||
from kivy.input.factory import MotionEventFactory
|
||||
import kivy.input.providers
|
||||
|
||||
__all__ = (
|
||||
MotionEvent.__name__,
|
||||
MotionEventProvider.__name__,
|
||||
MotionEventFactory.__name__,
|
||||
'kivy_postproc_modules')
|
||||
BIN
kivy/input/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
kivy/input/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/input/__pycache__/factory.cpython-310.pyc
Normal file
BIN
kivy/input/__pycache__/factory.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/input/__pycache__/motionevent.cpython-310.pyc
Normal file
BIN
kivy/input/__pycache__/motionevent.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/input/__pycache__/provider.cpython-310.pyc
Normal file
BIN
kivy/input/__pycache__/provider.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/input/__pycache__/recorder.cpython-310.pyc
Normal file
BIN
kivy/input/__pycache__/recorder.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/input/__pycache__/shape.cpython-310.pyc
Normal file
BIN
kivy/input/__pycache__/shape.cpython-310.pyc
Normal file
Binary file not shown.
35
kivy/input/factory.py
Normal file
35
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/input/motionevent.py
Normal file
587
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
|
||||
27
kivy/input/postproc/__init__.py
Normal file
27
kivy/input/postproc/__init__.py
Normal file
@@ -0,0 +1,27 @@
|
||||
'''
|
||||
Input Postprocessing
|
||||
====================
|
||||
|
||||
'''
|
||||
|
||||
__all__ = ('kivy_postproc_modules', )
|
||||
|
||||
import os
|
||||
from kivy.input.postproc.doubletap import InputPostprocDoubleTap
|
||||
from kivy.input.postproc.tripletap import InputPostprocTripleTap
|
||||
from kivy.input.postproc.ignorelist import InputPostprocIgnoreList
|
||||
from kivy.input.postproc.retaintouch import InputPostprocRetainTouch
|
||||
from kivy.input.postproc.dejitter import InputPostprocDejitter
|
||||
from kivy.input.postproc.calibration import InputPostprocCalibration
|
||||
|
||||
# Mapping of ID to module
|
||||
kivy_postproc_modules = {}
|
||||
|
||||
# Don't go further if we generate documentation
|
||||
if 'KIVY_DOC' not in os.environ:
|
||||
kivy_postproc_modules['calibration'] = InputPostprocCalibration()
|
||||
kivy_postproc_modules['retaintouch'] = InputPostprocRetainTouch()
|
||||
kivy_postproc_modules['ignorelist'] = InputPostprocIgnoreList()
|
||||
kivy_postproc_modules['doubletap'] = InputPostprocDoubleTap()
|
||||
kivy_postproc_modules['tripletap'] = InputPostprocTripleTap()
|
||||
kivy_postproc_modules['dejitter'] = InputPostprocDejitter()
|
||||
BIN
kivy/input/postproc/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
kivy/input/postproc/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/input/postproc/__pycache__/calibration.cpython-310.pyc
Normal file
BIN
kivy/input/postproc/__pycache__/calibration.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/input/postproc/__pycache__/dejitter.cpython-310.pyc
Normal file
BIN
kivy/input/postproc/__pycache__/dejitter.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/input/postproc/__pycache__/doubletap.cpython-310.pyc
Normal file
BIN
kivy/input/postproc/__pycache__/doubletap.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/input/postproc/__pycache__/ignorelist.cpython-310.pyc
Normal file
BIN
kivy/input/postproc/__pycache__/ignorelist.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/input/postproc/__pycache__/retaintouch.cpython-310.pyc
Normal file
BIN
kivy/input/postproc/__pycache__/retaintouch.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/input/postproc/__pycache__/tripletap.cpython-310.pyc
Normal file
BIN
kivy/input/postproc/__pycache__/tripletap.cpython-310.pyc
Normal file
Binary file not shown.
200
kivy/input/postproc/calibration.py
Normal file
200
kivy/input/postproc/calibration.py
Normal file
@@ -0,0 +1,200 @@
|
||||
'''
|
||||
Calibration
|
||||
===========
|
||||
|
||||
.. versionadded:: 1.9.0
|
||||
|
||||
Recalibrate input device to a specific range / offset.
|
||||
|
||||
Let's say you have 3 1080p displays, the 2 firsts are multitouch. By default,
|
||||
both will have mixed touch, the range will conflict with each others: the 0-1
|
||||
range will goes to 0-5760 px (remember, 3 * 1920 = 5760.)
|
||||
|
||||
To fix it, you need to manually reference them. For example::
|
||||
|
||||
[input]
|
||||
left = mtdev,/dev/input/event17
|
||||
middle = mtdev,/dev/input/event15
|
||||
# the right screen is just a display.
|
||||
|
||||
Then, you can use the calibration postproc module::
|
||||
|
||||
[postproc:calibration]
|
||||
left = xratio=0.3333
|
||||
middle = xratio=0.3333,xoffset=0.3333
|
||||
|
||||
Now, the touches from the left screen will be within 0-0.3333 range, and the
|
||||
touches from the middle screen will be within 0.3333-0.6666 range.
|
||||
|
||||
You can also match calibration rules to devices based on their provider type.
|
||||
This is useful when probesysfs is used to match devices. For example::
|
||||
|
||||
[input]
|
||||
mtdev_%(name)s = probesysfs,provider=mtdev
|
||||
|
||||
Then to apply calibration to any mtdev device, you can assign rules to the
|
||||
provider name enclosed by parentheses::
|
||||
|
||||
[postproc:calibration]
|
||||
(mtdev) = xratio=0.3333,xoffset=0.3333
|
||||
|
||||
Calibrating devices like this means the device's path doesn't need to be
|
||||
configured ahead of time. Note that with this method, all mtdev inputs will
|
||||
have the same calibration applied to them. For this reason, matching by
|
||||
provider will typically be useful when expecting only one input device.
|
||||
'''
|
||||
|
||||
__all__ = ('InputPostprocCalibration', )
|
||||
|
||||
from kivy.config import Config
|
||||
from kivy.logger import Logger
|
||||
from kivy.input import providers
|
||||
from kivy.input.factory import MotionEventFactory
|
||||
from kivy.input.motionevent import MotionEvent
|
||||
|
||||
|
||||
class InputPostprocCalibration(object):
|
||||
'''Recalibrate the inputs.
|
||||
|
||||
The configuration must go within a section named `postproc:calibration`.
|
||||
Within the section, you must have a line like::
|
||||
|
||||
devicename = param=value,param=value
|
||||
|
||||
If you wish to match by provider, you must have a line like::
|
||||
|
||||
(provider) = param=value,param=value
|
||||
|
||||
:Parameters:
|
||||
`xratio`: float
|
||||
Value to multiply X
|
||||
`yratio`: float
|
||||
Value to multiply Y
|
||||
`xoffset`: float
|
||||
Value to add to X
|
||||
`yoffset`: float
|
||||
Value to add to Y
|
||||
`auto`: str
|
||||
If set, then the touch is transformed from screen-relative
|
||||
to window-relative The value is used as an indication of
|
||||
screen size, e.g for fullHD:
|
||||
|
||||
auto=1920x1080
|
||||
|
||||
If present, this setting overrides all the others.
|
||||
This assumes the input device exactly covers the display
|
||||
area, if they are different, the computations will be wrong.
|
||||
|
||||
.. versionchanged:: 1.11.0
|
||||
Added `auto` parameter
|
||||
'''
|
||||
|
||||
def __init__(self):
|
||||
super(InputPostprocCalibration, self).__init__()
|
||||
self.devices = {}
|
||||
self.frame = 0
|
||||
self.provider_map = self._get_provider_map()
|
||||
if not Config.has_section('postproc:calibration'):
|
||||
return
|
||||
default_params = {'xoffset': 0, 'yoffset': 0, 'xratio': 1, 'yratio': 1}
|
||||
for device_key, params_str in Config.items('postproc:calibration'):
|
||||
params = default_params.copy()
|
||||
for param in params_str.split(','):
|
||||
param = param.strip()
|
||||
if not param:
|
||||
continue
|
||||
key, value = param.split('=', 1)
|
||||
if key == 'auto':
|
||||
width, height = [float(x) for x in value.split('x')]
|
||||
params['auto'] = width, height
|
||||
break
|
||||
if key not in ('xoffset', 'yoffset', 'xratio', 'yratio'):
|
||||
Logger.error(
|
||||
'Calibration: invalid key provided: {}'.format(key))
|
||||
params[key] = float(value)
|
||||
self.devices[device_key] = params
|
||||
|
||||
def _get_provider_map(self):
|
||||
"""Iterates through all registered input provider names and finds the
|
||||
respective MotionEvent subclass for each. Returns a dict of MotionEvent
|
||||
subclasses mapped to their provider name.
|
||||
"""
|
||||
provider_map = {}
|
||||
for input_provider in MotionEventFactory.list():
|
||||
if not hasattr(providers, input_provider):
|
||||
continue
|
||||
|
||||
p = getattr(providers, input_provider)
|
||||
for m in p.__all__:
|
||||
event = getattr(p, m)
|
||||
if issubclass(event, MotionEvent):
|
||||
provider_map[event] = input_provider
|
||||
|
||||
return provider_map
|
||||
|
||||
def _get_provider_key(self, event):
|
||||
"""Returns the provider key for the event if the provider is configured
|
||||
for calibration.
|
||||
"""
|
||||
input_type = self.provider_map.get(event.__class__)
|
||||
key = '({})'.format(input_type)
|
||||
if input_type and key in self.devices:
|
||||
return key
|
||||
|
||||
def process(self, events):
|
||||
# avoid doing any processing if there is no device to calibrate at all.
|
||||
if not self.devices:
|
||||
return events
|
||||
|
||||
self.frame += 1
|
||||
frame = self.frame
|
||||
to_remove = []
|
||||
for etype, event in events:
|
||||
# frame-based logic below doesn't account for
|
||||
# end events having been already processed
|
||||
if etype == 'end':
|
||||
continue
|
||||
|
||||
if event.device in self.devices:
|
||||
dev = event.device
|
||||
else:
|
||||
dev = self._get_provider_key(event)
|
||||
if not dev:
|
||||
continue
|
||||
|
||||
# some providers use the same event to update and end
|
||||
if 'calibration:frame' not in event.ud:
|
||||
event.ud['calibration:frame'] = frame
|
||||
elif event.ud['calibration:frame'] == frame:
|
||||
continue
|
||||
event.ud['calibration:frame'] = frame
|
||||
|
||||
params = self.devices[dev]
|
||||
if 'auto' in params:
|
||||
event.sx, event.sy = self.auto_calibrate(
|
||||
event.sx, event.sy, params['auto'])
|
||||
if not (0 <= event.sx <= 1 and 0 <= event.sy <= 1):
|
||||
to_remove.append((etype, event))
|
||||
else:
|
||||
event.sx = event.sx * params['xratio'] + params['xoffset']
|
||||
event.sy = event.sy * params['yratio'] + params['yoffset']
|
||||
|
||||
for event in to_remove:
|
||||
events.remove(event)
|
||||
|
||||
return events
|
||||
|
||||
def auto_calibrate(self, sx, sy, size):
|
||||
from kivy.core.window import Window as W
|
||||
WIDTH, HEIGHT = size
|
||||
|
||||
xratio = WIDTH / W.width
|
||||
yratio = HEIGHT / W.height
|
||||
|
||||
xoffset = - W.left / W.width
|
||||
yoffset = - (HEIGHT - W.top - W.height) / W.height
|
||||
|
||||
sx = sx * xratio + xoffset
|
||||
sy = sy * yratio + yoffset
|
||||
|
||||
return sx, sy
|
||||
74
kivy/input/postproc/dejitter.py
Normal file
74
kivy/input/postproc/dejitter.py
Normal file
@@ -0,0 +1,74 @@
|
||||
'''
|
||||
Dejitter
|
||||
========
|
||||
|
||||
Prevent blob jittering.
|
||||
|
||||
A problem that is often faced (esp. in optical MT setups) is that of
|
||||
jitterish BLOBs caused by bad camera characteristics. With this module
|
||||
you can get rid of that jitter. You just define a threshold
|
||||
`jitter_distance` in your config, and all touch movements that move
|
||||
the touch by less than the jitter distance are considered 'bad'
|
||||
movements caused by jitter and will be discarded.
|
||||
'''
|
||||
|
||||
__all__ = ('InputPostprocDejitter', )
|
||||
|
||||
from kivy.config import Config
|
||||
|
||||
|
||||
class InputPostprocDejitter(object):
|
||||
'''
|
||||
Get rid of jitterish BLOBs.
|
||||
Example::
|
||||
|
||||
[postproc]
|
||||
jitter_distance = 0.004
|
||||
jitter_ignore_devices = mouse,mactouch
|
||||
|
||||
:Configuration:
|
||||
`jitter_distance`: float
|
||||
A float in range 0-1.
|
||||
`jitter_ignore_devices`: string
|
||||
A comma-separated list of device identifiers that
|
||||
should not be processed by dejitter (because they're
|
||||
very precise already).
|
||||
'''
|
||||
|
||||
def __init__(self):
|
||||
self.jitterdist = Config.getfloat('postproc', 'jitter_distance')
|
||||
ignore_devices = Config.get('postproc', 'jitter_ignore_devices')
|
||||
self.ignore_devices = ignore_devices.split(',')
|
||||
self.last_touches = {}
|
||||
|
||||
def taxicab_distance(self, p, q):
|
||||
# Get the taxicab/manhattan/citiblock distance for efficiency reasons
|
||||
return abs(p[0] - q[0]) + abs(p[1] - q[1])
|
||||
|
||||
def process(self, events):
|
||||
if not self.jitterdist:
|
||||
return events
|
||||
processed = []
|
||||
for etype, touch in events:
|
||||
if not touch.is_touch:
|
||||
continue
|
||||
if touch.device in self.ignore_devices:
|
||||
processed.append((etype, touch))
|
||||
continue
|
||||
if etype == 'begin':
|
||||
self.last_touches[touch.id] = touch.spos
|
||||
if etype == 'end':
|
||||
if touch.id in self.last_touches:
|
||||
del self.last_touches[touch.id]
|
||||
if etype != 'update':
|
||||
processed.append((etype, touch))
|
||||
continue
|
||||
# Check whether the touch moved more than the jitter distance
|
||||
last_spos = self.last_touches[touch.id]
|
||||
dist = self.taxicab_distance(last_spos, touch.spos)
|
||||
if dist > self.jitterdist:
|
||||
# Only if the touch has moved more than the jitter dist we take
|
||||
# it into account and dispatch it. Otherwise suppress it.
|
||||
self.last_touches[touch.id] = touch.spos
|
||||
processed.append((etype, touch))
|
||||
return processed
|
||||
101
kivy/input/postproc/doubletap.py
Normal file
101
kivy/input/postproc/doubletap.py
Normal file
@@ -0,0 +1,101 @@
|
||||
'''
|
||||
Double Tap
|
||||
==========
|
||||
|
||||
Search touch for a double tap
|
||||
'''
|
||||
|
||||
__all__ = ('InputPostprocDoubleTap', )
|
||||
|
||||
from time import time
|
||||
from kivy.config import Config
|
||||
from kivy.vector import Vector
|
||||
|
||||
|
||||
class InputPostprocDoubleTap(object):
|
||||
'''
|
||||
InputPostProcDoubleTap is a post-processor to check if
|
||||
a touch is a double tap or not.
|
||||
Double tap can be configured in the Kivy config file::
|
||||
|
||||
[postproc]
|
||||
double_tap_time = 250
|
||||
double_tap_distance = 20
|
||||
|
||||
Distance parameter is in the range 0-1000 and time is in milliseconds.
|
||||
'''
|
||||
|
||||
def __init__(self):
|
||||
dist = Config.getint('postproc', 'double_tap_distance')
|
||||
self.double_tap_distance = dist / 1000.0
|
||||
tap_time = Config.getint('postproc', 'double_tap_time')
|
||||
self.double_tap_time = tap_time / 1000.0
|
||||
self.touches = {}
|
||||
|
||||
def find_double_tap(self, ref):
|
||||
'''Find a double tap touch within self.touches.
|
||||
The touch must be not a previous double tap and the distance must be
|
||||
within the specified threshold. Additionally, the touch profiles
|
||||
must be the same kind of touch.
|
||||
'''
|
||||
ref_button = None
|
||||
if 'button' in ref.profile:
|
||||
ref_button = ref.button
|
||||
|
||||
for touchid in self.touches:
|
||||
if ref.uid == touchid:
|
||||
continue
|
||||
etype, touch = self.touches[touchid]
|
||||
if etype != 'end':
|
||||
continue
|
||||
if touch.is_double_tap:
|
||||
continue
|
||||
distance = Vector.distance(
|
||||
Vector(ref.sx, ref.sy),
|
||||
Vector(touch.osx, touch.osy))
|
||||
if distance > self.double_tap_distance:
|
||||
continue
|
||||
if touch.is_mouse_scrolling or ref.is_mouse_scrolling:
|
||||
continue
|
||||
touch_button = None
|
||||
if 'button' in touch.profile:
|
||||
touch_button = touch.button
|
||||
if touch_button != ref_button:
|
||||
continue
|
||||
touch.double_tap_distance = distance
|
||||
return touch
|
||||
|
||||
def process(self, events):
|
||||
if self.double_tap_distance == 0 or self.double_tap_time == 0:
|
||||
return events
|
||||
# first, check if a touch down have a double tap
|
||||
for etype, touch in events:
|
||||
if not touch.is_touch:
|
||||
continue
|
||||
if etype == 'begin':
|
||||
double_tap = self.find_double_tap(touch)
|
||||
if double_tap:
|
||||
touch.is_double_tap = True
|
||||
tap_time = touch.time_start - double_tap.time_start
|
||||
touch.double_tap_time = tap_time
|
||||
distance = double_tap.double_tap_distance
|
||||
touch.double_tap_distance = distance
|
||||
|
||||
# add the touch internally
|
||||
self.touches[touch.uid] = (etype, touch)
|
||||
|
||||
# second, check if up-touch is timeout for double tap
|
||||
time_current = time()
|
||||
to_delete = []
|
||||
for touchid in self.touches.keys():
|
||||
etype, touch = self.touches[touchid]
|
||||
if etype != 'end':
|
||||
continue
|
||||
if time_current - touch.time_start < self.double_tap_time:
|
||||
continue
|
||||
to_delete.append(touchid)
|
||||
|
||||
for touchid in to_delete:
|
||||
del self.touches[touchid]
|
||||
|
||||
return events
|
||||
47
kivy/input/postproc/ignorelist.py
Normal file
47
kivy/input/postproc/ignorelist.py
Normal file
@@ -0,0 +1,47 @@
|
||||
'''
|
||||
Ignore list
|
||||
===========
|
||||
|
||||
Ignore touch on some areas of the screen
|
||||
'''
|
||||
|
||||
__all__ = ('InputPostprocIgnoreList', )
|
||||
|
||||
from kivy.config import Config
|
||||
from kivy.utils import strtotuple
|
||||
|
||||
|
||||
class InputPostprocIgnoreList(object):
|
||||
'''
|
||||
InputPostprocIgnoreList is a post-processor which removes touches in the
|
||||
Ignore list. The Ignore list can be configured in the Kivy config file::
|
||||
|
||||
[postproc]
|
||||
# Format: [(xmin, ymin, xmax, ymax), ...]
|
||||
ignore = [(0.1, 0.1, 0.15, 0.15)]
|
||||
|
||||
The Ignore list coordinates are in the range 0-1, not in screen pixels.
|
||||
'''
|
||||
|
||||
def __init__(self):
|
||||
self.ignore_list = strtotuple(Config.get('postproc', 'ignore'))
|
||||
|
||||
def collide_ignore(self, touch):
|
||||
x, y = touch.sx, touch.sy
|
||||
for l in self.ignore_list:
|
||||
xmin, ymin, xmax, ymax = l
|
||||
if x > xmin and x < xmax and y > ymin and y < ymax:
|
||||
return True
|
||||
|
||||
def process(self, events):
|
||||
if not len(self.ignore_list):
|
||||
return events
|
||||
for etype, touch in events:
|
||||
if not touch.is_touch:
|
||||
continue
|
||||
if etype != 'begin':
|
||||
continue
|
||||
if self.collide_ignore(touch):
|
||||
touch.ud.__pp_ignore__ = True
|
||||
return [(etype, touch) for etype, touch in events
|
||||
if '__pp_ignore__' not in touch.ud]
|
||||
93
kivy/input/postproc/retaintouch.py
Normal file
93
kivy/input/postproc/retaintouch.py
Normal file
@@ -0,0 +1,93 @@
|
||||
'''
|
||||
Retain Touch
|
||||
============
|
||||
|
||||
Reuse touch to counter lost finger behavior
|
||||
'''
|
||||
|
||||
__all__ = ('InputPostprocRetainTouch', )
|
||||
|
||||
from kivy.config import Config
|
||||
from kivy.vector import Vector
|
||||
import time
|
||||
|
||||
|
||||
class InputPostprocRetainTouch(object):
|
||||
'''
|
||||
InputPostprocRetainTouch is a post-processor to delay the 'up' event of a
|
||||
touch, to reuse it under certains conditions. This module is designed to
|
||||
prevent lost finger touches on some hardware/setups.
|
||||
|
||||
Retain touch can be configured in the Kivy config file::
|
||||
|
||||
[postproc]
|
||||
retain_time = 100
|
||||
retain_distance = 50
|
||||
|
||||
The distance parameter is in the range 0-1000 and time is in milliseconds.
|
||||
'''
|
||||
|
||||
def __init__(self):
|
||||
self.timeout = Config.getint('postproc', 'retain_time') / 1000.0
|
||||
self.distance = Config.getint('postproc', 'retain_distance') / 1000.0
|
||||
self._available = []
|
||||
self._links = {}
|
||||
|
||||
def process(self, events):
|
||||
# check if module is disabled
|
||||
if self.timeout == 0:
|
||||
return events
|
||||
|
||||
d = time.time()
|
||||
for etype, touch in events[:]:
|
||||
if not touch.is_touch:
|
||||
continue
|
||||
if etype == 'end':
|
||||
events.remove((etype, touch))
|
||||
if touch.uid in self._links:
|
||||
selection = self._links[touch.uid]
|
||||
selection.ud.__pp_retain_time__ = d
|
||||
self._available.append(selection)
|
||||
del self._links[touch.uid]
|
||||
else:
|
||||
touch.ud.__pp_retain_time__ = d
|
||||
self._available.append(touch)
|
||||
elif etype == 'update':
|
||||
if touch.uid in self._links:
|
||||
selection = self._links[touch.uid]
|
||||
selection.x = touch.x
|
||||
selection.y = touch.y
|
||||
selection.sx = touch.sx
|
||||
selection.sy = touch.sy
|
||||
events.remove((etype, touch))
|
||||
events.append((etype, selection))
|
||||
else:
|
||||
pass
|
||||
elif etype == 'begin':
|
||||
# new touch, found the nearest one
|
||||
selection = None
|
||||
selection_distance = 99999
|
||||
for touch2 in self._available:
|
||||
touch_distance = Vector(touch2.spos).distance(touch.spos)
|
||||
if touch_distance > self.distance:
|
||||
continue
|
||||
if touch2.__class__ != touch.__class__:
|
||||
continue
|
||||
if touch_distance < selection_distance:
|
||||
# eligible for continuation
|
||||
selection_distance = touch_distance
|
||||
selection = touch2
|
||||
if selection is None:
|
||||
continue
|
||||
|
||||
self._links[touch.uid] = selection
|
||||
self._available.remove(selection)
|
||||
events.remove((etype, touch))
|
||||
|
||||
for touch in self._available[:]:
|
||||
t = touch.ud.__pp_retain_time__
|
||||
if d - t > self.timeout:
|
||||
self._available.remove(touch)
|
||||
events.append(('end', touch))
|
||||
|
||||
return events
|
||||
106
kivy/input/postproc/tripletap.py
Normal file
106
kivy/input/postproc/tripletap.py
Normal file
@@ -0,0 +1,106 @@
|
||||
'''
|
||||
Triple Tap
|
||||
==========
|
||||
|
||||
.. versionadded:: 1.7.0
|
||||
|
||||
Search touch for a triple tap
|
||||
'''
|
||||
|
||||
__all__ = ('InputPostprocTripleTap', )
|
||||
|
||||
from time import time
|
||||
from kivy.config import Config
|
||||
from kivy.vector import Vector
|
||||
|
||||
|
||||
class InputPostprocTripleTap(object):
|
||||
'''
|
||||
InputPostProcTripleTap is a post-processor to check if
|
||||
a touch is a triple tap or not.
|
||||
Triple tap can be configured in the Kivy config file::
|
||||
|
||||
[postproc]
|
||||
triple_tap_time = 250
|
||||
triple_tap_distance = 20
|
||||
|
||||
The distance parameter is in the range 0-1000 and time is in milliseconds.
|
||||
'''
|
||||
|
||||
def __init__(self):
|
||||
dist = Config.getint('postproc', 'triple_tap_distance')
|
||||
self.triple_tap_distance = dist / 1000.0
|
||||
time = Config.getint('postproc', 'triple_tap_time')
|
||||
self.triple_tap_time = time / 1000.0
|
||||
self.touches = {}
|
||||
|
||||
def find_triple_tap(self, ref):
|
||||
'''Find a triple tap touch within *self.touches*.
|
||||
The touch must be not be a previous triple tap and the distance
|
||||
must be 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
|
||||
40
kivy/input/provider.py
Normal file
40
kivy/input/provider.py
Normal file
@@ -0,0 +1,40 @@
|
||||
'''
|
||||
Motion Event Provider
|
||||
=====================
|
||||
|
||||
Abstract class for the implementation of a
|
||||
:class:`~kivy.input.motionevent.MotionEvent`
|
||||
provider. The implementation must support the
|
||||
:meth:`~MotionEventProvider.start`, :meth:`~MotionEventProvider.stop` and
|
||||
:meth:`~MotionEventProvider.update` methods.
|
||||
'''
|
||||
|
||||
__all__ = ('MotionEventProvider', )
|
||||
|
||||
|
||||
class MotionEventProvider(object):
|
||||
'''Base class for a provider.
|
||||
'''
|
||||
|
||||
def __init__(self, device, args):
|
||||
self.device = device
|
||||
if self.__class__ == MotionEventProvider:
|
||||
raise NotImplementedError('class MotionEventProvider is abstract')
|
||||
|
||||
def start(self):
|
||||
'''Start the provider. This method is automatically called when the
|
||||
application is started and if the configuration uses the current
|
||||
provider.
|
||||
'''
|
||||
pass
|
||||
|
||||
def stop(self):
|
||||
'''Stop the provider.
|
||||
'''
|
||||
pass
|
||||
|
||||
def update(self, dispatch_fn):
|
||||
'''Update the provider and dispatch all the new touch events though the
|
||||
`dispatch_fn` argument.
|
||||
'''
|
||||
pass
|
||||
68
kivy/input/providers/__init__.py
Normal file
68
kivy/input/providers/__init__.py
Normal file
@@ -0,0 +1,68 @@
|
||||
# pylint: disable=W0611
|
||||
'''
|
||||
Providers
|
||||
=========
|
||||
|
||||
'''
|
||||
|
||||
import os
|
||||
|
||||
from kivy.utils import platform as core_platform
|
||||
from kivy.logger import Logger
|
||||
from kivy.setupconfig import USE_SDL2
|
||||
|
||||
import kivy.input.providers.tuio
|
||||
import kivy.input.providers.mouse
|
||||
|
||||
platform = core_platform
|
||||
|
||||
if platform == 'win' or 'KIVY_DOC' in os.environ:
|
||||
try:
|
||||
import kivy.input.providers.wm_touch
|
||||
import kivy.input.providers.wm_pen
|
||||
except:
|
||||
err = 'Input: WM_Touch/WM_Pen not supported by your version of Windows'
|
||||
Logger.warning(err)
|
||||
|
||||
if platform == 'macosx' or 'KIVY_DOC' in os.environ:
|
||||
try:
|
||||
import kivy.input.providers.mactouch
|
||||
except:
|
||||
err = 'Input: MacMultitouchSupport is not supported by your system'
|
||||
Logger.exception(err)
|
||||
|
||||
if platform == 'linux' or 'KIVY_DOC' in os.environ:
|
||||
try:
|
||||
import kivy.input.providers.probesysfs
|
||||
except:
|
||||
err = 'Input: ProbeSysfs is not supported by your version of linux'
|
||||
Logger.exception(err)
|
||||
try:
|
||||
import kivy.input.providers.mtdev
|
||||
except:
|
||||
err = 'Input: MTDev is not supported by your version of linux'
|
||||
Logger.exception(err)
|
||||
try:
|
||||
import kivy.input.providers.hidinput
|
||||
except:
|
||||
err = 'Input: HIDInput is not supported by your version of linux'
|
||||
Logger.exception(err)
|
||||
try:
|
||||
import kivy.input.providers.linuxwacom
|
||||
except:
|
||||
err = 'Input: LinuxWacom is not supported by your version of linux'
|
||||
Logger.exception(err)
|
||||
|
||||
if (platform == 'android' and not USE_SDL2) or 'KIVY_DOC' in os.environ:
|
||||
try:
|
||||
import kivy.input.providers.androidjoystick
|
||||
except:
|
||||
err = 'Input: AndroidJoystick is not supported by your version ' \
|
||||
'of linux'
|
||||
Logger.exception(err)
|
||||
|
||||
try:
|
||||
import kivy.input.providers.leapfinger # NOQA
|
||||
except:
|
||||
err = 'Input: LeapFinger is not available on your system'
|
||||
Logger.exception(err)
|
||||
BIN
kivy/input/providers/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
kivy/input/providers/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/input/providers/__pycache__/androidjoystick.cpython-310.pyc
Normal file
BIN
kivy/input/providers/__pycache__/androidjoystick.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/input/providers/__pycache__/hidinput.cpython-310.pyc
Normal file
BIN
kivy/input/providers/__pycache__/hidinput.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/input/providers/__pycache__/leapfinger.cpython-310.pyc
Normal file
BIN
kivy/input/providers/__pycache__/leapfinger.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/input/providers/__pycache__/linuxwacom.cpython-310.pyc
Normal file
BIN
kivy/input/providers/__pycache__/linuxwacom.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/input/providers/__pycache__/mactouch.cpython-310.pyc
Normal file
BIN
kivy/input/providers/__pycache__/mactouch.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/input/providers/__pycache__/mouse.cpython-310.pyc
Normal file
BIN
kivy/input/providers/__pycache__/mouse.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/input/providers/__pycache__/mtdev.cpython-310.pyc
Normal file
BIN
kivy/input/providers/__pycache__/mtdev.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/input/providers/__pycache__/probesysfs.cpython-310.pyc
Normal file
BIN
kivy/input/providers/__pycache__/probesysfs.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/input/providers/__pycache__/tuio.cpython-310.pyc
Normal file
BIN
kivy/input/providers/__pycache__/tuio.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/input/providers/__pycache__/wm_common.cpython-310.pyc
Normal file
BIN
kivy/input/providers/__pycache__/wm_common.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/input/providers/__pycache__/wm_pen.cpython-310.pyc
Normal file
BIN
kivy/input/providers/__pycache__/wm_pen.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/input/providers/__pycache__/wm_touch.cpython-310.pyc
Normal file
BIN
kivy/input/providers/__pycache__/wm_touch.cpython-310.pyc
Normal file
Binary file not shown.
117
kivy/input/providers/androidjoystick.py
Normal file
117
kivy/input/providers/androidjoystick.py
Normal file
@@ -0,0 +1,117 @@
|
||||
# pylint: disable=W0611
|
||||
'''
|
||||
Android Joystick Input Provider
|
||||
===============================
|
||||
|
||||
This module is based on the PyGame JoyStick Input Provider. For more
|
||||
information, please refer to
|
||||
`<http://www.pygame.org/docs/ref/joystick.html>`_
|
||||
|
||||
|
||||
'''
|
||||
__all__ = ('AndroidMotionEventProvider', )
|
||||
|
||||
import os
|
||||
|
||||
try:
|
||||
import android # NOQA
|
||||
except ImportError:
|
||||
if 'KIVY_DOC' not in os.environ:
|
||||
raise Exception('android lib not found.')
|
||||
|
||||
from kivy.logger import Logger
|
||||
from kivy.input.provider import MotionEventProvider
|
||||
from kivy.input.factory import MotionEventFactory
|
||||
from kivy.input.shape import ShapeRect
|
||||
from kivy.input.motionevent import MotionEvent
|
||||
if 'KIVY_DOC' not in os.environ:
|
||||
import pygame.joystick
|
||||
|
||||
|
||||
class AndroidMotionEvent(MotionEvent):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault('is_touch', True)
|
||||
kwargs.setdefault('type_id', 'touch')
|
||||
super().__init__(*args, **kwargs)
|
||||
self.profile = ['pos', 'pressure', 'shape']
|
||||
|
||||
def depack(self, args):
|
||||
self.sx, self.sy, self.pressure, radius = args
|
||||
self.shape = ShapeRect()
|
||||
self.shape.width = radius
|
||||
self.shape.height = radius
|
||||
super().depack(args)
|
||||
|
||||
|
||||
class AndroidMotionEventProvider(MotionEventProvider):
|
||||
|
||||
def __init__(self, device, args):
|
||||
super(AndroidMotionEventProvider, self).__init__(device, args)
|
||||
self.joysticks = []
|
||||
self.touches = {}
|
||||
self.uid = 0
|
||||
self.window = None
|
||||
|
||||
def create_joystick(self, index):
|
||||
Logger.info('Android: create joystick <%d>' % index)
|
||||
js = pygame.joystick.Joystick(index)
|
||||
js.init()
|
||||
if js.get_numbuttons() == 0:
|
||||
Logger.info('Android: discard joystick <%d> cause no button' %
|
||||
index)
|
||||
return
|
||||
self.joysticks.append(js)
|
||||
|
||||
def start(self):
|
||||
pygame.joystick.init()
|
||||
Logger.info('Android: found %d joystick' % pygame.joystick.get_count())
|
||||
for i in range(pygame.joystick.get_count()):
|
||||
self.create_joystick(i)
|
||||
|
||||
def stop(self):
|
||||
self.joysticks = []
|
||||
|
||||
def update(self, dispatch_fn):
|
||||
if not self.window:
|
||||
from kivy.core.window import Window
|
||||
self.window = Window
|
||||
w, h = self.window.system_size
|
||||
touches = self.touches
|
||||
for joy in self.joysticks:
|
||||
jid = joy.get_id()
|
||||
pressed = joy.get_button(0)
|
||||
if pressed or jid in touches:
|
||||
x = joy.get_axis(0) * 32768. / w
|
||||
y = 1. - (joy.get_axis(1) * 32768. / h)
|
||||
|
||||
# python for android do * 1000.
|
||||
pressure = joy.get_axis(2) / 1000.
|
||||
radius = joy.get_axis(3) / 1000.
|
||||
|
||||
# new touch ?
|
||||
if pressed and jid not in touches:
|
||||
self.uid += 1
|
||||
touch = AndroidMotionEvent(self.device, self.uid,
|
||||
[x, y, pressure, radius])
|
||||
touches[jid] = touch
|
||||
dispatch_fn('begin', touch)
|
||||
# update touch
|
||||
elif pressed:
|
||||
touch = touches[jid]
|
||||
# avoid same touch position
|
||||
if (touch.sx == x and touch.sy == y and
|
||||
touch.pressure == pressure):
|
||||
continue
|
||||
touch.move([x, y, pressure, radius])
|
||||
dispatch_fn('update', touch)
|
||||
# disappear
|
||||
elif not pressed and jid in touches:
|
||||
touch = touches[jid]
|
||||
touch.move([x, y, pressure, radius])
|
||||
touch.update_time_end()
|
||||
dispatch_fn('end', touch)
|
||||
touches.pop(jid)
|
||||
|
||||
|
||||
MotionEventFactory.register('android', AndroidMotionEventProvider)
|
||||
778
kivy/input/providers/hidinput.py
Normal file
778
kivy/input/providers/hidinput.py
Normal file
@@ -0,0 +1,778 @@
|
||||
# coding utf-8
|
||||
'''
|
||||
Native support for HID input from the linux kernel
|
||||
==================================================
|
||||
|
||||
Support starts from 2.6.32-ubuntu, or 2.6.34.
|
||||
|
||||
To configure HIDInput, add this to your configuration::
|
||||
|
||||
[input]
|
||||
# devicename = hidinput,/dev/input/eventXX
|
||||
# example with Stantum MTP4.3" screen
|
||||
stantum = hidinput,/dev/input/event2
|
||||
|
||||
.. note::
|
||||
You must have read access to the input event.
|
||||
|
||||
You can use a custom range for the X, Y and pressure values.
|
||||
For some drivers, the range reported is invalid.
|
||||
To fix that, you can add these options to the argument line:
|
||||
|
||||
* invert_x : 1 to invert X axis
|
||||
* invert_y : 1 to invert Y axis
|
||||
* min_position_x : X relative minimum
|
||||
* max_position_x : X relative maximum
|
||||
* min_position_y : Y relative minimum
|
||||
* max_position_y : Y relative maximum
|
||||
* min_abs_x : X absolute minimum
|
||||
* min_abs_y : Y absolute minimum
|
||||
* max_abs_x : X absolute maximum
|
||||
* max_abs_y : Y absolute maximum
|
||||
* min_pressure : pressure minimum
|
||||
* max_pressure : pressure maximum
|
||||
* rotation : rotate the input coordinate (0, 90, 180, 270)
|
||||
|
||||
For example, on the Asus T101M, the touchscreen reports a range from 0-4095 for
|
||||
the X and Y values, but the real values are in a range from 0-32768. To correct
|
||||
this, you can add the following to the configuration::
|
||||
|
||||
[input]
|
||||
t101m = hidinput,/dev/input/event7,max_position_x=32768,\
|
||||
max_position_y=32768
|
||||
|
||||
.. versionadded:: 1.9.1
|
||||
|
||||
`rotation` configuration token added.
|
||||
|
||||
'''
|
||||
import os
|
||||
from kivy.input.motionevent import MotionEvent
|
||||
from kivy.input.shape import ShapeRect
|
||||
|
||||
__all__ = ('HIDInputMotionEventProvider', 'HIDMotionEvent')
|
||||
|
||||
# late imports
|
||||
Window = None
|
||||
Keyboard = None
|
||||
|
||||
|
||||
class HIDMotionEvent(MotionEvent):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault('is_touch', True)
|
||||
kwargs.setdefault('type_id', 'touch')
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def depack(self, args):
|
||||
self.sx = args['x']
|
||||
self.sy = args['y']
|
||||
self.profile = ['pos']
|
||||
if 'size_w' in args and 'size_h' in args:
|
||||
self.shape = ShapeRect()
|
||||
self.shape.width = args['size_w']
|
||||
self.shape.height = args['size_h']
|
||||
self.profile.append('shape')
|
||||
if 'pressure' in args:
|
||||
self.pressure = args['pressure']
|
||||
self.profile.append('pressure')
|
||||
if 'button' in args:
|
||||
self.button = args['button']
|
||||
self.profile.append('button')
|
||||
super().depack(args)
|
||||
|
||||
def __str__(self):
|
||||
return '<HIDMotionEvent id=%d pos=(%f, %f) device=%s>' \
|
||||
% (self.id, self.sx, self.sy, self.device)
|
||||
|
||||
|
||||
if 'KIVY_DOC' in os.environ:
|
||||
# documentation hack
|
||||
HIDInputMotionEventProvider = None
|
||||
|
||||
else:
|
||||
import threading
|
||||
import collections
|
||||
import struct
|
||||
import fcntl
|
||||
from kivy.input.provider import MotionEventProvider
|
||||
from kivy.input.factory import MotionEventFactory
|
||||
from kivy.logger import Logger
|
||||
|
||||
#
|
||||
# This part is taken from linux-source-2.6.32/include/linux/input.h
|
||||
#
|
||||
|
||||
# Event types
|
||||
EV_SYN = 0x00
|
||||
EV_KEY = 0x01
|
||||
EV_REL = 0x02
|
||||
EV_ABS = 0x03
|
||||
EV_MSC = 0x04
|
||||
EV_SW = 0x05
|
||||
EV_LED = 0x11
|
||||
EV_SND = 0x12
|
||||
EV_REP = 0x14
|
||||
EV_FF = 0x15
|
||||
EV_PWR = 0x16
|
||||
EV_FF_STATUS = 0x17
|
||||
EV_MAX = 0x1f
|
||||
EV_CNT = (EV_MAX + 1)
|
||||
|
||||
KEY_MAX = 0x2ff
|
||||
|
||||
# Synchronization events
|
||||
SYN_REPORT = 0
|
||||
SYN_CONFIG = 1
|
||||
SYN_MT_REPORT = 2
|
||||
|
||||
# Misc events
|
||||
MSC_SERIAL = 0x00
|
||||
MSC_PULSELED = 0x01
|
||||
MSC_GESTURE = 0x02
|
||||
MSC_RAW = 0x03
|
||||
MSC_SCAN = 0x04
|
||||
MSC_MAX = 0x07
|
||||
MSC_CNT = (MSC_MAX + 1)
|
||||
|
||||
ABS_X = 0x00
|
||||
ABS_Y = 0x01
|
||||
ABS_PRESSURE = 0x18
|
||||
ABS_MT_TOUCH_MAJOR = 0x30 # Major axis of touching ellipse
|
||||
ABS_MT_TOUCH_MINOR = 0x31 # Minor axis (omit if circular)
|
||||
ABS_MT_WIDTH_MAJOR = 0x32 # Major axis of approaching ellipse
|
||||
ABS_MT_WIDTH_MINOR = 0x33 # Minor axis (omit if circular)
|
||||
ABS_MT_ORIENTATION = 0x34 # Ellipse orientation
|
||||
ABS_MT_POSITION_X = 0x35 # Center X ellipse position
|
||||
ABS_MT_POSITION_Y = 0x36 # Center Y ellipse position
|
||||
ABS_MT_TOOL_TYPE = 0x37 # Type of touching device
|
||||
ABS_MT_BLOB_ID = 0x38 # Group a set of packets as a blob
|
||||
ABS_MT_TRACKING_ID = 0x39 # Unique ID of initiated contact
|
||||
ABS_MT_PRESSURE = 0x3a # Pressure on contact area
|
||||
|
||||
# some ioctl base (with 0 value)
|
||||
EVIOCGNAME = 2147501318
|
||||
EVIOCGBIT = 2147501344
|
||||
EVIOCGABS = 2149074240
|
||||
|
||||
keyboard_keys = {
|
||||
0x29: ('`', '~'),
|
||||
0x02: ('1', '!'),
|
||||
0x03: ('2', '@'),
|
||||
0x04: ('3', '#'),
|
||||
0x05: ('4', '$'),
|
||||
0x06: ('5', '%'),
|
||||
0x07: ('6', '^'),
|
||||
0x08: ('7', '&'),
|
||||
0x09: ('8', '*'),
|
||||
0x0a: ('9', '('),
|
||||
0x0b: ('0', ')'),
|
||||
0x0c: ('-', '_'),
|
||||
0x0d: ('=', '+'),
|
||||
0x0e: ('backspace', ),
|
||||
0x0f: ('tab', ),
|
||||
0x10: ('q', 'Q'),
|
||||
0x11: ('w', 'W'),
|
||||
0x12: ('e', 'E'),
|
||||
0x13: ('r', 'R'),
|
||||
0x14: ('t', 'T'),
|
||||
0x15: ('y', 'Y'),
|
||||
0x16: ('u', 'U'),
|
||||
0x17: ('i', 'I'),
|
||||
0x18: ('o', 'O'),
|
||||
0x19: ('p', 'P'),
|
||||
0x1a: ('[', '{'),
|
||||
0x1b: (']', '}'),
|
||||
0x2b: ('\\', '|'),
|
||||
0x3a: ('capslock', ),
|
||||
0x1e: ('a', 'A'),
|
||||
0x1f: ('s', 'S'),
|
||||
0x20: ('d', 'D'),
|
||||
0x21: ('f', 'F'),
|
||||
0x22: ('g', 'G'),
|
||||
0x23: ('h', 'H'),
|
||||
0x24: ('j', 'J'),
|
||||
0x25: ('k', 'K'),
|
||||
0x26: ('l', 'L'),
|
||||
0x27: (';', ':'),
|
||||
0x28: ("'", '"'),
|
||||
0xff: ('non-US-1', ),
|
||||
0x1c: ('enter', ),
|
||||
0x2a: ('shift', ),
|
||||
0x2c: ('z', 'Z'),
|
||||
0x2d: ('x', 'X'),
|
||||
0x2e: ('c', 'C'),
|
||||
0x2f: ('v', 'V'),
|
||||
0x30: ('b', 'B'),
|
||||
0x31: ('n', 'N'),
|
||||
0x32: ('m', 'M'),
|
||||
0x33: (',', '<'),
|
||||
0x34: ('.', '>'),
|
||||
0x35: ('/', '?'),
|
||||
0x36: ('shift', ),
|
||||
0x56: ('pipe', ),
|
||||
0x1d: ('lctrl', ),
|
||||
0x7D: ('super', ),
|
||||
0x38: ('alt', ),
|
||||
0x39: ('spacebar', ),
|
||||
0x64: ('alt-gr', ),
|
||||
0x7e: ('super', ),
|
||||
0x7f: ('compose', ),
|
||||
0x61: ('rctrl', ),
|
||||
0x45: ('numlock', ),
|
||||
0x47: ('numpad7', 'home'),
|
||||
0x4b: ('numpad4', 'left'),
|
||||
0x4f: ('numpad1', 'end'),
|
||||
0x48: ('numpad8', 'up'),
|
||||
0x4c: ('numpad5', ),
|
||||
0x50: ('numpad2', 'down'),
|
||||
0x52: ('numpad0', 'insert'),
|
||||
0x37: ('numpadmul', ),
|
||||
0x62: ('numpaddivide', ),
|
||||
0x49: ('numpad9', 'pageup'),
|
||||
0x4d: ('numpad6', 'right'),
|
||||
0x51: ('numpad3', 'pagedown'),
|
||||
0x53: ('numpaddecimal', 'delete'),
|
||||
0x4a: ('numpadsubstract', ),
|
||||
0x4e: ('numpadadd', ),
|
||||
0x60: ('numpadenter', ),
|
||||
0x01: ('escape', ),
|
||||
0x3b: ('f1', ),
|
||||
0x3c: ('f2', ),
|
||||
0x3d: ('f3', ),
|
||||
0x3e: ('f4', ),
|
||||
0x3f: ('f5', ),
|
||||
0x40: ('f6', ),
|
||||
0x41: ('f7', ),
|
||||
0x42: ('f8', ),
|
||||
0x43: ('f9', ),
|
||||
0x44: ('f10', ),
|
||||
0x57: ('f11', ),
|
||||
0x58: ('f12', ),
|
||||
0x54: ('Alt+SysRq', ),
|
||||
0x46: ('Screenlock', ),
|
||||
0x67: ('up', ),
|
||||
0x6c: ('down', ),
|
||||
0x69: ('left', ),
|
||||
0x6a: ('right', ),
|
||||
0x6e: ('insert', ),
|
||||
0x6f: ('delete', ),
|
||||
0x66: ('home', ),
|
||||
0x6b: ('end', ),
|
||||
0x68: ('pageup', ),
|
||||
0x6d: ('pagedown', ),
|
||||
0x63: ('print', ),
|
||||
0x77: ('pause', ),
|
||||
|
||||
|
||||
# TODO combinations
|
||||
# e0-37 PrtScr
|
||||
# e0-46 Ctrl+Break
|
||||
# e0-5b LWin (USB: LGUI)
|
||||
# e0-5c RWin (USB: RGUI)
|
||||
# e0-5d Menu
|
||||
# e0-5f Sleep
|
||||
# e0-5e Power
|
||||
# e0-63 Wake
|
||||
# e0-38 RAlt
|
||||
# e0-1d RCtrl
|
||||
# e0-52 Insert
|
||||
# e0-53 Delete
|
||||
# e0-47 Home
|
||||
# e0-4f End
|
||||
# e0-49 PgUp
|
||||
# e0-51 PgDn
|
||||
# e0-4b Left
|
||||
# e0-48 Up
|
||||
# e0-50 Down
|
||||
# e0-4d Right
|
||||
# e0-35 KP-/
|
||||
# e0-1c KP-Enter
|
||||
# e1-1d-45 77 Pause
|
||||
}
|
||||
|
||||
keys_str = {
|
||||
'spacebar': ' ',
|
||||
'tab': ' ',
|
||||
'shift': '',
|
||||
'alt': '',
|
||||
'ctrl': '',
|
||||
'escape': '',
|
||||
'numpad1': '1',
|
||||
'numpad2': '2',
|
||||
'numpad3': '3',
|
||||
'numpad4': '4',
|
||||
'numpad5': '5',
|
||||
'numpad6': '6',
|
||||
'numpad7': '7',
|
||||
'numpad8': '8',
|
||||
'numpad9': '9',
|
||||
'numpad0': '0',
|
||||
'numpadmul': '*',
|
||||
'numpaddivide': '/',
|
||||
'numpadadd': '+',
|
||||
'numpaddecimal': '.',
|
||||
'numpadsubstract': '-',
|
||||
}
|
||||
|
||||
# sizeof(struct input_event)
|
||||
struct_input_event_sz = struct.calcsize('LLHHi')
|
||||
struct_input_absinfo_sz = struct.calcsize('iiiiii')
|
||||
sz_l = struct.calcsize('Q')
|
||||
|
||||
class HIDInputMotionEventProvider(MotionEventProvider):
|
||||
|
||||
options = ('min_position_x', 'max_position_x',
|
||||
'min_position_y', 'max_position_y',
|
||||
'min_pressure', 'max_pressure',
|
||||
'min_abs_x', 'max_abs_x',
|
||||
'min_abs_y', 'max_abs_y',
|
||||
'invert_x', 'invert_y', 'rotation')
|
||||
|
||||
def __init__(self, device, args):
|
||||
super(HIDInputMotionEventProvider, self).__init__(device, args)
|
||||
global Window, Keyboard
|
||||
|
||||
if Window is None:
|
||||
from kivy.core.window import Window
|
||||
if Keyboard is None:
|
||||
from kivy.core.window import Keyboard
|
||||
|
||||
self.input_fn = None
|
||||
self.default_ranges = dict()
|
||||
|
||||
# split arguments
|
||||
args = args.split(',')
|
||||
if not args:
|
||||
Logger.error('HIDInput: Filename missing in configuration')
|
||||
Logger.error('HIDInput: Use /dev/input/event0 for example')
|
||||
return None
|
||||
|
||||
# read filename
|
||||
self.input_fn = args[0]
|
||||
Logger.info('HIDInput: Read event from <%s>' % self.input_fn)
|
||||
|
||||
# read parameters
|
||||
for arg in args[1:]:
|
||||
if arg == '':
|
||||
continue
|
||||
arg = arg.split('=')
|
||||
|
||||
# ensure it's a key = value
|
||||
if len(arg) != 2:
|
||||
Logger.error('HIDInput: invalid parameter '
|
||||
'%s, not in key=value format.' % arg)
|
||||
continue
|
||||
|
||||
# ensure the key exist
|
||||
key, value = arg
|
||||
if key not in HIDInputMotionEventProvider.options:
|
||||
Logger.error('HIDInput: unknown %s option' % key)
|
||||
continue
|
||||
|
||||
# ensure the value
|
||||
try:
|
||||
self.default_ranges[key] = int(value)
|
||||
except ValueError:
|
||||
err = 'HIDInput: invalid value "%s" for "%s"' % (
|
||||
key, value)
|
||||
Logger.error(err)
|
||||
continue
|
||||
|
||||
# all good!
|
||||
Logger.info('HIDInput: Set custom %s to %d' % (
|
||||
key, int(value)))
|
||||
|
||||
if 'rotation' not in self.default_ranges:
|
||||
self.default_ranges['rotation'] = 0
|
||||
elif self.default_ranges['rotation'] not in (0, 90, 180, 270):
|
||||
Logger.error('HIDInput: invalid rotation value ({})'.format(
|
||||
self.default_ranges['rotation']))
|
||||
self.default_ranges['rotation'] = 0
|
||||
|
||||
def start(self):
|
||||
if self.input_fn is None:
|
||||
return
|
||||
self.uid = 0
|
||||
self.queue = collections.deque()
|
||||
self.dispatch_queue = []
|
||||
self.thread = threading.Thread(
|
||||
name=self.__class__.__name__,
|
||||
target=self._thread_run,
|
||||
kwargs=dict(
|
||||
queue=self.queue,
|
||||
input_fn=self.input_fn,
|
||||
device=self.device,
|
||||
default_ranges=self.default_ranges))
|
||||
self.thread.daemon = True
|
||||
self.thread.start()
|
||||
|
||||
def _thread_run(self, **kwargs):
|
||||
input_fn = kwargs.get('input_fn')
|
||||
queue = self.queue
|
||||
dispatch_queue = self.dispatch_queue
|
||||
device = kwargs.get('device')
|
||||
drs = kwargs.get('default_ranges').get
|
||||
touches = {}
|
||||
touches_sent = []
|
||||
point = {}
|
||||
l_points = []
|
||||
|
||||
# prepare some vars to get limit of some component
|
||||
range_min_position_x = 0
|
||||
range_max_position_x = 2048
|
||||
range_min_position_y = 0
|
||||
range_max_position_y = 2048
|
||||
range_min_pressure = 0
|
||||
range_max_pressure = 255
|
||||
range_min_abs_x = 0
|
||||
range_max_abs_x = 255
|
||||
range_min_abs_y = 0
|
||||
range_max_abs_y = 255
|
||||
range_min_abs_pressure = 0
|
||||
range_max_abs_pressure = 255
|
||||
invert_x = int(bool(drs('invert_x', 0)))
|
||||
invert_y = int(bool(drs('invert_y', 1)))
|
||||
rotation = drs('rotation', 0)
|
||||
|
||||
def assign_coord(point, value, invert, coords):
|
||||
cx, cy = coords
|
||||
if invert:
|
||||
value = 1. - value
|
||||
if rotation == 0:
|
||||
point[cx] = value
|
||||
elif rotation == 90:
|
||||
point[cy] = value
|
||||
elif rotation == 180:
|
||||
point[cx] = 1. - value
|
||||
elif rotation == 270:
|
||||
point[cy] = 1. - value
|
||||
|
||||
def assign_rel_coord(point, value, invert, coords):
|
||||
cx, cy = coords
|
||||
if invert:
|
||||
value = -1 * value
|
||||
if rotation == 0:
|
||||
point[cx] += value
|
||||
elif rotation == 90:
|
||||
point[cy] += value
|
||||
elif rotation == 180:
|
||||
point[cx] += -value
|
||||
elif rotation == 270:
|
||||
point[cy] += -value
|
||||
|
||||
# limit it to the screen area 0-1
|
||||
point['x'] = min(1., max(0., point['x']))
|
||||
point['y'] = min(1., max(0., point['y']))
|
||||
|
||||
def process_as_multitouch(tv_sec, tv_usec, ev_type,
|
||||
ev_code, ev_value):
|
||||
# sync event
|
||||
if ev_type == EV_SYN:
|
||||
if ev_code == SYN_MT_REPORT:
|
||||
if 'id' not in point:
|
||||
return
|
||||
l_points.append(point.copy())
|
||||
elif ev_code == SYN_REPORT:
|
||||
process(l_points)
|
||||
del l_points[:]
|
||||
|
||||
elif ev_type == EV_MSC and ev_code in (MSC_RAW, MSC_SCAN):
|
||||
pass
|
||||
|
||||
else:
|
||||
# compute multitouch track
|
||||
if ev_code == ABS_MT_TRACKING_ID:
|
||||
point.clear()
|
||||
point['id'] = ev_value
|
||||
elif ev_code == ABS_MT_POSITION_X:
|
||||
val = normalize(ev_value,
|
||||
range_min_position_x,
|
||||
range_max_position_x)
|
||||
assign_coord(point, val, invert_x, 'xy')
|
||||
elif ev_code == ABS_MT_POSITION_Y:
|
||||
val = 1. - normalize(ev_value,
|
||||
range_min_position_y,
|
||||
range_max_position_y)
|
||||
assign_coord(point, val, invert_y, 'yx')
|
||||
elif ev_code == ABS_MT_ORIENTATION:
|
||||
point['orientation'] = ev_value
|
||||
elif ev_code == ABS_MT_BLOB_ID:
|
||||
point['blobid'] = ev_value
|
||||
elif ev_code == ABS_MT_PRESSURE:
|
||||
point['pressure'] = normalize(ev_value,
|
||||
range_min_pressure,
|
||||
range_max_pressure)
|
||||
elif ev_code == ABS_MT_TOUCH_MAJOR:
|
||||
point['size_w'] = ev_value
|
||||
elif ev_code == ABS_MT_TOUCH_MINOR:
|
||||
point['size_h'] = ev_value
|
||||
|
||||
def process_as_mouse_or_keyboard(
|
||||
tv_sec, tv_usec, ev_type, ev_code, ev_value):
|
||||
if ev_type == EV_SYN:
|
||||
if ev_code == SYN_REPORT:
|
||||
process([point])
|
||||
if ('button' in point and
|
||||
point['button'].startswith('scroll')):
|
||||
# for scrolls we need to remove it as there is
|
||||
# no up key
|
||||
del point['button']
|
||||
point['id'] += 1
|
||||
point['_avoid'] = True
|
||||
process([point])
|
||||
|
||||
elif ev_type == EV_REL:
|
||||
if ev_code == 0:
|
||||
assign_rel_coord(point,
|
||||
min(1., max(-1., ev_value / 1000.)),
|
||||
invert_x, 'xy')
|
||||
elif ev_code == 1:
|
||||
assign_rel_coord(point,
|
||||
min(1., max(-1., ev_value / 1000.)),
|
||||
invert_y, 'yx')
|
||||
elif ev_code == 8: # Wheel
|
||||
# translates the wheel move to a button
|
||||
b = "scrollup" if ev_value < 0 else "scrolldown"
|
||||
if 'button' not in point:
|
||||
point['button'] = b
|
||||
point['id'] += 1
|
||||
if '_avoid' in point:
|
||||
del point['_avoid']
|
||||
|
||||
elif ev_type != EV_KEY:
|
||||
if ev_code == ABS_X:
|
||||
val = normalize(ev_value,
|
||||
range_min_abs_x,
|
||||
range_max_abs_x)
|
||||
assign_coord(point, val, invert_x, 'xy')
|
||||
elif ev_code == ABS_Y:
|
||||
val = 1. - normalize(ev_value,
|
||||
range_min_abs_y,
|
||||
range_max_abs_y)
|
||||
assign_coord(point, val, invert_y, 'yx')
|
||||
elif ev_code == ABS_PRESSURE:
|
||||
point['pressure'] = normalize(ev_value,
|
||||
range_min_abs_pressure,
|
||||
range_max_abs_pressure)
|
||||
else:
|
||||
buttons = {
|
||||
272: 'left',
|
||||
273: 'right',
|
||||
274: 'middle',
|
||||
275: 'side',
|
||||
276: 'extra',
|
||||
277: 'forward',
|
||||
278: 'back',
|
||||
279: 'task',
|
||||
330: 'touch',
|
||||
320: 'pen'}
|
||||
|
||||
if ev_code in buttons.keys():
|
||||
if ev_value:
|
||||
if 'button' not in point:
|
||||
point['button'] = buttons[ev_code]
|
||||
point['id'] += 1
|
||||
if '_avoid' in point:
|
||||
del point['_avoid']
|
||||
elif 'button' in point:
|
||||
if point['button'] == buttons[ev_code]:
|
||||
del point['button']
|
||||
point['id'] += 1
|
||||
point['_avoid'] = True
|
||||
else:
|
||||
if not 0 <= ev_value <= 1:
|
||||
return
|
||||
|
||||
if ev_code not in keyboard_keys:
|
||||
Logger.warn('HIDInput: unhandled HID code: {}'.
|
||||
format(ev_code))
|
||||
return
|
||||
|
||||
z = keyboard_keys[ev_code][-1 if 'shift' in
|
||||
Window._modifiers else 0]
|
||||
if z.lower() not in Keyboard.keycodes:
|
||||
# or if it is not in this LUT
|
||||
Logger.warn('HIDInput: unhandled character: {}'.
|
||||
format(z))
|
||||
return
|
||||
|
||||
keycode = Keyboard.keycodes[z.lower()]
|
||||
|
||||
if ev_value == 1:
|
||||
if z == 'shift' or z == 'alt':
|
||||
Window._modifiers.append(z)
|
||||
elif z.endswith('ctrl'):
|
||||
Window._modifiers.append('ctrl')
|
||||
|
||||
dispatch_queue.append(('key_down', (
|
||||
keycode, ev_code,
|
||||
keys_str.get(z, z), Window._modifiers)))
|
||||
elif ev_value == 0:
|
||||
dispatch_queue.append(('key_up', (
|
||||
keycode, ev_code,
|
||||
keys_str.get(z, z), Window._modifiers)))
|
||||
if ((z == 'shift' or z == 'alt') and
|
||||
(z in Window._modifiers)):
|
||||
Window._modifiers.remove(z)
|
||||
elif (z.endswith('ctrl') and
|
||||
'ctrl' in Window._modifiers):
|
||||
Window._modifiers.remove('ctrl')
|
||||
|
||||
def process(points):
|
||||
if not is_multitouch:
|
||||
dispatch_queue.append(('mouse_pos', (
|
||||
points[0]['x'] * Window.width,
|
||||
points[0]['y'] * Window.height)))
|
||||
|
||||
actives = [args['id']
|
||||
for args in points
|
||||
if 'id' in args and '_avoid' not in args]
|
||||
for args in points:
|
||||
tid = args['id']
|
||||
try:
|
||||
touch = touches[tid]
|
||||
if touch.sx == args['x'] and touch.sy == args['y']:
|
||||
continue
|
||||
touch.move(args)
|
||||
if tid not in touches_sent:
|
||||
queue.append(('begin', touch))
|
||||
touches_sent.append(tid)
|
||||
queue.append(('update', touch))
|
||||
except KeyError:
|
||||
if '_avoid' not in args:
|
||||
touch = HIDMotionEvent(device, tid, args)
|
||||
touches[touch.id] = touch
|
||||
if tid not in touches_sent:
|
||||
queue.append(('begin', touch))
|
||||
touches_sent.append(tid)
|
||||
|
||||
for tid in list(touches.keys())[:]:
|
||||
if tid not in actives:
|
||||
touch = touches[tid]
|
||||
if tid in touches_sent:
|
||||
touch.update_time_end()
|
||||
queue.append(('end', touch))
|
||||
touches_sent.remove(tid)
|
||||
del touches[tid]
|
||||
|
||||
def normalize(value, vmin, vmax):
|
||||
return (value - vmin) / float(vmax - vmin)
|
||||
|
||||
# open the input
|
||||
fd = open(input_fn, 'rb')
|
||||
|
||||
# get the controller name (EVIOCGNAME)
|
||||
device_name = fcntl.ioctl(fd, EVIOCGNAME + (256 << 16),
|
||||
" " * 256).decode().strip()
|
||||
Logger.info('HIDMotionEvent: using <%s>' % device_name)
|
||||
|
||||
# get abs infos
|
||||
bit = fcntl.ioctl(fd, EVIOCGBIT + (EV_MAX << 16), ' ' * sz_l)
|
||||
bit, = struct.unpack('Q', bit)
|
||||
is_multitouch = False
|
||||
for x in range(EV_MAX):
|
||||
# preserve this, we may want other things than EV_ABS
|
||||
if x != EV_ABS:
|
||||
continue
|
||||
# EV_ABS available for this device ?
|
||||
if (bit & (1 << x)) == 0:
|
||||
continue
|
||||
# ask abs info keys to the devices
|
||||
sbit = fcntl.ioctl(fd, EVIOCGBIT + x + (KEY_MAX << 16),
|
||||
' ' * sz_l)
|
||||
sbit, = struct.unpack('Q', sbit)
|
||||
for y in range(KEY_MAX):
|
||||
if (sbit & (1 << y)) == 0:
|
||||
continue
|
||||
absinfo = fcntl.ioctl(fd, EVIOCGABS + y +
|
||||
(struct_input_absinfo_sz << 16),
|
||||
' ' * struct_input_absinfo_sz)
|
||||
abs_value, abs_min, abs_max, abs_fuzz, \
|
||||
abs_flat, abs_res = struct.unpack('iiiiii', absinfo)
|
||||
if y == ABS_MT_POSITION_X:
|
||||
is_multitouch = True
|
||||
range_min_position_x = drs('min_position_x', abs_min)
|
||||
range_max_position_x = drs('max_position_x', abs_max)
|
||||
Logger.info('HIDMotionEvent: ' +
|
||||
'<%s> range position X is %d - %d' % (
|
||||
device_name, abs_min, abs_max))
|
||||
elif y == ABS_MT_POSITION_Y:
|
||||
is_multitouch = True
|
||||
range_min_position_y = drs('min_position_y', abs_min)
|
||||
range_max_position_y = drs('max_position_y', abs_max)
|
||||
Logger.info('HIDMotionEvent: ' +
|
||||
'<%s> range position Y is %d - %d' % (
|
||||
device_name, abs_min, abs_max))
|
||||
elif y == ABS_MT_PRESSURE:
|
||||
range_min_pressure = drs('min_pressure', abs_min)
|
||||
range_max_pressure = drs('max_pressure', abs_max)
|
||||
Logger.info('HIDMotionEvent: ' +
|
||||
'<%s> range pressure is %d - %d' % (
|
||||
device_name, abs_min, abs_max))
|
||||
elif y == ABS_X:
|
||||
range_min_abs_x = drs('min_abs_x', abs_min)
|
||||
range_max_abs_x = drs('max_abs_x', abs_max)
|
||||
Logger.info('HIDMotionEvent: ' +
|
||||
'<%s> range ABS X position is %d - %d' % (
|
||||
device_name, abs_min, abs_max))
|
||||
elif y == ABS_Y:
|
||||
range_min_abs_y = drs('min_abs_y', abs_min)
|
||||
range_max_abs_y = drs('max_abs_y', abs_max)
|
||||
Logger.info('HIDMotionEvent: ' +
|
||||
'<%s> range ABS Y position is %d - %d' % (
|
||||
device_name, abs_min, abs_max))
|
||||
elif y == ABS_PRESSURE:
|
||||
range_min_abs_pressure = drs(
|
||||
'min_abs_pressure', abs_min)
|
||||
range_max_abs_pressure = drs(
|
||||
'max_abs_pressure', abs_max)
|
||||
Logger.info('HIDMotionEvent: ' +
|
||||
'<%s> range ABS pressure is %d - %d' % (
|
||||
device_name, abs_min, abs_max))
|
||||
|
||||
# init the point
|
||||
if not is_multitouch:
|
||||
point = {'x': .5, 'y': .5, 'id': 0, '_avoid': True}
|
||||
|
||||
# read until the end
|
||||
while fd:
|
||||
|
||||
data = fd.read(struct_input_event_sz)
|
||||
if len(data) < struct_input_event_sz:
|
||||
break
|
||||
|
||||
# extract each event
|
||||
for i in range(int(len(data) / struct_input_event_sz)):
|
||||
ev = data[i * struct_input_event_sz:]
|
||||
|
||||
# extract timeval + event infos
|
||||
infos = struct.unpack('LLHHi', ev[:struct_input_event_sz])
|
||||
|
||||
if is_multitouch:
|
||||
process_as_multitouch(*infos)
|
||||
else:
|
||||
process_as_mouse_or_keyboard(*infos)
|
||||
|
||||
def update(self, dispatch_fn):
|
||||
# dispatch all events from threads
|
||||
dispatch_queue = self.dispatch_queue
|
||||
n = len(dispatch_queue)
|
||||
for name, args in dispatch_queue[:n]:
|
||||
if name == 'mouse_pos':
|
||||
Window.mouse_pos = args
|
||||
elif name == 'key_down':
|
||||
if not Window.dispatch('on_key_down', *args):
|
||||
Window.dispatch('on_keyboard', *args)
|
||||
elif name == 'key_up':
|
||||
Window.dispatch('on_key_up', *args)
|
||||
del dispatch_queue[:n]
|
||||
|
||||
try:
|
||||
while True:
|
||||
event_type, touch = self.queue.popleft()
|
||||
dispatch_fn(event_type, touch)
|
||||
except:
|
||||
pass
|
||||
|
||||
MotionEventFactory.register('hidinput', HIDInputMotionEventProvider)
|
||||
113
kivy/input/providers/leapfinger.py
Normal file
113
kivy/input/providers/leapfinger.py
Normal file
@@ -0,0 +1,113 @@
|
||||
'''
|
||||
Leap Motion - finger only
|
||||
=========================
|
||||
'''
|
||||
|
||||
__all__ = ('LeapFingerEventProvider', 'LeapFingerEvent')
|
||||
|
||||
from collections import deque
|
||||
from kivy.logger import Logger
|
||||
from kivy.input.provider import MotionEventProvider
|
||||
from kivy.input.factory import MotionEventFactory
|
||||
from kivy.input.motionevent import MotionEvent
|
||||
|
||||
_LEAP_QUEUE = deque()
|
||||
|
||||
Leap = InteractionBox = None
|
||||
|
||||
|
||||
def normalize(value, a, b):
|
||||
return (value - a) / float(b - a)
|
||||
|
||||
|
||||
class LeapFingerEvent(MotionEvent):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault('is_touch', True)
|
||||
kwargs.setdefault('type_id', 'touch')
|
||||
super().__init__(*args, **kwargs)
|
||||
self.profile = ('pos', 'pos3d',)
|
||||
|
||||
def depack(self, args):
|
||||
super().depack(args)
|
||||
if args[0] is None:
|
||||
return
|
||||
x, y, z = args
|
||||
self.sx = normalize(x, -150, 150)
|
||||
self.sy = normalize(y, 40, 460)
|
||||
self.sz = normalize(z, -350, 350)
|
||||
self.z = z
|
||||
|
||||
|
||||
class LeapFingerEventProvider(MotionEventProvider):
|
||||
|
||||
__handlers__ = {}
|
||||
|
||||
def start(self):
|
||||
# don't do the import at start, or the error will be always displayed
|
||||
# for user who don't have Leap
|
||||
global Leap, InteractionBox
|
||||
import Leap
|
||||
from Leap import InteractionBox
|
||||
|
||||
class LeapMotionListener(Leap.Listener):
|
||||
|
||||
def on_init(self, controller):
|
||||
Logger.info('leapmotion: Initialized')
|
||||
|
||||
def on_connect(self, controller):
|
||||
Logger.info('leapmotion: Connected')
|
||||
|
||||
def on_disconnect(self, controller):
|
||||
Logger.info('leapmotion: Disconnected')
|
||||
|
||||
def on_frame(self, controller):
|
||||
frame = controller.frame()
|
||||
_LEAP_QUEUE.append(frame)
|
||||
|
||||
def on_exit(self, controller):
|
||||
pass
|
||||
|
||||
self.uid = 0
|
||||
self.touches = {}
|
||||
self.listener = LeapMotionListener()
|
||||
self.controller = Leap.Controller(self.listener)
|
||||
|
||||
def update(self, dispatch_fn):
|
||||
try:
|
||||
while True:
|
||||
frame = _LEAP_QUEUE.popleft()
|
||||
events = self.process_frame(frame)
|
||||
for ev in events:
|
||||
dispatch_fn(*ev)
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
def process_frame(self, frame):
|
||||
events = []
|
||||
touches = self.touches
|
||||
available_uid = []
|
||||
for hand in frame.hands:
|
||||
for finger in hand.fingers:
|
||||
# print(hand.id(), finger.id(), finger.tip())
|
||||
uid = '{0}:{1}'.format(hand.id, finger.id)
|
||||
available_uid.append(uid)
|
||||
position = finger.tip_position
|
||||
args = (position.x, position.y, position.z)
|
||||
if uid not in touches:
|
||||
touch = LeapFingerEvent(self.device, uid, args)
|
||||
events.append(('begin', touch))
|
||||
touches[uid] = touch
|
||||
else:
|
||||
touch = touches[uid]
|
||||
touch.move(args)
|
||||
events.append(('update', touch))
|
||||
for key in list(touches.keys())[:]:
|
||||
if key not in available_uid:
|
||||
events.append(('end', touches[key]))
|
||||
del touches[key]
|
||||
return events
|
||||
|
||||
|
||||
# registers
|
||||
MotionEventFactory.register('leapfinger', LeapFingerEventProvider)
|
||||
396
kivy/input/providers/linuxwacom.py
Normal file
396
kivy/input/providers/linuxwacom.py
Normal file
@@ -0,0 +1,396 @@
|
||||
'''
|
||||
Native support of Wacom tablet from linuxwacom driver
|
||||
=====================================================
|
||||
|
||||
To configure LinuxWacom, add this to your configuration::
|
||||
|
||||
[input]
|
||||
pen = linuxwacom,/dev/input/event2,mode=pen
|
||||
finger = linuxwacom,/dev/input/event3,mode=touch
|
||||
|
||||
.. note::
|
||||
You must have read access to the input event.
|
||||
|
||||
You can use a custom range for the X, Y and pressure values.
|
||||
On some drivers, the range reported is invalid.
|
||||
To fix that, you can add these options to the argument line:
|
||||
|
||||
* invert_x : 1 to invert X axis
|
||||
* invert_y : 1 to invert Y axis
|
||||
* min_position_x : X minimum
|
||||
* max_position_x : X maximum
|
||||
* min_position_y : Y minimum
|
||||
* max_position_y : Y maximum
|
||||
* min_pressure : pressure minimum
|
||||
* max_pressure : pressure maximum
|
||||
'''
|
||||
|
||||
__all__ = ('LinuxWacomMotionEventProvider', 'LinuxWacomMotionEvent')
|
||||
|
||||
import os
|
||||
from kivy.input.motionevent import MotionEvent
|
||||
from kivy.input.shape import ShapeRect
|
||||
|
||||
|
||||
class LinuxWacomMotionEvent(MotionEvent):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault('is_touch', True)
|
||||
kwargs.setdefault('type_id', 'touch')
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def depack(self, args):
|
||||
self.sx = args['x']
|
||||
self.sy = args['y']
|
||||
self.profile = ['pos']
|
||||
if 'size_w' in args and 'size_h' in args:
|
||||
self.shape = ShapeRect()
|
||||
self.shape.width = args['size_w']
|
||||
self.shape.height = args['size_h']
|
||||
self.profile.append('shape')
|
||||
if 'pressure' in args:
|
||||
self.pressure = args['pressure']
|
||||
self.profile.append('pressure')
|
||||
super().depack(args)
|
||||
|
||||
def __str__(self):
|
||||
return '<LinuxWacomMotionEvent id=%d pos=(%f, %f) device=%s>' \
|
||||
% (self.id, self.sx, self.sy, self.device)
|
||||
|
||||
|
||||
if 'KIVY_DOC' in os.environ:
|
||||
# documentation hack
|
||||
LinuxWacomMotionEventProvider = None
|
||||
|
||||
else:
|
||||
import threading
|
||||
import collections
|
||||
import struct
|
||||
import fcntl
|
||||
from kivy.input.provider import MotionEventProvider
|
||||
from kivy.input.factory import MotionEventFactory
|
||||
from kivy.logger import Logger
|
||||
|
||||
#
|
||||
# This part is taken from linux-source-2.6.32/include/linux/input.h
|
||||
#
|
||||
|
||||
# Event types
|
||||
EV_SYN = 0x00
|
||||
EV_KEY = 0x01
|
||||
EV_REL = 0x02
|
||||
EV_ABS = 0x03
|
||||
EV_MSC = 0x04
|
||||
EV_SW = 0x05
|
||||
EV_LED = 0x11
|
||||
EV_SND = 0x12
|
||||
EV_REP = 0x14
|
||||
EV_FF = 0x15
|
||||
EV_PWR = 0x16
|
||||
EV_FF_STATUS = 0x17
|
||||
EV_MAX = 0x1f
|
||||
EV_CNT = (EV_MAX + 1)
|
||||
|
||||
KEY_MAX = 0x2ff
|
||||
|
||||
# Synchronization events
|
||||
SYN_REPORT = 0
|
||||
SYN_CONFIG = 1
|
||||
SYN_MT_REPORT = 2
|
||||
|
||||
# Misc events
|
||||
MSC_SERIAL = 0x00
|
||||
MSC_PULSELED = 0x01
|
||||
MSC_GESTURE = 0x02
|
||||
MSC_RAW = 0x03
|
||||
MSC_SCAN = 0x04
|
||||
MSC_MAX = 0x07
|
||||
MSC_CNT = (MSC_MAX + 1)
|
||||
|
||||
ABS_X = 0x00
|
||||
ABS_Y = 0x01
|
||||
ABS_PRESSURE = 0x18
|
||||
ABS_MISC = 0x28 # if 0, it's touch up
|
||||
ABS_MT_TOUCH_MAJOR = 0x30 # Major axis of touching ellipse
|
||||
ABS_MT_TOUCH_MINOR = 0x31 # Minor axis (omit if circular)
|
||||
ABS_MT_WIDTH_MAJOR = 0x32 # Major axis of approaching ellipse
|
||||
ABS_MT_WIDTH_MINOR = 0x33 # Minor axis (omit if circular)
|
||||
ABS_MT_ORIENTATION = 0x34 # Ellipse orientation
|
||||
ABS_MT_POSITION_X = 0x35 # Center X ellipse position
|
||||
ABS_MT_POSITION_Y = 0x36 # Center Y ellipse position
|
||||
ABS_MT_TOOL_TYPE = 0x37 # Type of touching device
|
||||
ABS_MT_BLOB_ID = 0x38 # Group a set of packets as a blob
|
||||
ABS_MT_TRACKING_ID = 0x39 # Unique ID of initiated contact
|
||||
ABS_MT_PRESSURE = 0x3a # Pressure on contact area
|
||||
|
||||
# some ioctl base (with 0 value)
|
||||
EVIOCGNAME = 2147501318
|
||||
EVIOCGBIT = 2147501344
|
||||
EVIOCGABS = 2149074240
|
||||
|
||||
# sizeof(struct input_event)
|
||||
struct_input_event_sz = struct.calcsize('LLHHi')
|
||||
struct_input_absinfo_sz = struct.calcsize('iiiiii')
|
||||
sz_l = struct.calcsize('Q')
|
||||
|
||||
class LinuxWacomMotionEventProvider(MotionEventProvider):
|
||||
|
||||
options = ('min_position_x', 'max_position_x',
|
||||
'min_position_y', 'max_position_y',
|
||||
'min_pressure', 'max_pressure',
|
||||
'invert_x', 'invert_y')
|
||||
|
||||
def __init__(self, device, args):
|
||||
super(LinuxWacomMotionEventProvider, self).__init__(device, args)
|
||||
self.input_fn = None
|
||||
self.default_ranges = dict()
|
||||
self.mode = 'touch'
|
||||
|
||||
# split arguments
|
||||
args = args.split(',')
|
||||
if not args:
|
||||
Logger.error('LinuxWacom: No filename given in config')
|
||||
Logger.error('LinuxWacom: Use /dev/input/event0 for example')
|
||||
return
|
||||
|
||||
# read filename
|
||||
self.input_fn = args[0]
|
||||
Logger.info('LinuxWacom: Read event from <%s>' % self.input_fn)
|
||||
|
||||
# read parameters
|
||||
for arg in args[1:]:
|
||||
if arg == '':
|
||||
continue
|
||||
arg = arg.split('=')
|
||||
|
||||
# ensure it's a key = value
|
||||
if len(arg) != 2:
|
||||
err = 'LinuxWacom: Bad parameter' \
|
||||
'%s: Not in key=value format.' % arg
|
||||
Logger.error(err)
|
||||
continue
|
||||
|
||||
# ensure the key exist
|
||||
key, value = arg
|
||||
if key == 'mode':
|
||||
self.mode = value
|
||||
continue
|
||||
|
||||
if key not in LinuxWacomMotionEventProvider.options:
|
||||
Logger.error('LinuxWacom: unknown %s option' % key)
|
||||
continue
|
||||
|
||||
# ensure the value
|
||||
try:
|
||||
self.default_ranges[key] = int(value)
|
||||
except ValueError:
|
||||
err = 'LinuxWacom: value %s invalid for %s' % (key, value)
|
||||
Logger.error(err)
|
||||
continue
|
||||
|
||||
# all good!
|
||||
msg = 'LinuxWacom: Set custom %s to %d' % (key, int(value))
|
||||
Logger.info(msg)
|
||||
Logger.info('LinuxWacom: mode is <%s>' % self.mode)
|
||||
|
||||
def start(self):
|
||||
if self.input_fn is None:
|
||||
return
|
||||
self.uid = 0
|
||||
self.queue = collections.deque()
|
||||
self.thread = threading.Thread(
|
||||
target=self._thread_run,
|
||||
kwargs=dict(
|
||||
queue=self.queue,
|
||||
input_fn=self.input_fn,
|
||||
device=self.device,
|
||||
default_ranges=self.default_ranges))
|
||||
self.thread.daemon = True
|
||||
self.thread.start()
|
||||
|
||||
def _thread_run(self, **kwargs):
|
||||
input_fn = kwargs.get('input_fn')
|
||||
queue = kwargs.get('queue')
|
||||
device = kwargs.get('device')
|
||||
drs = kwargs.get('default_ranges').get
|
||||
touches = {}
|
||||
touches_sent = []
|
||||
l_points = {}
|
||||
|
||||
# prepare some vars to get limit of some component
|
||||
range_min_position_x = 0
|
||||
range_max_position_x = 2048
|
||||
range_min_position_y = 0
|
||||
range_max_position_y = 2048
|
||||
range_min_pressure = 0
|
||||
range_max_pressure = 255
|
||||
invert_x = int(bool(drs('invert_x', 0)))
|
||||
invert_y = int(bool(drs('invert_y', 0)))
|
||||
reset_touch = False
|
||||
|
||||
def process(points):
|
||||
actives = list(points.keys())
|
||||
for args in points.values():
|
||||
tid = args['id']
|
||||
try:
|
||||
touch = touches[tid]
|
||||
except KeyError:
|
||||
touch = LinuxWacomMotionEvent(device, tid, args)
|
||||
touches[touch.id] = touch
|
||||
if touch.sx == args['x'] \
|
||||
and touch.sy == args['y'] \
|
||||
and tid in touches_sent:
|
||||
continue
|
||||
touch.move(args)
|
||||
if tid not in touches_sent:
|
||||
queue.append(('begin', touch))
|
||||
touches_sent.append(tid)
|
||||
queue.append(('update', touch))
|
||||
|
||||
for tid in list(touches.keys())[:]:
|
||||
if tid not in actives:
|
||||
touch = touches[tid]
|
||||
if tid in touches_sent:
|
||||
touch.update_time_end()
|
||||
queue.append(('end', touch))
|
||||
touches_sent.remove(tid)
|
||||
del touches[tid]
|
||||
|
||||
def normalize(value, vmin, vmax):
|
||||
return (value - vmin) / float(vmax - vmin)
|
||||
|
||||
# open the input
|
||||
try:
|
||||
fd = open(input_fn, 'rb')
|
||||
except IOError:
|
||||
Logger.exception('Unable to open %s' % input_fn)
|
||||
return
|
||||
|
||||
# get the controller name (EVIOCGNAME)
|
||||
device_name = fcntl.ioctl(fd, EVIOCGNAME + (256 << 16),
|
||||
" " * 256).split('\x00')[0]
|
||||
Logger.info('LinuxWacom: using <%s>' % device_name)
|
||||
|
||||
# get abs infos
|
||||
bit = fcntl.ioctl(fd, EVIOCGBIT + (EV_MAX << 16), ' ' * sz_l)
|
||||
bit, = struct.unpack('Q', bit)
|
||||
for x in range(EV_MAX):
|
||||
# preserve this, we may want other things than EV_ABS
|
||||
if x != EV_ABS:
|
||||
continue
|
||||
# EV_ABS available for this device ?
|
||||
if (bit & (1 << x)) == 0:
|
||||
continue
|
||||
# ask abs info keys to the devices
|
||||
sbit = fcntl.ioctl(fd, EVIOCGBIT + x + (KEY_MAX << 16),
|
||||
' ' * sz_l)
|
||||
sbit, = struct.unpack('Q', sbit)
|
||||
for y in range(KEY_MAX):
|
||||
if (sbit & (1 << y)) == 0:
|
||||
continue
|
||||
absinfo = fcntl.ioctl(fd, EVIOCGABS + y +
|
||||
(struct_input_absinfo_sz << 16),
|
||||
' ' * struct_input_absinfo_sz)
|
||||
abs_value, abs_min, abs_max, abs_fuzz, \
|
||||
abs_flat, abs_res = struct.unpack('iiiiii', absinfo)
|
||||
if y == ABS_X:
|
||||
range_min_position_x = drs('min_position_x', abs_min)
|
||||
range_max_position_x = drs('max_position_x', abs_max)
|
||||
Logger.info('LinuxWacom: ' +
|
||||
'<%s> range position X is %d - %d' % (
|
||||
device_name, abs_min, abs_max))
|
||||
elif y == ABS_Y:
|
||||
range_min_position_y = drs('min_position_y', abs_min)
|
||||
range_max_position_y = drs('max_position_y', abs_max)
|
||||
Logger.info('LinuxWacom: ' +
|
||||
'<%s> range position Y is %d - %d' % (
|
||||
device_name, abs_min, abs_max))
|
||||
elif y == ABS_PRESSURE:
|
||||
range_min_pressure = drs('min_pressure', abs_min)
|
||||
range_max_pressure = drs('max_pressure', abs_max)
|
||||
Logger.info('LinuxWacom: ' +
|
||||
'<%s> range pressure is %d - %d' % (
|
||||
device_name, abs_min, abs_max))
|
||||
|
||||
# read until the end
|
||||
changed = False
|
||||
touch_id = 0
|
||||
touch_x = 0
|
||||
touch_y = 0
|
||||
touch_pressure = 0
|
||||
while fd:
|
||||
|
||||
data = fd.read(struct_input_event_sz)
|
||||
if len(data) < struct_input_event_sz:
|
||||
break
|
||||
|
||||
# extract each event
|
||||
for i in range(len(data) / struct_input_event_sz):
|
||||
ev = data[i * struct_input_event_sz:]
|
||||
|
||||
# extract timeval + event infos
|
||||
tv_sec, tv_usec, ev_type, ev_code, ev_value = \
|
||||
struct.unpack('LLHHi', ev[:struct_input_event_sz])
|
||||
|
||||
if ev_type == EV_SYN and ev_code == SYN_REPORT:
|
||||
if touch_id in l_points:
|
||||
p = l_points[touch_id]
|
||||
else:
|
||||
p = dict()
|
||||
l_points[touch_id] = p
|
||||
p['id'] = touch_id
|
||||
if not reset_touch:
|
||||
p['x'] = touch_x
|
||||
p['y'] = touch_y
|
||||
p['pressure'] = touch_pressure
|
||||
if self.mode == 'pen' \
|
||||
and touch_pressure == 0 \
|
||||
and not reset_touch:
|
||||
del l_points[touch_id]
|
||||
if changed:
|
||||
if 'x' not in p:
|
||||
reset_touch = False
|
||||
continue
|
||||
process(l_points)
|
||||
changed = False
|
||||
if reset_touch:
|
||||
l_points.clear()
|
||||
reset_touch = False
|
||||
process(l_points)
|
||||
elif ev_type == EV_MSC and ev_code == MSC_SERIAL:
|
||||
touch_id = ev_value
|
||||
elif ev_type == EV_ABS and ev_code == ABS_X:
|
||||
val = normalize(ev_value,
|
||||
range_min_position_x,
|
||||
range_max_position_x)
|
||||
if invert_x:
|
||||
val = 1. - val
|
||||
touch_x = val
|
||||
changed = True
|
||||
elif ev_type == EV_ABS and ev_code == ABS_Y:
|
||||
val = 1. - normalize(ev_value,
|
||||
range_min_position_y,
|
||||
range_max_position_y)
|
||||
if invert_y:
|
||||
val = 1. - val
|
||||
touch_y = val
|
||||
changed = True
|
||||
elif ev_type == EV_ABS and ev_code == ABS_PRESSURE:
|
||||
touch_pressure = normalize(ev_value,
|
||||
range_min_pressure,
|
||||
range_max_pressure)
|
||||
changed = True
|
||||
elif ev_type == EV_ABS and ev_code == ABS_MISC:
|
||||
if ev_value == 0:
|
||||
reset_touch = True
|
||||
|
||||
def update(self, dispatch_fn):
|
||||
# dispatch all event from threads
|
||||
try:
|
||||
while True:
|
||||
event_type, touch = self.queue.popleft()
|
||||
dispatch_fn(event_type, touch)
|
||||
except:
|
||||
pass
|
||||
|
||||
MotionEventFactory.register('linuxwacom', LinuxWacomMotionEventProvider)
|
||||
220
kivy/input/providers/mactouch.py
Normal file
220
kivy/input/providers/mactouch.py
Normal file
@@ -0,0 +1,220 @@
|
||||
'''
|
||||
Native support of MultitouchSupport framework for MacBook (MaxOSX platform)
|
||||
===========================================================================
|
||||
'''
|
||||
|
||||
__all__ = ('MacMotionEventProvider', )
|
||||
|
||||
import ctypes
|
||||
import threading
|
||||
import collections
|
||||
import os
|
||||
from kivy.input.provider import MotionEventProvider
|
||||
from kivy.input.factory import MotionEventFactory
|
||||
from kivy.input.motionevent import MotionEvent
|
||||
from kivy.input.shape import ShapeRect
|
||||
|
||||
if 'KIVY_DOC' not in os.environ:
|
||||
CFArrayRef = ctypes.c_void_p
|
||||
CFMutableArrayRef = ctypes.c_void_p
|
||||
CFIndex = ctypes.c_long
|
||||
|
||||
dll = '/System/Library/PrivateFrameworks/' + \
|
||||
'MultitouchSupport.framework/MultitouchSupport'
|
||||
MultitouchSupport = ctypes.CDLL(dll)
|
||||
|
||||
CFArrayGetCount = MultitouchSupport.CFArrayGetCount
|
||||
CFArrayGetCount.argtypes = [CFArrayRef]
|
||||
CFArrayGetCount.restype = CFIndex
|
||||
|
||||
CFArrayGetValueAtIndex = MultitouchSupport.CFArrayGetValueAtIndex
|
||||
CFArrayGetValueAtIndex.argtypes = [CFArrayRef, CFIndex]
|
||||
CFArrayGetValueAtIndex.restype = ctypes.c_void_p
|
||||
|
||||
MTDeviceCreateList = MultitouchSupport.MTDeviceCreateList
|
||||
MTDeviceCreateList.argtypes = []
|
||||
MTDeviceCreateList.restype = CFMutableArrayRef
|
||||
|
||||
class MTPoint(ctypes.Structure):
|
||||
_fields_ = [('x', ctypes.c_float),
|
||||
('y', ctypes.c_float)]
|
||||
|
||||
class MTVector(ctypes.Structure):
|
||||
_fields_ = [('position', MTPoint),
|
||||
('velocity', MTPoint)]
|
||||
|
||||
class MTData(ctypes.Structure):
|
||||
_fields_ = [
|
||||
('frame', ctypes.c_int),
|
||||
('timestamp', ctypes.c_double),
|
||||
('identifier', ctypes.c_int),
|
||||
# Current state (of unknown meaning).
|
||||
('state', ctypes.c_int),
|
||||
('unknown1', ctypes.c_int),
|
||||
('unknown2', ctypes.c_int),
|
||||
# Normalized position and vector of the touch (0 to 1)
|
||||
('normalized', MTVector),
|
||||
# The area of the touch.
|
||||
('size', ctypes.c_float),
|
||||
('unknown3', ctypes.c_int),
|
||||
# The following three define the ellipsoid of a finger.
|
||||
('angle', ctypes.c_float),
|
||||
('major_axis', ctypes.c_float),
|
||||
('minor_axis', ctypes.c_float),
|
||||
('unknown4', MTVector),
|
||||
('unknown5_1', ctypes.c_int),
|
||||
('unknown5_2', ctypes.c_int),
|
||||
('unknown6', ctypes.c_float), ]
|
||||
|
||||
MTDataRef = ctypes.POINTER(MTData)
|
||||
|
||||
MTContactCallbackFunction = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int,
|
||||
MTDataRef, ctypes.c_int,
|
||||
ctypes.c_double, ctypes.c_int)
|
||||
|
||||
MTDeviceRef = ctypes.c_void_p
|
||||
|
||||
MTRegisterContactFrameCallback = \
|
||||
MultitouchSupport.MTRegisterContactFrameCallback
|
||||
MTRegisterContactFrameCallback.argtypes = \
|
||||
[MTDeviceRef, MTContactCallbackFunction]
|
||||
MTRegisterContactFrameCallback.restype = None
|
||||
|
||||
MTDeviceStart = MultitouchSupport.MTDeviceStart
|
||||
MTDeviceStart.argtypes = [MTDeviceRef, ctypes.c_int]
|
||||
MTDeviceStart.restype = None
|
||||
|
||||
else:
|
||||
MTContactCallbackFunction = lambda x: None
|
||||
|
||||
|
||||
class MacMotionEvent(MotionEvent):
|
||||
'''MotionEvent representing a contact point on the touchpad. Supports pos
|
||||
and shape profiles.
|
||||
'''
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault('is_touch', True)
|
||||
kwargs.setdefault('type_id', 'touch')
|
||||
super().__init__(*args, **kwargs)
|
||||
self.profile = ('pos', 'shape')
|
||||
|
||||
def depack(self, args):
|
||||
self.shape = ShapeRect()
|
||||
self.sx, self.sy = args[0], args[1]
|
||||
self.shape.width = args[2]
|
||||
self.shape.height = args[2]
|
||||
super().depack(args)
|
||||
|
||||
def __str__(self):
|
||||
return '<MacMotionEvent id=%d pos=(%f, %f) device=%s>' \
|
||||
% (self.id, self.sx, self.sy, self.device)
|
||||
|
||||
|
||||
_instance = None
|
||||
|
||||
|
||||
class MacMotionEventProvider(MotionEventProvider):
|
||||
|
||||
def __init__(self, *largs, **kwargs):
|
||||
global _instance
|
||||
if _instance is not None:
|
||||
raise Exception('Only one MacMotionEvent provider is allowed.')
|
||||
_instance = self
|
||||
super(MacMotionEventProvider, self).__init__(*largs, **kwargs)
|
||||
|
||||
def start(self):
|
||||
# global uid
|
||||
self.uid = 0
|
||||
# touches will be per devices
|
||||
self.touches = {}
|
||||
# lock needed to access on uid
|
||||
self.lock = threading.Lock()
|
||||
# event queue to dispatch in main thread
|
||||
self.queue = collections.deque()
|
||||
|
||||
# ok, listing devices, and attach !
|
||||
devices = MultitouchSupport.MTDeviceCreateList()
|
||||
num_devices = CFArrayGetCount(devices)
|
||||
for i in range(num_devices):
|
||||
device = CFArrayGetValueAtIndex(devices, i)
|
||||
# create touch dict for this device
|
||||
data_id = str(device)
|
||||
self.touches[data_id] = {}
|
||||
# start !
|
||||
MTRegisterContactFrameCallback(device, self._mts_callback)
|
||||
MTDeviceStart(device, 0)
|
||||
|
||||
def update(self, dispatch_fn):
|
||||
# dispatch all event from threads
|
||||
try:
|
||||
while True:
|
||||
event_type, touch = self.queue.popleft()
|
||||
dispatch_fn(event_type, touch)
|
||||
except:
|
||||
pass
|
||||
|
||||
def stop(self):
|
||||
# i don't known how to stop it...
|
||||
pass
|
||||
|
||||
@MTContactCallbackFunction
|
||||
def _mts_callback(device, data_ptr, n_fingers, timestamp, frame):
|
||||
global _instance
|
||||
devid = str(device)
|
||||
|
||||
# XXX create live touch, we get one case that
|
||||
# the device announced by macosx don't match the device
|
||||
# in _mts_callback....
|
||||
if devid not in _instance.touches:
|
||||
_instance.touches[devid] = {}
|
||||
|
||||
touches = _instance.touches[devid]
|
||||
actives = []
|
||||
|
||||
for i in range(n_fingers):
|
||||
# get pointer on data
|
||||
data = data_ptr[i]
|
||||
|
||||
# add this touch as an active touch
|
||||
actives.append(data.identifier)
|
||||
|
||||
# extract identifier
|
||||
data_id = data.identifier
|
||||
|
||||
# prepare argument position
|
||||
norm_pos = data.normalized.position
|
||||
args = (norm_pos.x, norm_pos.y, data.size)
|
||||
|
||||
if data_id not in touches:
|
||||
# increment uid
|
||||
_instance.lock.acquire()
|
||||
_instance.uid += 1
|
||||
# create a touch
|
||||
touch = MacMotionEvent(_instance.device, _instance.uid, args)
|
||||
_instance.lock.release()
|
||||
# create event
|
||||
_instance.queue.append(('begin', touch))
|
||||
# store touch
|
||||
touches[data_id] = touch
|
||||
else:
|
||||
touch = touches[data_id]
|
||||
# check if he really moved
|
||||
if data.normalized.position.x == touch.sx and \
|
||||
data.normalized.position.y == touch.sy:
|
||||
continue
|
||||
touch.move(args)
|
||||
_instance.queue.append(('update', touch))
|
||||
|
||||
# delete old touchs
|
||||
for tid in list(touches.keys())[:]:
|
||||
if tid not in actives:
|
||||
touch = touches[tid]
|
||||
touch.update_time_end()
|
||||
_instance.queue.append(('end', touch))
|
||||
del touches[tid]
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
MotionEventFactory.register('mactouch', MacMotionEventProvider)
|
||||
424
kivy/input/providers/mouse.py
Normal file
424
kivy/input/providers/mouse.py
Normal file
@@ -0,0 +1,424 @@
|
||||
'''
|
||||
Mouse provider implementation
|
||||
=============================
|
||||
|
||||
On linux systems, the mouse provider can be annoying when used with another
|
||||
multitouch provider (hidinput or mtdev). The Mouse can conflict with them: a
|
||||
single touch can generate one event from the mouse provider and another
|
||||
from the multitouch provider.
|
||||
|
||||
To avoid this behavior, you can activate the "disable_on_activity" token in
|
||||
the mouse configuration. Then, if any touches are created by another
|
||||
provider, the mouse event will be discarded. Add this to your configuration::
|
||||
|
||||
[input]
|
||||
mouse = mouse,disable_on_activity
|
||||
|
||||
Using multitouch interaction with the mouse
|
||||
-------------------------------------------
|
||||
|
||||
.. versionadded:: 1.3.0
|
||||
|
||||
By default, the middle and right mouse buttons, as well as a combination of
|
||||
ctrl + left mouse button are used for multitouch emulation.
|
||||
If you want to use them for other purposes, you can disable this behavior by
|
||||
activating the "disable_multitouch" token::
|
||||
|
||||
[input]
|
||||
mouse = mouse,disable_multitouch
|
||||
|
||||
.. versionchanged:: 1.9.0
|
||||
|
||||
You can now selectively control whether a click initiated as described above
|
||||
will emulate multi-touch. If the touch has been initiated in the above manner
|
||||
(e.g. right mouse button), a `multitouch_sim` value will be added to the
|
||||
touch's profile, and a `multitouch_sim` property will be added to the touch.
|
||||
By default, `multitouch_sim` is True and multitouch will be emulated for that
|
||||
touch. If, however, `multitouch_on_demand` is added to the config::
|
||||
|
||||
[input]
|
||||
mouse = mouse,multitouch_on_demand
|
||||
|
||||
then `multitouch_sim` defaults to `False`. In that case, if `multitouch_sim`
|
||||
is set to True before the mouse is released (e.g. in on_touch_down/move), the
|
||||
touch will simulate a multi-touch event. For example::
|
||||
|
||||
if 'multitouch_sim' in touch.profile:
|
||||
touch.multitouch_sim = True
|
||||
|
||||
.. versionchanged:: 2.1.0
|
||||
|
||||
Provider dispatches hover events by listening to properties/events in
|
||||
:class:`~kivy.core.window.Window`. Dispatching can be disabled by setting
|
||||
:attr:`MouseMotionEventProvider.disable_hover` to ``True`` or by adding
|
||||
`disable_hover` in the config::
|
||||
|
||||
[input]
|
||||
mouse = mouse,disable_hover
|
||||
|
||||
It's also possible to enable/disable hover events at runtime with
|
||||
:attr:`MouseMotionEventProvider.disable_hover` property.
|
||||
|
||||
Following is a list of the supported values for the
|
||||
:attr:`~kivy.input.motionevent.MotionEvent.profile` property list.
|
||||
|
||||
================ ==========================================================
|
||||
Profile value Description
|
||||
---------------- ----------------------------------------------------------
|
||||
button Mouse button (one of `left`, `right`, `middle`, `scrollup`
|
||||
or `scrolldown`). Accessed via the 'button' property.
|
||||
pos 2D position. Also reflected in the
|
||||
:attr:`~kivy.input.motionevent.MotionEvent.x`,
|
||||
:attr:`~kivy.input.motionevent.MotionEvent.y`
|
||||
and :attr:`~kivy.input.motionevent.MotionEvent.pos`
|
||||
properties.
|
||||
multitouch_sim Specifies whether multitouch is simulated or not. Accessed
|
||||
via the 'multitouch_sim' property.
|
||||
================ ==========================================================
|
||||
|
||||
'''
|
||||
|
||||
__all__ = ('MouseMotionEventProvider', )
|
||||
|
||||
from kivy.base import EventLoop
|
||||
from collections import deque
|
||||
from kivy.logger import Logger
|
||||
from kivy.input.provider import MotionEventProvider
|
||||
from kivy.input.factory import MotionEventFactory
|
||||
from kivy.input.motionevent import MotionEvent
|
||||
|
||||
# late binding
|
||||
Color = Ellipse = None
|
||||
|
||||
|
||||
class MouseMotionEvent(MotionEvent):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.multitouch_sim = False
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def depack(self, args):
|
||||
self.sx, self.sy = args[:2]
|
||||
profile = self.profile
|
||||
if self.is_touch:
|
||||
# don't overwrite previous profile
|
||||
if not profile:
|
||||
profile.extend(('pos', 'button'))
|
||||
if len(args) >= 3:
|
||||
self.button = args[2]
|
||||
if len(args) == 4:
|
||||
self.multitouch_sim = args[3]
|
||||
profile.append('multitouch_sim')
|
||||
else:
|
||||
if not profile:
|
||||
profile.append('pos')
|
||||
super().depack(args)
|
||||
|
||||
#
|
||||
# Create automatically touch on the surface.
|
||||
#
|
||||
|
||||
def update_graphics(self, win, create=False):
|
||||
global Color, Ellipse
|
||||
de = self.ud.get('_drawelement', None)
|
||||
if de is None and create:
|
||||
if Color is None:
|
||||
from kivy.graphics import Color, Ellipse
|
||||
with win.canvas.after:
|
||||
de = (
|
||||
Color(.8, .2, .2, .7),
|
||||
Ellipse(size=(20, 20), segments=15))
|
||||
self.ud._drawelement = de
|
||||
if de is not None:
|
||||
self.push()
|
||||
|
||||
# use same logic as WindowBase.on_motion() so we get correct
|
||||
# coordinates when _density != 1
|
||||
w, h = win._get_effective_size()
|
||||
|
||||
self.scale_for_screen(w, h, rotation=win.rotation)
|
||||
|
||||
de[1].pos = self.x - 10, self.y - 10
|
||||
self.pop()
|
||||
|
||||
def clear_graphics(self, win):
|
||||
de = self.ud.pop('_drawelement', None)
|
||||
if de is not None:
|
||||
win.canvas.after.remove(de[0])
|
||||
win.canvas.after.remove(de[1])
|
||||
|
||||
|
||||
class MouseMotionEventProvider(MotionEventProvider):
|
||||
__handlers__ = {}
|
||||
|
||||
def __init__(self, device, args):
|
||||
super(MouseMotionEventProvider, self).__init__(device, args)
|
||||
self.waiting_event = deque()
|
||||
self.touches = {}
|
||||
self.counter = 0
|
||||
self.current_drag = None
|
||||
self.alt_touch = None
|
||||
self.disable_on_activity = False
|
||||
self.disable_multitouch = False
|
||||
self.multitouch_on_demand = False
|
||||
self.hover_event = None
|
||||
self._disable_hover = False
|
||||
self._running = False
|
||||
# split arguments
|
||||
args = args.split(',')
|
||||
for arg in args:
|
||||
arg = arg.strip()
|
||||
if arg == '':
|
||||
continue
|
||||
elif arg == 'disable_on_activity':
|
||||
self.disable_on_activity = True
|
||||
elif arg == 'disable_multitouch':
|
||||
self.disable_multitouch = True
|
||||
elif arg == 'disable_hover':
|
||||
self.disable_hover = True
|
||||
elif arg == 'multitouch_on_demand':
|
||||
self.multitouch_on_demand = True
|
||||
else:
|
||||
Logger.error('Mouse: unknown parameter <%s>' % arg)
|
||||
|
||||
def _get_disable_hover(self):
|
||||
return self._disable_hover
|
||||
|
||||
def _set_disable_hover(self, value):
|
||||
if self._disable_hover != value:
|
||||
if self._running:
|
||||
if value:
|
||||
self._stop_hover_events()
|
||||
else:
|
||||
self._start_hover_events()
|
||||
self._disable_hover = value
|
||||
|
||||
disable_hover = property(_get_disable_hover, _set_disable_hover)
|
||||
'''Disables dispatching of hover events if set to ``True``.
|
||||
|
||||
Hover events are enabled by default (`disable_hover` is ``False``). See
|
||||
module documentation if you want to enable/disable hover events through
|
||||
config file.
|
||||
|
||||
.. versionadded:: 2.1.0
|
||||
'''
|
||||
|
||||
def start(self):
|
||||
'''Start the mouse provider'''
|
||||
if not EventLoop.window:
|
||||
return
|
||||
fbind = EventLoop.window.fbind
|
||||
fbind('on_mouse_down', self.on_mouse_press)
|
||||
fbind('on_mouse_move', self.on_mouse_motion)
|
||||
fbind('on_mouse_up', self.on_mouse_release)
|
||||
fbind('on_rotate', self.update_touch_graphics)
|
||||
fbind('system_size', self.update_touch_graphics)
|
||||
if not self.disable_hover:
|
||||
self._start_hover_events()
|
||||
self._running = True
|
||||
|
||||
def _start_hover_events(self):
|
||||
fbind = EventLoop.window.fbind
|
||||
fbind('mouse_pos', self.begin_or_update_hover_event)
|
||||
fbind('system_size', self.update_hover_event)
|
||||
fbind('on_cursor_enter', self.begin_hover_event)
|
||||
fbind('on_cursor_leave', self.end_hover_event)
|
||||
fbind('on_close', self.end_hover_event)
|
||||
fbind('on_rotate', self.update_hover_event)
|
||||
|
||||
def stop(self):
|
||||
'''Stop the mouse provider'''
|
||||
if not EventLoop.window:
|
||||
return
|
||||
funbind = EventLoop.window.funbind
|
||||
funbind('on_mouse_down', self.on_mouse_press)
|
||||
funbind('on_mouse_move', self.on_mouse_motion)
|
||||
funbind('on_mouse_up', self.on_mouse_release)
|
||||
funbind('on_rotate', self.update_touch_graphics)
|
||||
funbind('system_size', self.update_touch_graphics)
|
||||
if not self.disable_hover:
|
||||
self._stop_hover_events()
|
||||
self._running = False
|
||||
|
||||
def _stop_hover_events(self):
|
||||
funbind = EventLoop.window.funbind
|
||||
funbind('mouse_pos', self.begin_or_update_hover_event)
|
||||
funbind('system_size', self.update_hover_event)
|
||||
funbind('on_cursor_enter', self.begin_hover_event)
|
||||
funbind('on_cursor_leave', self.end_hover_event)
|
||||
funbind('on_close', self.end_hover_event)
|
||||
funbind('on_rotate', self.update_hover_event)
|
||||
|
||||
def test_activity(self):
|
||||
if not self.disable_on_activity:
|
||||
return False
|
||||
# trying to get if we currently have other touch than us
|
||||
# discard touches generated from kinetic
|
||||
for touch in EventLoop.touches:
|
||||
# discard all kinetic touch
|
||||
if touch.__class__.__name__ == 'KineticMotionEvent':
|
||||
continue
|
||||
# not our instance, stop mouse
|
||||
if touch.__class__ != MouseMotionEvent:
|
||||
return True
|
||||
return False
|
||||
|
||||
def find_touch(self, win, x, y):
|
||||
factor = 10. / win.system_size[0]
|
||||
for touch in self.touches.values():
|
||||
if abs(x - touch.sx) < factor and abs(y - touch.sy) < factor:
|
||||
return touch
|
||||
return None
|
||||
|
||||
def create_event_id(self):
|
||||
self.counter += 1
|
||||
return self.device + str(self.counter)
|
||||
|
||||
def create_touch(self, win, nx, ny, is_double_tap, do_graphics, button):
|
||||
event_id = self.create_event_id()
|
||||
args = [nx, ny, button]
|
||||
if do_graphics:
|
||||
args += [not self.multitouch_on_demand]
|
||||
self.current_drag = touch = MouseMotionEvent(
|
||||
self.device, event_id, args,
|
||||
is_touch=True,
|
||||
type_id='touch'
|
||||
)
|
||||
touch.is_double_tap = is_double_tap
|
||||
self.touches[event_id] = touch
|
||||
if do_graphics:
|
||||
# only draw red circle if multitouch is not disabled, and
|
||||
# if the multitouch_on_demand feature is not enable
|
||||
# (because in that case, we wait to see if multitouch_sim
|
||||
# is True or not before doing the multitouch)
|
||||
create_flag = (
|
||||
not self.disable_multitouch
|
||||
and not self.multitouch_on_demand
|
||||
)
|
||||
touch.update_graphics(win, create_flag)
|
||||
self.waiting_event.append(('begin', touch))
|
||||
return touch
|
||||
|
||||
def remove_touch(self, win, touch):
|
||||
if touch.id in self.touches:
|
||||
del self.touches[touch.id]
|
||||
touch.update_time_end()
|
||||
self.waiting_event.append(('end', touch))
|
||||
touch.clear_graphics(win)
|
||||
|
||||
def create_hover(self, win, etype):
|
||||
nx, ny = win.to_normalized_pos(*win.mouse_pos)
|
||||
# Divide by density because it's used by mouse_pos
|
||||
nx /= win._density
|
||||
ny /= win._density
|
||||
args = (nx, ny)
|
||||
hover = self.hover_event
|
||||
if hover:
|
||||
hover.move(args)
|
||||
else:
|
||||
self.hover_event = hover = MouseMotionEvent(
|
||||
self.device,
|
||||
self.create_event_id(),
|
||||
args,
|
||||
type_id='hover'
|
||||
)
|
||||
if etype == 'end':
|
||||
hover.update_time_end()
|
||||
self.hover_event = None
|
||||
self.waiting_event.append((etype, hover))
|
||||
|
||||
def on_mouse_motion(self, win, x, y, modifiers):
|
||||
nx, ny = win.to_normalized_pos(x, y)
|
||||
ny = 1.0 - ny
|
||||
if self.current_drag:
|
||||
touch = self.current_drag
|
||||
touch.move([nx, ny])
|
||||
touch.update_graphics(win)
|
||||
self.waiting_event.append(('update', touch))
|
||||
elif self.alt_touch is not None and 'alt' not in modifiers:
|
||||
# alt just released ?
|
||||
is_double_tap = 'shift' in modifiers
|
||||
self.create_touch(win, nx, ny, is_double_tap, True, [])
|
||||
|
||||
def on_mouse_press(self, win, x, y, button, modifiers):
|
||||
if self.test_activity():
|
||||
return
|
||||
nx, ny = win.to_normalized_pos(x, y)
|
||||
ny = 1.0 - ny
|
||||
found_touch = self.find_touch(win, nx, ny)
|
||||
if found_touch:
|
||||
self.current_drag = found_touch
|
||||
else:
|
||||
is_double_tap = 'shift' in modifiers
|
||||
do_graphics = (
|
||||
not self.disable_multitouch
|
||||
and (button != 'left' or 'ctrl' in modifiers)
|
||||
)
|
||||
touch = self.create_touch(
|
||||
win, nx, ny, is_double_tap, do_graphics, button
|
||||
)
|
||||
if 'alt' in modifiers:
|
||||
self.alt_touch = touch
|
||||
self.current_drag = None
|
||||
|
||||
def on_mouse_release(self, win, x, y, button, modifiers):
|
||||
if button == 'all':
|
||||
# Special case, if button is all,
|
||||
# then remove all the current touches.
|
||||
for touch in list(self.touches.values()):
|
||||
self.remove_touch(win, touch)
|
||||
self.current_drag = None
|
||||
touch = self.current_drag
|
||||
if touch:
|
||||
not_right = button in (
|
||||
'left',
|
||||
'scrollup', 'scrolldown',
|
||||
'scrollleft', 'scrollright'
|
||||
)
|
||||
not_ctrl = 'ctrl' not in modifiers
|
||||
not_multi = (
|
||||
self.disable_multitouch
|
||||
or 'multitouch_sim' not in touch.profile
|
||||
or not touch.multitouch_sim
|
||||
)
|
||||
if not_right and not_ctrl or not_multi:
|
||||
self.remove_touch(win, touch)
|
||||
self.current_drag = None
|
||||
else:
|
||||
touch.update_graphics(win, True)
|
||||
if self.alt_touch:
|
||||
self.remove_touch(win, self.alt_touch)
|
||||
self.alt_touch = None
|
||||
|
||||
def update_touch_graphics(self, win, *args):
|
||||
for touch in self.touches.values():
|
||||
touch.update_graphics(win)
|
||||
|
||||
def begin_or_update_hover_event(self, win, *args):
|
||||
etype = 'update' if self.hover_event else 'begin'
|
||||
self.create_hover(win, etype)
|
||||
|
||||
def begin_hover_event(self, win, *args):
|
||||
if not self.hover_event:
|
||||
self.create_hover(win, 'begin')
|
||||
|
||||
def update_hover_event(self, win, *args):
|
||||
if self.hover_event:
|
||||
self.create_hover(win, 'update')
|
||||
|
||||
def end_hover_event(self, win, *args):
|
||||
if self.hover_event:
|
||||
self.create_hover(win, 'end')
|
||||
|
||||
def update(self, dispatch_fn):
|
||||
'''Update the mouse provider (pop event from the queue)'''
|
||||
try:
|
||||
while True:
|
||||
event = self.waiting_event.popleft()
|
||||
dispatch_fn(*event)
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
|
||||
# registers
|
||||
MotionEventFactory.register('mouse', MouseMotionEventProvider)
|
||||
383
kivy/input/providers/mtdev.py
Normal file
383
kivy/input/providers/mtdev.py
Normal file
@@ -0,0 +1,383 @@
|
||||
'''
|
||||
Native support for Multitouch devices on Linux, using libmtdev.
|
||||
===============================================================
|
||||
|
||||
The Mtdev project is a part of the Ubuntu Maverick multitouch architecture.
|
||||
You can read more on http://wiki.ubuntu.com/Multitouch
|
||||
|
||||
To configure MTDev, it's preferable to use probesysfs providers.
|
||||
Check :py:class:`~kivy.input.providers.probesysfs` for more information.
|
||||
|
||||
Otherwise, add this to your configuration::
|
||||
|
||||
[input]
|
||||
# devicename = hidinput,/dev/input/eventXX
|
||||
acert230h = mtdev,/dev/input/event2
|
||||
|
||||
.. note::
|
||||
You must have read access to the input event.
|
||||
|
||||
You can use a custom range for the X, Y and pressure values.
|
||||
On some drivers, the range reported is invalid.
|
||||
To fix that, you can add these options to the argument line:
|
||||
|
||||
* invert_x : 1 to invert X axis
|
||||
* invert_y : 1 to invert Y axis
|
||||
* min_position_x : X minimum
|
||||
* max_position_x : X maximum
|
||||
* min_position_y : Y minimum
|
||||
* max_position_y : Y maximum
|
||||
* min_pressure : pressure minimum
|
||||
* max_pressure : pressure maximum
|
||||
* min_touch_major : width shape minimum
|
||||
* max_touch_major : width shape maximum
|
||||
* min_touch_minor : width shape minimum
|
||||
* max_touch_minor : height shape maximum
|
||||
* rotation : 0,90,180 or 270 to rotate
|
||||
|
||||
An inverted display configuration will look like this::
|
||||
|
||||
[input]
|
||||
# example for inverting touch events
|
||||
display = mtdev,/dev/input/event0,invert_x=1,invert_y=1
|
||||
'''
|
||||
|
||||
__all__ = ('MTDMotionEventProvider', 'MTDMotionEvent')
|
||||
|
||||
import os
|
||||
import os.path
|
||||
import time
|
||||
from kivy.input.motionevent import MotionEvent
|
||||
from kivy.input.shape import ShapeRect
|
||||
|
||||
|
||||
class MTDMotionEvent(MotionEvent):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault('is_touch', True)
|
||||
kwargs.setdefault('type_id', 'touch')
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def depack(self, args):
|
||||
if 'x' in args:
|
||||
self.sx = args['x']
|
||||
else:
|
||||
self.sx = -1
|
||||
if 'y' in args:
|
||||
self.sy = args['y']
|
||||
else:
|
||||
self.sy = -1
|
||||
self.profile = ['pos']
|
||||
if 'size_w' in args and 'size_h' in args:
|
||||
self.shape = ShapeRect()
|
||||
self.shape.width = args['size_w']
|
||||
self.shape.height = args['size_h']
|
||||
self.profile.append('shape')
|
||||
if 'pressure' in args:
|
||||
self.pressure = args['pressure']
|
||||
self.profile.append('pressure')
|
||||
super().depack(args)
|
||||
|
||||
def __str__(self):
|
||||
i, sx, sy, d = (self.id, self.sx, self.sy, self.device)
|
||||
return '<MTDMotionEvent id=%d pos=(%f, %f) device=%s>' % (i, sx, sy, d)
|
||||
|
||||
|
||||
if 'KIVY_DOC' in os.environ:
|
||||
|
||||
# documentation hack
|
||||
MTDMotionEventProvider = None
|
||||
|
||||
else:
|
||||
import threading
|
||||
import collections
|
||||
from kivy.lib.mtdev import Device, \
|
||||
MTDEV_TYPE_EV_ABS, MTDEV_CODE_SLOT, MTDEV_CODE_POSITION_X, \
|
||||
MTDEV_CODE_POSITION_Y, MTDEV_CODE_PRESSURE, \
|
||||
MTDEV_CODE_TOUCH_MAJOR, MTDEV_CODE_TOUCH_MINOR, \
|
||||
MTDEV_CODE_TRACKING_ID, MTDEV_ABS_POSITION_X, \
|
||||
MTDEV_ABS_POSITION_Y, MTDEV_ABS_TOUCH_MINOR, \
|
||||
MTDEV_ABS_TOUCH_MAJOR
|
||||
from kivy.input.provider import MotionEventProvider
|
||||
from kivy.input.factory import MotionEventFactory
|
||||
from kivy.logger import Logger
|
||||
|
||||
class MTDMotionEventProvider(MotionEventProvider):
|
||||
|
||||
options = ('min_position_x', 'max_position_x',
|
||||
'min_position_y', 'max_position_y',
|
||||
'min_pressure', 'max_pressure',
|
||||
'min_touch_major', 'max_touch_major',
|
||||
'min_touch_minor', 'max_touch_minor',
|
||||
'invert_x', 'invert_y',
|
||||
'rotation')
|
||||
|
||||
def __init__(self, device, args):
|
||||
super(MTDMotionEventProvider, self).__init__(device, args)
|
||||
self._device = None
|
||||
self.input_fn = None
|
||||
self.default_ranges = dict()
|
||||
|
||||
# split arguments
|
||||
args = args.split(',')
|
||||
if not args:
|
||||
Logger.error('MTD: No filename pass to MTD configuration')
|
||||
Logger.error('MTD: Use /dev/input/event0 for example')
|
||||
return
|
||||
|
||||
# read filename
|
||||
self.input_fn = args[0]
|
||||
Logger.info('MTD: Read event from <%s>' % self.input_fn)
|
||||
|
||||
# read parameters
|
||||
for arg in args[1:]:
|
||||
if arg == '':
|
||||
continue
|
||||
arg = arg.split('=')
|
||||
|
||||
# ensure it's a key = value
|
||||
if len(arg) != 2:
|
||||
err = 'MTD: Bad parameter %s: Not in key=value format' %\
|
||||
arg
|
||||
Logger.error(err)
|
||||
continue
|
||||
|
||||
# ensure the key exist
|
||||
key, value = arg
|
||||
if key not in MTDMotionEventProvider.options:
|
||||
Logger.error('MTD: unknown %s option' % key)
|
||||
continue
|
||||
|
||||
# ensure the value
|
||||
try:
|
||||
self.default_ranges[key] = int(value)
|
||||
except ValueError:
|
||||
err = 'MTD: invalid value %s for option %s' % (key, value)
|
||||
Logger.error(err)
|
||||
continue
|
||||
|
||||
# all good!
|
||||
Logger.info('MTD: Set custom %s to %d' % (key, int(value)))
|
||||
|
||||
if 'rotation' not in self.default_ranges:
|
||||
self.default_ranges['rotation'] = 0
|
||||
elif self.default_ranges['rotation'] not in (0, 90, 180, 270):
|
||||
Logger.error('HIDInput: invalid rotation value ({})'.format(
|
||||
self.default_ranges['rotation']))
|
||||
self.default_ranges['rotation'] = 0
|
||||
|
||||
def start(self):
|
||||
if self.input_fn is None:
|
||||
return
|
||||
self.uid = 0
|
||||
self.queue = collections.deque()
|
||||
self.thread = threading.Thread(
|
||||
name=self.__class__.__name__,
|
||||
target=self._thread_run,
|
||||
kwargs=dict(
|
||||
queue=self.queue,
|
||||
input_fn=self.input_fn,
|
||||
device=self.device,
|
||||
default_ranges=self.default_ranges))
|
||||
self.thread.daemon = True
|
||||
self.thread.start()
|
||||
|
||||
def _thread_run(self, **kwargs):
|
||||
input_fn = kwargs.get('input_fn')
|
||||
queue = kwargs.get('queue')
|
||||
device = kwargs.get('device')
|
||||
drs = kwargs.get('default_ranges').get
|
||||
touches = {}
|
||||
touches_sent = []
|
||||
point = {}
|
||||
l_points = {}
|
||||
|
||||
def assign_coord(point, value, invert, coords):
|
||||
cx, cy = coords
|
||||
if invert:
|
||||
value = 1. - value
|
||||
if rotation == 0:
|
||||
point[cx] = value
|
||||
elif rotation == 90:
|
||||
point[cy] = value
|
||||
elif rotation == 180:
|
||||
point[cx] = 1. - value
|
||||
elif rotation == 270:
|
||||
point[cy] = 1. - value
|
||||
|
||||
def process(points):
|
||||
for args in points:
|
||||
# this can happen if we have a touch going on already at
|
||||
# the start of the app
|
||||
if 'id' not in args:
|
||||
continue
|
||||
tid = args['id']
|
||||
try:
|
||||
touch = touches[tid]
|
||||
except KeyError:
|
||||
touch = MTDMotionEvent(device, tid, args)
|
||||
touches[touch.id] = touch
|
||||
touch.move(args)
|
||||
action = 'update'
|
||||
if tid not in touches_sent:
|
||||
action = 'begin'
|
||||
touches_sent.append(tid)
|
||||
if 'delete' in args:
|
||||
action = 'end'
|
||||
del args['delete']
|
||||
del touches[touch.id]
|
||||
touches_sent.remove(tid)
|
||||
touch.update_time_end()
|
||||
queue.append((action, touch))
|
||||
|
||||
def normalize(value, vmin, vmax):
|
||||
try:
|
||||
return (value - vmin) / float(vmax - vmin)
|
||||
except ZeroDivisionError: # it's both in py2 and py3
|
||||
return (value - vmin)
|
||||
|
||||
# open mtdev device
|
||||
_fn = input_fn
|
||||
_slot = 0
|
||||
try:
|
||||
_device = Device(_fn)
|
||||
except OSError as e:
|
||||
if e.errno == 13: # Permission denied
|
||||
Logger.warn(
|
||||
'MTD: Unable to open device "{0}". Please ensure you'
|
||||
' have the appropriate permissions.'.format(_fn))
|
||||
return
|
||||
else:
|
||||
raise
|
||||
_changes = set()
|
||||
|
||||
# prepare some vars to get limit of some component
|
||||
ab = _device.get_abs(MTDEV_ABS_POSITION_X)
|
||||
range_min_position_x = drs('min_position_x', ab.minimum)
|
||||
range_max_position_x = drs('max_position_x', ab.maximum)
|
||||
Logger.info('MTD: <%s> range position X is %d - %d' %
|
||||
(_fn, range_min_position_x, range_max_position_x))
|
||||
|
||||
ab = _device.get_abs(MTDEV_ABS_POSITION_Y)
|
||||
range_min_position_y = drs('min_position_y', ab.minimum)
|
||||
range_max_position_y = drs('max_position_y', ab.maximum)
|
||||
Logger.info('MTD: <%s> range position Y is %d - %d' %
|
||||
(_fn, range_min_position_y, range_max_position_y))
|
||||
|
||||
ab = _device.get_abs(MTDEV_ABS_TOUCH_MAJOR)
|
||||
range_min_major = drs('min_touch_major', ab.minimum)
|
||||
range_max_major = drs('max_touch_major', ab.maximum)
|
||||
Logger.info('MTD: <%s> range touch major is %d - %d' %
|
||||
(_fn, range_min_major, range_max_major))
|
||||
|
||||
ab = _device.get_abs(MTDEV_ABS_TOUCH_MINOR)
|
||||
range_min_minor = drs('min_touch_minor', ab.minimum)
|
||||
range_max_minor = drs('max_touch_minor', ab.maximum)
|
||||
Logger.info('MTD: <%s> range touch minor is %d - %d' %
|
||||
(_fn, range_min_minor, range_max_minor))
|
||||
|
||||
range_min_pressure = drs('min_pressure', 0)
|
||||
range_max_pressure = drs('max_pressure', 255)
|
||||
Logger.info('MTD: <%s> range pressure is %d - %d' %
|
||||
(_fn, range_min_pressure, range_max_pressure))
|
||||
|
||||
invert_x = int(bool(drs('invert_x', 0)))
|
||||
invert_y = int(bool(drs('invert_y', 0)))
|
||||
Logger.info('MTD: <%s> axes invertion: 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)
|
||||
254
kivy/input/providers/probesysfs.py
Normal file
254
kivy/input/providers/probesysfs.py
Normal file
@@ -0,0 +1,254 @@
|
||||
'''
|
||||
Auto Create Input Provider Config Entry for Available MT Hardware (linux only).
|
||||
===============================================================================
|
||||
|
||||
Thanks to Marc Tardif for the probing code, taken from scan-for-mt-device.
|
||||
|
||||
The device discovery is done by this provider. However, the reading of
|
||||
input can be performed by other providers like: hidinput, mtdev and
|
||||
linuxwacom. mtdev is used prior to other providers. For more
|
||||
information about mtdev, check :py:class:`~kivy.input.providers.mtdev`.
|
||||
|
||||
Here is an example of auto creation::
|
||||
|
||||
[input]
|
||||
# using mtdev
|
||||
device_%(name)s = probesysfs,provider=mtdev
|
||||
# using hidinput
|
||||
device_%(name)s = probesysfs,provider=hidinput
|
||||
# using mtdev with a match on name
|
||||
device_%(name)s = probesysfs,provider=mtdev,match=acer
|
||||
|
||||
# using hidinput with custom parameters to hidinput (all on one line)
|
||||
%(name)s = probesysfs,
|
||||
provider=hidinput,param=min_pressure=1,param=max_pressure=99
|
||||
|
||||
# you can also match your wacom touchscreen
|
||||
touch = probesysfs,match=E3 Finger,provider=linuxwacom,
|
||||
select_all=1,param=mode=touch
|
||||
# and your wacom pen
|
||||
pen = probesysfs,match=E3 Pen,provider=linuxwacom,
|
||||
select_all=1,param=mode=pen
|
||||
|
||||
By default, ProbeSysfs module will enumerate hardware from the /sys/class/input
|
||||
device, and configure hardware with ABS_MT_POSITION_X capability. But for
|
||||
example, the wacom screen doesn't support this capability. You can prevent this
|
||||
behavior by putting select_all=1 in your config line. Add use_mouse=1 to also
|
||||
include touchscreen hardware that offers core pointer functionality.
|
||||
'''
|
||||
|
||||
__all__ = ('ProbeSysfsHardwareProbe', )
|
||||
|
||||
import os
|
||||
from os.path import sep
|
||||
|
||||
if 'KIVY_DOC' in os.environ:
|
||||
|
||||
ProbeSysfsHardwareProbe = None
|
||||
|
||||
else:
|
||||
import ctypes
|
||||
from re import match, IGNORECASE
|
||||
from glob import glob
|
||||
from subprocess import Popen, PIPE
|
||||
from kivy.logger import Logger
|
||||
from kivy.input.provider import MotionEventProvider
|
||||
from kivy.input.providers.mouse import MouseMotionEventProvider
|
||||
from kivy.input.factory import MotionEventFactory
|
||||
from kivy.config import _is_rpi
|
||||
|
||||
EventLoop = None
|
||||
|
||||
# See linux/input.h
|
||||
ABS_MT_POSITION_X = 0x35
|
||||
|
||||
_cache_input = None
|
||||
_cache_xinput = None
|
||||
|
||||
class Input(object):
|
||||
|
||||
def __init__(self, path):
|
||||
query_xinput()
|
||||
self.path = path
|
||||
|
||||
@property
|
||||
def device(self):
|
||||
base = os.path.basename(self.path)
|
||||
return os.path.join("/dev", "input", base)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
path = os.path.join(self.path, "device", "name")
|
||||
return read_line(path)
|
||||
|
||||
def get_capabilities(self):
|
||||
path = os.path.join(self.path, "device", "capabilities", "abs")
|
||||
line = "0"
|
||||
try:
|
||||
line = read_line(path)
|
||||
except (IOError, OSError):
|
||||
return []
|
||||
|
||||
capabilities = []
|
||||
long_bit = ctypes.sizeof(ctypes.c_long) * 8
|
||||
for i, word in enumerate(line.split(" ")):
|
||||
word = int(word, 16)
|
||||
subcapabilities = [bool(word & 1 << i)
|
||||
for i in range(long_bit)]
|
||||
capabilities[:0] = subcapabilities
|
||||
|
||||
return capabilities
|
||||
|
||||
def has_capability(self, capability):
|
||||
capabilities = self.get_capabilities()
|
||||
return len(capabilities) > capability and capabilities[capability]
|
||||
|
||||
@property
|
||||
def is_mouse(self):
|
||||
return self.device in _cache_xinput
|
||||
|
||||
def getout(*args):
|
||||
try:
|
||||
return Popen(args, stdout=PIPE).communicate()[0]
|
||||
except OSError:
|
||||
return ''
|
||||
|
||||
def query_xinput():
|
||||
global _cache_xinput
|
||||
if _cache_xinput is None:
|
||||
_cache_xinput = []
|
||||
devids = getout('xinput', '--list', '--id-only')
|
||||
for did in devids.splitlines():
|
||||
devprops = getout('xinput', '--list-props', did)
|
||||
evpath = None
|
||||
for prop in devprops.splitlines():
|
||||
prop = prop.strip()
|
||||
if (prop.startswith(b'Device Enabled') and
|
||||
prop.endswith(b'0')):
|
||||
evpath = None
|
||||
break
|
||||
if prop.startswith(b'Device Node'):
|
||||
try:
|
||||
evpath = prop.split('"')[1]
|
||||
except Exception:
|
||||
evpath = None
|
||||
if evpath:
|
||||
_cache_xinput.append(evpath)
|
||||
|
||||
def get_inputs(path):
|
||||
global _cache_input
|
||||
if _cache_input is None:
|
||||
event_glob = os.path.join(path, "event*")
|
||||
_cache_input = [Input(x) for x in glob(event_glob)]
|
||||
return _cache_input
|
||||
|
||||
def read_line(path):
|
||||
f = open(path)
|
||||
try:
|
||||
return f.readline().strip()
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
class ProbeSysfsHardwareProbe(MotionEventProvider):
|
||||
|
||||
def __new__(self, device, args):
|
||||
# hack to not return an instance of this provider.
|
||||
# :)
|
||||
instance = super(ProbeSysfsHardwareProbe, self).__new__(self)
|
||||
instance.__init__(device, args)
|
||||
|
||||
def __init__(self, device, args):
|
||||
super(ProbeSysfsHardwareProbe, self).__init__(device, args)
|
||||
self.provider = 'mtdev'
|
||||
self.match = None
|
||||
self.input_path = '/sys/class/input'
|
||||
self.select_all = True if _is_rpi else False
|
||||
self.use_mouse = False
|
||||
self.use_regex = False
|
||||
self.args = []
|
||||
|
||||
args = args.split(',')
|
||||
for arg in args:
|
||||
if arg == '':
|
||||
continue
|
||||
arg = arg.split('=', 1)
|
||||
# ensure it's a key = value
|
||||
if len(arg) != 2:
|
||||
Logger.error('ProbeSysfs: invalid parameters %s, not'
|
||||
' key=value format' % arg)
|
||||
continue
|
||||
|
||||
key, value = arg
|
||||
if key == 'match':
|
||||
self.match = value
|
||||
elif key == 'provider':
|
||||
self.provider = value
|
||||
elif key == 'use_regex':
|
||||
self.use_regex = bool(int(value))
|
||||
elif key == 'select_all':
|
||||
self.select_all = bool(int(value))
|
||||
elif key == 'use_mouse':
|
||||
self.use_mouse = bool(int(value))
|
||||
elif key == 'param':
|
||||
self.args.append(value)
|
||||
else:
|
||||
Logger.error('ProbeSysfs: unknown %s option' % key)
|
||||
continue
|
||||
|
||||
self.probe()
|
||||
|
||||
def should_use_mouse(self):
|
||||
return (self.use_mouse or
|
||||
not any(p for p in EventLoop.input_providers
|
||||
if isinstance(p, MouseMotionEventProvider)))
|
||||
|
||||
def probe(self):
|
||||
global EventLoop
|
||||
from kivy.base import EventLoop
|
||||
|
||||
inputs = get_inputs(self.input_path)
|
||||
Logger.debug('ProbeSysfs: using probesysfs!')
|
||||
|
||||
use_mouse = self.should_use_mouse()
|
||||
|
||||
if not self.select_all:
|
||||
inputs = [x for x in inputs if
|
||||
x.has_capability(ABS_MT_POSITION_X) and
|
||||
(use_mouse or not x.is_mouse)]
|
||||
for device in inputs:
|
||||
Logger.debug('ProbeSysfs: found device: %s at %s' % (
|
||||
device.name, device.device))
|
||||
|
||||
# must ignore ?
|
||||
if self.match:
|
||||
if self.use_regex:
|
||||
if not match(self.match, device.name, IGNORECASE):
|
||||
Logger.debug('ProbeSysfs: device not match the'
|
||||
' rule in config, ignoring.')
|
||||
continue
|
||||
else:
|
||||
if self.match not in device.name:
|
||||
continue
|
||||
|
||||
Logger.info('ProbeSysfs: device match: %s' % device.device)
|
||||
|
||||
d = device.device
|
||||
devicename = self.device % dict(name=d.split(sep)[-1])
|
||||
|
||||
provider = MotionEventFactory.get(self.provider)
|
||||
if provider is None:
|
||||
Logger.info('ProbeSysfs: Unable to find provider %s' %
|
||||
self.provider)
|
||||
Logger.info('ProbeSysfs: fallback on hidinput')
|
||||
provider = MotionEventFactory.get('hidinput')
|
||||
if provider is None:
|
||||
Logger.critical('ProbeSysfs: no input provider found'
|
||||
' to handle this device !')
|
||||
continue
|
||||
|
||||
instance = provider(devicename, '%s,%s' % (
|
||||
device.device, ','.join(self.args)))
|
||||
if instance:
|
||||
EventLoop.add_input_provider(instance)
|
||||
|
||||
MotionEventFactory.register('probesysfs', ProbeSysfsHardwareProbe)
|
||||
326
kivy/input/providers/tuio.py
Normal file
326
kivy/input/providers/tuio.py
Normal file
@@ -0,0 +1,326 @@
|
||||
'''
|
||||
TUIO Input Provider
|
||||
===================
|
||||
|
||||
TUIO is the de facto standard network protocol for the transmission of
|
||||
touch and fiducial information between a server and a client. To learn
|
||||
more about TUIO (which is itself based on the OSC protocol), please
|
||||
refer to http://tuio.org -- The specification should be of special
|
||||
interest.
|
||||
|
||||
Configure a TUIO provider in the config.ini
|
||||
-------------------------------------------
|
||||
|
||||
The TUIO provider can be configured in the configuration file in the
|
||||
``[input]`` section::
|
||||
|
||||
[input]
|
||||
# name = tuio,<ip>:<port>
|
||||
multitouchtable = tuio,192.168.0.1:3333
|
||||
|
||||
Configure a TUIO provider in the App
|
||||
------------------------------------
|
||||
|
||||
You must add the provider before your application is run, like this::
|
||||
|
||||
from kivy.app import App
|
||||
from kivy.config import Config
|
||||
|
||||
class TestApp(App):
|
||||
def build(self):
|
||||
Config.set('input', 'multitouchscreen1', 'tuio,0.0.0.0:3333')
|
||||
# You can also add a second TUIO listener
|
||||
# Config.set('input', 'source2', 'tuio,0.0.0.0:3334')
|
||||
# Then do the usual things
|
||||
# ...
|
||||
return
|
||||
'''
|
||||
|
||||
__all__ = ('TuioMotionEventProvider', 'Tuio2dCurMotionEvent',
|
||||
'Tuio2dObjMotionEvent')
|
||||
|
||||
from kivy.logger import Logger
|
||||
|
||||
from functools import partial
|
||||
from collections import deque
|
||||
from kivy.input.provider import MotionEventProvider
|
||||
from kivy.input.factory import MotionEventFactory
|
||||
from kivy.input.motionevent import MotionEvent
|
||||
from kivy.input.shape import ShapeRect
|
||||
|
||||
|
||||
class TuioMotionEventProvider(MotionEventProvider):
|
||||
'''The TUIO provider listens to a socket and handles some of the incoming
|
||||
OSC messages:
|
||||
|
||||
* /tuio/2Dcur
|
||||
* /tuio/2Dobj
|
||||
|
||||
You can easily extend the provider to handle new TUIO paths like so::
|
||||
|
||||
# Create a class to handle the new TUIO type/path
|
||||
# Replace NEWPATH with the pathname you want to handle
|
||||
class TuioNEWPATHMotionEvent(MotionEvent):
|
||||
|
||||
def depack(self, args):
|
||||
# In this method, implement 'unpacking' for the received
|
||||
# arguments. you basically translate from TUIO args to Kivy
|
||||
# MotionEvent variables. If all you receive are x and y
|
||||
# values, you can do it like this:
|
||||
if len(args) == 2:
|
||||
self.sx, self.sy = args
|
||||
self.profile = ('pos', )
|
||||
self.sy = 1 - self.sy
|
||||
super().depack(args)
|
||||
|
||||
# Register it with the TUIO MotionEvent provider.
|
||||
# You obviously need to replace the PATH placeholders appropriately.
|
||||
TuioMotionEventProvider.register('/tuio/PATH', TuioNEWPATHMotionEvent)
|
||||
|
||||
.. note::
|
||||
|
||||
The class name is of no technical importance. Your class will be
|
||||
associated with the path that you pass to the ``register()``
|
||||
function. To keep things simple, you should name your class after the
|
||||
path that it handles, though.
|
||||
'''
|
||||
|
||||
__handlers__ = {}
|
||||
|
||||
def __init__(self, device, args):
|
||||
super().__init__(device, args)
|
||||
args = args.split(',')
|
||||
if len(args) == 0:
|
||||
Logger.error('Tuio: Invalid configuration for TUIO provider')
|
||||
Logger.error('Tuio: Format must be ip:port (eg. 127.0.0.1:3333)')
|
||||
err = 'Tuio: Current configuration is <%s>' % (str(','.join(args)))
|
||||
Logger.error(err)
|
||||
return
|
||||
ipport = args[0].split(':')
|
||||
if len(ipport) != 2:
|
||||
Logger.error('Tuio: Invalid configuration for TUIO provider')
|
||||
Logger.error('Tuio: Format must be ip:port (eg. 127.0.0.1:3333)')
|
||||
err = 'Tuio: Current configuration is <%s>' % (str(','.join(args)))
|
||||
Logger.error(err)
|
||||
return
|
||||
self.ip, self.port = args[0].split(':')
|
||||
self.port = int(self.port)
|
||||
self.handlers = {}
|
||||
self.oscid = None
|
||||
self.tuio_event_q = deque()
|
||||
self.touches = {}
|
||||
|
||||
@staticmethod
|
||||
def register(oscpath, classname):
|
||||
'''Register a new path to handle in TUIO provider'''
|
||||
TuioMotionEventProvider.__handlers__[oscpath] = classname
|
||||
|
||||
@staticmethod
|
||||
def unregister(oscpath, classname):
|
||||
'''Unregister a path to stop handling it in the TUIO provider'''
|
||||
if oscpath in TuioMotionEventProvider.__handlers__:
|
||||
del TuioMotionEventProvider.__handlers__[oscpath]
|
||||
|
||||
@staticmethod
|
||||
def create(oscpath, **kwargs):
|
||||
'''Create a touch event from a TUIO path'''
|
||||
if oscpath not in TuioMotionEventProvider.__handlers__:
|
||||
raise Exception('Unknown %s touch path' % oscpath)
|
||||
return TuioMotionEventProvider.__handlers__[oscpath](**kwargs)
|
||||
|
||||
def start(self):
|
||||
'''Start the TUIO provider'''
|
||||
try:
|
||||
from oscpy.server import OSCThreadServer
|
||||
except ImportError:
|
||||
Logger.info(
|
||||
'Please install the oscpy python module to use the TUIO '
|
||||
'provider.'
|
||||
)
|
||||
raise
|
||||
self.oscid = osc = OSCThreadServer()
|
||||
osc.listen(self.ip, self.port, default=True)
|
||||
for oscpath in TuioMotionEventProvider.__handlers__:
|
||||
self.touches[oscpath] = {}
|
||||
osc.bind(oscpath, partial(self._osc_tuio_cb, oscpath))
|
||||
|
||||
def stop(self):
|
||||
'''Stop the TUIO provider'''
|
||||
self.oscid.stop_all()
|
||||
|
||||
def update(self, dispatch_fn):
|
||||
'''Update the TUIO provider (pop events from the queue)'''
|
||||
|
||||
# read the Queue with event
|
||||
while True:
|
||||
try:
|
||||
value = self.tuio_event_q.pop()
|
||||
except IndexError:
|
||||
# queue is empty, we're done for now
|
||||
return
|
||||
self._update(dispatch_fn, value)
|
||||
|
||||
def _osc_tuio_cb(self, oscpath, address, *args):
|
||||
self.tuio_event_q.appendleft([oscpath, address, args])
|
||||
|
||||
def _update(self, dispatch_fn, value):
|
||||
oscpath, command, args = value
|
||||
|
||||
# verify commands
|
||||
if command not in [b'alive', b'set']:
|
||||
return
|
||||
|
||||
# move or create a new touch
|
||||
if command == b'set':
|
||||
id = args[0]
|
||||
if id not in self.touches[oscpath]:
|
||||
# new touch
|
||||
touch = TuioMotionEventProvider.__handlers__[oscpath](
|
||||
self.device, id, args[1:])
|
||||
self.touches[oscpath][id] = touch
|
||||
dispatch_fn('begin', touch)
|
||||
else:
|
||||
# update a current touch
|
||||
touch = self.touches[oscpath][id]
|
||||
touch.move(args[1:])
|
||||
dispatch_fn('update', touch)
|
||||
|
||||
# alive event, check for deleted touch
|
||||
if command == b'alive':
|
||||
alives = args
|
||||
to_delete = []
|
||||
for id in self.touches[oscpath]:
|
||||
if id not in alives:
|
||||
# touch up
|
||||
touch = self.touches[oscpath][id]
|
||||
if touch not in to_delete:
|
||||
to_delete.append(touch)
|
||||
|
||||
for touch in to_delete:
|
||||
dispatch_fn('end', touch)
|
||||
del self.touches[oscpath][touch.id]
|
||||
|
||||
|
||||
class TuioMotionEvent(MotionEvent):
|
||||
'''Abstraction for TUIO touches/fiducials.
|
||||
|
||||
Depending on the tracking software you use (e.g. Movid, CCV, etc.) and its
|
||||
TUIO implementation, the TuioMotionEvent object can support multiple
|
||||
profiles such as:
|
||||
|
||||
* Fiducial ID: profile name 'markerid', attribute ``.fid``
|
||||
* Position: profile name 'pos', attributes ``.x``, ``.y``
|
||||
* Angle: profile name 'angle', attribute ``.a``
|
||||
* Velocity vector: profile name 'mov', attributes ``.X``, ``.Y``
|
||||
* Rotation velocity: profile name 'rot', attribute ``.A``
|
||||
* Motion acceleration: profile name 'motacc', attribute ``.m``
|
||||
* Rotation acceleration: profile name 'rotacc', attribute ``.r``
|
||||
'''
|
||||
__attrs__ = ('a', 'b', 'c', 'X', 'Y', 'Z', 'A', 'B', 'C', 'm', 'r')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault('is_touch', True)
|
||||
kwargs.setdefault('type_id', 'touch')
|
||||
super().__init__(*args, **kwargs)
|
||||
# Default argument for TUIO touches
|
||||
self.a = 0.0
|
||||
self.b = 0.0
|
||||
self.c = 0.0
|
||||
self.X = 0.0
|
||||
self.Y = 0.0
|
||||
self.Z = 0.0
|
||||
self.A = 0.0
|
||||
self.B = 0.0
|
||||
self.C = 0.0
|
||||
self.m = 0.0
|
||||
self.r = 0.0
|
||||
|
||||
angle = property(lambda self: self.a)
|
||||
mot_accel = property(lambda self: self.m)
|
||||
rot_accel = property(lambda self: self.r)
|
||||
xmot = property(lambda self: self.X)
|
||||
ymot = property(lambda self: self.Y)
|
||||
zmot = property(lambda self: self.Z)
|
||||
|
||||
|
||||
class Tuio2dCurMotionEvent(TuioMotionEvent):
|
||||
'''A 2dCur TUIO touch.'''
|
||||
|
||||
def depack(self, args):
|
||||
if len(args) < 5:
|
||||
self.sx, self.sy = list(map(float, args[0:2]))
|
||||
self.profile = ('pos', )
|
||||
elif len(args) == 5:
|
||||
self.sx, self.sy, self.X, self.Y, self.m = list(map(float,
|
||||
args[0:5]))
|
||||
self.Y = -self.Y
|
||||
self.profile = ('pos', 'mov', 'motacc')
|
||||
else:
|
||||
self.sx, self.sy, self.X, self.Y = list(map(float, args[0:4]))
|
||||
self.m, width, height = list(map(float, args[4:7]))
|
||||
self.Y = -self.Y
|
||||
self.profile = ('pos', 'mov', 'motacc', 'shape')
|
||||
if self.shape is None:
|
||||
self.shape = ShapeRect()
|
||||
self.shape.width = width
|
||||
self.shape.height = height
|
||||
self.sy = 1 - self.sy
|
||||
super().depack(args)
|
||||
|
||||
|
||||
class Tuio2dObjMotionEvent(TuioMotionEvent):
|
||||
'''A 2dObj TUIO object.
|
||||
'''
|
||||
|
||||
def depack(self, args):
|
||||
if len(args) < 5:
|
||||
self.sx, self.sy = args[0:2]
|
||||
self.profile = ('pos', )
|
||||
elif len(args) == 9:
|
||||
self.fid, self.sx, self.sy, self.a, self.X, self.Y = args[:6]
|
||||
self.A, self.m, self.r = args[6:9]
|
||||
self.Y = -self.Y
|
||||
self.profile = ('markerid', 'pos', 'angle', 'mov', 'rot',
|
||||
'motacc', 'rotacc')
|
||||
else:
|
||||
self.fid, self.sx, self.sy, self.a, self.X, self.Y = args[:6]
|
||||
self.A, self.m, self.r, width, height = args[6:11]
|
||||
self.Y = -self.Y
|
||||
self.profile = ('markerid', 'pos', 'angle', 'mov', 'rot', 'rotacc',
|
||||
'acc', 'shape')
|
||||
if self.shape is None:
|
||||
self.shape = ShapeRect()
|
||||
self.shape.width = width
|
||||
self.shape.height = height
|
||||
self.sy = 1 - self.sy
|
||||
super().depack(args)
|
||||
|
||||
|
||||
class Tuio2dBlbMotionEvent(TuioMotionEvent):
|
||||
'''A 2dBlb TUIO object.
|
||||
# FIXME 3d shape are not supported
|
||||
/tuio/2Dobj set s i x y a X Y A m r
|
||||
/tuio/2Dblb set s x y a w h f X Y A m r
|
||||
'''
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.profile = ('pos', 'angle', 'mov', 'rot', 'rotacc', 'acc', 'shape')
|
||||
|
||||
def depack(self, args):
|
||||
self.sx, self.sy, self.a, self.X, self.Y, sw, sh, sd, \
|
||||
self.A, self.m, self.r = args
|
||||
self.Y = -self.Y
|
||||
if self.shape is None:
|
||||
self.shape = ShapeRect()
|
||||
self.shape.width = sw
|
||||
self.shape.height = sh
|
||||
self.sy = 1 - self.sy
|
||||
super().depack(args)
|
||||
|
||||
|
||||
# registers
|
||||
TuioMotionEventProvider.register(b'/tuio/2Dcur', Tuio2dCurMotionEvent)
|
||||
TuioMotionEventProvider.register(b'/tuio/2Dobj', Tuio2dObjMotionEvent)
|
||||
TuioMotionEventProvider.register(b'/tuio/2Dblb', Tuio2dBlbMotionEvent)
|
||||
MotionEventFactory.register('tuio', TuioMotionEventProvider)
|
||||
162
kivy/input/providers/wm_common.py
Normal file
162
kivy/input/providers/wm_common.py
Normal file
@@ -0,0 +1,162 @@
|
||||
'''
|
||||
Common definitions for a Windows provider
|
||||
=========================================
|
||||
|
||||
This file provides common definitions for constants used by WM_Touch / WM_Pen.
|
||||
'''
|
||||
import os
|
||||
|
||||
WM_MOUSEFIRST = 512
|
||||
WM_MOUSEMOVE = 512
|
||||
WM_LBUTTONDOWN = 513
|
||||
WM_LBUTTONUP = 514
|
||||
WM_LBUTTONDBLCLK = 515
|
||||
WM_RBUTTONDOWN = 516
|
||||
WM_RBUTTONUP = 517
|
||||
WM_RBUTTONDBLCLK = 518
|
||||
WM_MBUTTONDOWN = 519
|
||||
WM_MBUTTONUP = 520
|
||||
WM_MBUTTONDBLCLK = 521
|
||||
WM_MOUSEWHEEL = 522
|
||||
WM_MOUSELAST = 522
|
||||
WM_DPICHANGED = 736
|
||||
WM_GETDPISCALEDSIZE = 740
|
||||
WM_NCCALCSIZE = 131
|
||||
|
||||
WM_TOUCH = 576
|
||||
TOUCHEVENTF_MOVE = 1
|
||||
TOUCHEVENTF_DOWN = 2
|
||||
TOUCHEVENTF_UP = 4
|
||||
|
||||
PEN_OR_TOUCH_SIGNATURE = 0xFF515700
|
||||
PEN_OR_TOUCH_MASK = 0xFFFFFF00
|
||||
PEN_EVENT_TOUCH_MASK = 0x80
|
||||
|
||||
SM_CYCAPTION = 4
|
||||
|
||||
WM_TABLET_QUERYSYSTEMGESTURE = 0x000002CC
|
||||
TABLET_DISABLE_PRESSANDHOLD = 0x00000001
|
||||
TABLET_DISABLE_PENTAPFEEDBACK = 0x00000008
|
||||
TABLET_DISABLE_PENBARRELFEEDBACK = 0x00000010
|
||||
TABLET_DISABLE_TOUCHUIFORCEON = 0x00000100
|
||||
TABLET_DISABLE_TOUCHUIFORCEOFF = 0x00000200
|
||||
TABLET_DISABLE_TOUCHSWITCH = 0x00008000
|
||||
TABLET_DISABLE_FLICKS = 0x00010000
|
||||
TABLET_ENABLE_FLICKSONCONTEXT = 0x00020000
|
||||
TABLET_ENABLE_FLICKLEARNINGMODE = 0x00040000
|
||||
TABLET_DISABLE_SMOOTHSCROLLING = 0x00080000
|
||||
TABLET_DISABLE_FLICKFALLBACKKEYS = 0x00100000
|
||||
GWL_WNDPROC = -4
|
||||
|
||||
|
||||
QUERYSYSTEMGESTURE_WNDPROC = (
|
||||
TABLET_DISABLE_PRESSANDHOLD |
|
||||
TABLET_DISABLE_PENTAPFEEDBACK |
|
||||
TABLET_DISABLE_PENBARRELFEEDBACK |
|
||||
TABLET_DISABLE_SMOOTHSCROLLING |
|
||||
TABLET_DISABLE_FLICKFALLBACKKEYS |
|
||||
TABLET_DISABLE_TOUCHSWITCH |
|
||||
TABLET_DISABLE_FLICKS)
|
||||
|
||||
if 'KIVY_DOC' not in os.environ:
|
||||
from ctypes.wintypes import (ULONG, HANDLE, DWORD, LONG, UINT,
|
||||
WPARAM, LPARAM, BOOL, HWND, POINT,
|
||||
RECT as RECT_BASE)
|
||||
from ctypes import (windll, WINFUNCTYPE, POINTER,
|
||||
c_int, c_longlong, c_void_p, Structure,
|
||||
sizeof, byref, cast)
|
||||
|
||||
class RECT(RECT_BASE):
|
||||
x = property(lambda self: self.left)
|
||||
y = property(lambda self: self.top)
|
||||
w = property(lambda self: self.right - self.left)
|
||||
h = property(lambda self: self.bottom - self.top)
|
||||
|
||||
# check availability of RegisterTouchWindow
|
||||
if not hasattr(windll.user32, 'RegisterTouchWindow'):
|
||||
raise Exception('Unsupported Window version')
|
||||
|
||||
LRESULT = LPARAM
|
||||
WNDPROC = WINFUNCTYPE(LRESULT, HWND, UINT, WPARAM, LPARAM)
|
||||
|
||||
class TOUCHINPUT(Structure):
|
||||
_fields_ = [
|
||||
('x', LONG),
|
||||
('y', LONG),
|
||||
('pSource', HANDLE),
|
||||
('id', DWORD),
|
||||
('flags', DWORD),
|
||||
('mask', DWORD),
|
||||
('time', DWORD),
|
||||
('extraInfo', POINTER(ULONG)),
|
||||
('size_x', DWORD),
|
||||
('size_y', DWORD)]
|
||||
|
||||
def size(self):
|
||||
return (self.size_x, self.size_y)
|
||||
|
||||
def screen_x(self):
|
||||
return self.x / 100.0
|
||||
|
||||
def screen_y(self):
|
||||
return self.y / 100.0
|
||||
|
||||
def _event_type(self):
|
||||
if self.flags & TOUCHEVENTF_MOVE:
|
||||
return 'update'
|
||||
if self.flags & TOUCHEVENTF_DOWN:
|
||||
return 'begin'
|
||||
if self.flags & TOUCHEVENTF_UP:
|
||||
return 'end'
|
||||
event_type = property(_event_type)
|
||||
|
||||
def SetWindowLong_WndProc_wrapper_generator(func):
|
||||
def _closure(hWnd, wndProc):
|
||||
oldAddr = func(hWnd, GWL_WNDPROC, cast(wndProc, c_void_p).value)
|
||||
return cast(c_void_p(oldAddr), WNDPROC)
|
||||
|
||||
return _closure
|
||||
|
||||
try:
|
||||
LONG_PTR = c_longlong
|
||||
windll.user32.SetWindowLongPtrW.restype = LONG_PTR
|
||||
windll.user32.SetWindowLongPtrW.argtypes = [HWND, c_int, LONG_PTR]
|
||||
SetWindowLong_WndProc_wrapper = \
|
||||
SetWindowLong_WndProc_wrapper_generator(
|
||||
windll.user32.SetWindowLongPtrW)
|
||||
except AttributeError:
|
||||
windll.user32.SetWindowLongW.restype = LONG
|
||||
windll.user32.SetWindowLongW.argtypes = [HWND, c_int, LONG]
|
||||
SetWindowLong_WndProc_wrapper = \
|
||||
SetWindowLong_WndProc_wrapper_generator(
|
||||
windll.user32.SetWindowLongW)
|
||||
|
||||
windll.user32.GetMessageExtraInfo.restype = LPARAM
|
||||
windll.user32.GetMessageExtraInfo.argtypes = []
|
||||
windll.user32.GetClientRect.restype = BOOL
|
||||
windll.user32.GetClientRect.argtypes = [HANDLE, POINTER(RECT_BASE)]
|
||||
windll.user32.GetWindowRect.restype = BOOL
|
||||
windll.user32.GetWindowRect.argtypes = [HANDLE, POINTER(RECT_BASE)]
|
||||
windll.user32.CallWindowProcW.restype = LRESULT
|
||||
windll.user32.CallWindowProcW.argtypes = [WNDPROC, HWND, UINT, WPARAM,
|
||||
LPARAM]
|
||||
windll.user32.GetActiveWindow.restype = HWND
|
||||
windll.user32.GetActiveWindow.argtypes = []
|
||||
windll.user32.RegisterTouchWindow.restype = BOOL
|
||||
windll.user32.RegisterTouchWindow.argtypes = [HWND, ULONG]
|
||||
windll.user32.UnregisterTouchWindow.restype = BOOL
|
||||
windll.user32.UnregisterTouchWindow.argtypes = [HWND]
|
||||
windll.user32.GetTouchInputInfo.restype = BOOL
|
||||
windll.user32.GetTouchInputInfo.argtypes = [HANDLE, UINT,
|
||||
POINTER(TOUCHINPUT), c_int]
|
||||
windll.user32.GetSystemMetrics.restype = c_int
|
||||
windll.user32.GetSystemMetrics.argtypes = [c_int]
|
||||
|
||||
windll.user32.ClientToScreen.restype = BOOL
|
||||
windll.user32.ClientToScreen.argtypes = [HWND, POINTER(POINT)]
|
||||
|
||||
try:
|
||||
windll.user32.GetDpiForWindow.restype = UINT
|
||||
windll.user32.GetDpiForWindow.argtypes = [HWND]
|
||||
except AttributeError:
|
||||
pass
|
||||
121
kivy/input/providers/wm_pen.py
Normal file
121
kivy/input/providers/wm_pen.py
Normal file
@@ -0,0 +1,121 @@
|
||||
'''
|
||||
Support for WM_PEN messages (Windows platform)
|
||||
==============================================
|
||||
'''
|
||||
|
||||
__all__ = ('WM_PenProvider', 'WM_Pen')
|
||||
|
||||
import os
|
||||
from kivy.input.providers.wm_common import RECT, PEN_OR_TOUCH_MASK, \
|
||||
PEN_OR_TOUCH_SIGNATURE, PEN_EVENT_TOUCH_MASK, WM_LBUTTONDOWN, \
|
||||
WM_MOUSEMOVE, WM_LBUTTONUP, WM_TABLET_QUERYSYSTEMGESTURE, \
|
||||
QUERYSYSTEMGESTURE_WNDPROC, WNDPROC, SetWindowLong_WndProc_wrapper
|
||||
from kivy.input.motionevent import MotionEvent
|
||||
|
||||
|
||||
class WM_Pen(MotionEvent):
|
||||
'''MotionEvent representing the WM_Pen event. Supports the pos profile.'''
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault('is_touch', True)
|
||||
kwargs.setdefault('type_id', 'touch')
|
||||
super().__init__(*args, **kwargs)
|
||||
self.profile = ['pos']
|
||||
|
||||
def depack(self, args):
|
||||
self.sx, self.sy = args[0], args[1]
|
||||
super().depack(args)
|
||||
|
||||
def __str__(self):
|
||||
i, u, s, d = (self.id, self.uid, str(self.spos), self.device)
|
||||
return '<WMPen id:%d uid:%d pos:%s device:%s>' % (i, u, s, d)
|
||||
|
||||
|
||||
if 'KIVY_DOC' in os.environ:
|
||||
# documentation hack
|
||||
WM_PenProvider = None
|
||||
|
||||
else:
|
||||
from collections import deque
|
||||
from ctypes import windll, byref, c_int16, c_int
|
||||
from kivy.input.provider import MotionEventProvider
|
||||
from kivy.input.factory import MotionEventFactory
|
||||
|
||||
win_rect = RECT()
|
||||
|
||||
class WM_PenProvider(MotionEventProvider):
|
||||
|
||||
def _is_pen_message(self, msg):
|
||||
info = windll.user32.GetMessageExtraInfo()
|
||||
# It's a touch or a pen
|
||||
if (info & PEN_OR_TOUCH_MASK) == PEN_OR_TOUCH_SIGNATURE:
|
||||
if not info & PEN_EVENT_TOUCH_MASK:
|
||||
return True
|
||||
|
||||
def _pen_handler(self, msg, wParam, lParam):
|
||||
if msg not in (WM_LBUTTONDOWN, WM_MOUSEMOVE, WM_LBUTTONUP):
|
||||
return
|
||||
|
||||
windll.user32.GetClientRect(self.hwnd, byref(win_rect))
|
||||
x = c_int16(lParam & 0xffff).value / float(win_rect.w)
|
||||
y = c_int16(lParam >> 16).value / float(win_rect.h)
|
||||
y = abs(1.0 - y)
|
||||
|
||||
if msg == WM_LBUTTONDOWN:
|
||||
self.pen_events.appendleft(('begin', x, y))
|
||||
self.pen_status = True
|
||||
|
||||
if msg == WM_MOUSEMOVE and self.pen_status:
|
||||
self.pen_events.appendleft(('update', x, y))
|
||||
|
||||
if msg == WM_LBUTTONUP:
|
||||
self.pen_events.appendleft(('end', x, y))
|
||||
self.pen_status = False
|
||||
|
||||
def _pen_wndProc(self, hwnd, msg, wParam, lParam):
|
||||
if msg == WM_TABLET_QUERYSYSTEMGESTURE:
|
||||
return QUERYSYSTEMGESTURE_WNDPROC
|
||||
if self._is_pen_message(msg):
|
||||
self._pen_handler(msg, wParam, lParam)
|
||||
return 1
|
||||
else:
|
||||
return windll.user32.CallWindowProcW(self.old_windProc,
|
||||
hwnd, msg, wParam, lParam)
|
||||
|
||||
def start(self):
|
||||
self.uid = 0
|
||||
self.pen = None
|
||||
self.pen_status = None
|
||||
self.pen_events = deque()
|
||||
|
||||
self.hwnd = windll.user32.GetActiveWindow()
|
||||
|
||||
# inject our own wndProc to handle messages
|
||||
# before window manager does
|
||||
self.new_windProc = WNDPROC(self._pen_wndProc)
|
||||
self.old_windProc = SetWindowLong_WndProc_wrapper(
|
||||
self.hwnd, self.new_windProc)
|
||||
|
||||
def update(self, dispatch_fn):
|
||||
while True:
|
||||
|
||||
try:
|
||||
etype, x, y = self.pen_events.pop()
|
||||
except:
|
||||
break
|
||||
|
||||
if etype == 'begin':
|
||||
self.uid += 1
|
||||
self.pen = WM_Pen(self.device, self.uid, [x, y])
|
||||
elif etype == 'update':
|
||||
self.pen.move([x, y])
|
||||
elif etype == 'end':
|
||||
self.pen.update_time_end()
|
||||
|
||||
dispatch_fn(etype, self.pen)
|
||||
|
||||
def stop(self):
|
||||
self.pen = None
|
||||
SetWindowLong_WndProc_wrapper(self.hwnd, self.old_windProc)
|
||||
|
||||
MotionEventFactory.register('wm_pen', WM_PenProvider)
|
||||
157
kivy/input/providers/wm_touch.py
Normal file
157
kivy/input/providers/wm_touch.py
Normal file
@@ -0,0 +1,157 @@
|
||||
'''
|
||||
Support for WM_TOUCH messages (Windows platform)
|
||||
================================================
|
||||
'''
|
||||
|
||||
__all__ = ('WM_MotionEventProvider', 'WM_MotionEvent')
|
||||
|
||||
import os
|
||||
from kivy.input.providers.wm_common import WNDPROC, \
|
||||
SetWindowLong_WndProc_wrapper, RECT, POINT, WM_TABLET_QUERYSYSTEMGESTURE, \
|
||||
QUERYSYSTEMGESTURE_WNDPROC, WM_TOUCH, WM_MOUSEMOVE, WM_MOUSELAST, \
|
||||
TOUCHINPUT, PEN_OR_TOUCH_MASK, PEN_OR_TOUCH_SIGNATURE, PEN_EVENT_TOUCH_MASK
|
||||
from kivy.input.motionevent import MotionEvent
|
||||
from kivy.input.shape import ShapeRect
|
||||
|
||||
Window = None
|
||||
|
||||
|
||||
class WM_MotionEvent(MotionEvent):
|
||||
'''MotionEvent representing the WM_MotionEvent event.
|
||||
Supports pos, shape and size profiles.
|
||||
'''
|
||||
__attrs__ = ('size', )
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault('is_touch', True)
|
||||
kwargs.setdefault('type_id', 'touch')
|
||||
super().__init__(*args, **kwargs)
|
||||
self.profile = ('pos', 'shape', 'size')
|
||||
|
||||
def depack(self, args):
|
||||
self.shape = ShapeRect()
|
||||
self.sx, self.sy = args[0], args[1]
|
||||
self.shape.width = args[2][0]
|
||||
self.shape.height = args[2][1]
|
||||
self.size = self.shape.width * self.shape.height
|
||||
super().depack(args)
|
||||
|
||||
def __str__(self):
|
||||
args = (self.id, self.uid, str(self.spos), self.device)
|
||||
return '<WMMotionEvent id:%d uid:%d pos:%s device:%s>' % args
|
||||
|
||||
|
||||
if 'KIVY_DOC' in os.environ:
|
||||
# documentation hack
|
||||
WM_MotionEventProvider = None
|
||||
|
||||
else:
|
||||
from ctypes.wintypes import HANDLE
|
||||
from ctypes import (windll, sizeof, byref)
|
||||
from collections import deque
|
||||
from kivy.input.provider import MotionEventProvider
|
||||
from kivy.input.factory import MotionEventFactory
|
||||
|
||||
class WM_MotionEventProvider(MotionEventProvider):
|
||||
|
||||
def start(self):
|
||||
global Window
|
||||
if not Window:
|
||||
from kivy.core.window import Window
|
||||
|
||||
self.touch_events = deque()
|
||||
self.touches = {}
|
||||
self.uid = 0
|
||||
|
||||
# get window handle, and register to receive WM_TOUCH messages
|
||||
self.hwnd = windll.user32.GetActiveWindow()
|
||||
windll.user32.RegisterTouchWindow(self.hwnd, 1)
|
||||
|
||||
# inject our own wndProc to handle messages
|
||||
# before window manager does
|
||||
self.new_windProc = WNDPROC(self._touch_wndProc)
|
||||
self.old_windProc = SetWindowLong_WndProc_wrapper(
|
||||
self.hwnd, self.new_windProc)
|
||||
|
||||
def update(self, dispatch_fn):
|
||||
c_rect = RECT()
|
||||
windll.user32.GetClientRect(self.hwnd, byref(c_rect))
|
||||
pt = POINT(x=0, y=0)
|
||||
windll.user32.ClientToScreen(self.hwnd, byref(pt))
|
||||
x_offset, y_offset = pt.x, pt.y
|
||||
usable_w, usable_h = float(c_rect.w), float(c_rect.h)
|
||||
|
||||
while True:
|
||||
try:
|
||||
t = self.touch_events.pop()
|
||||
except:
|
||||
break
|
||||
|
||||
# adjust x,y to window coordinates (0.0 to 1.0)
|
||||
x = (t.screen_x() - x_offset) / usable_w
|
||||
y = 1.0 - (t.screen_y() - y_offset) / usable_h
|
||||
|
||||
# actually dispatch input
|
||||
if t.event_type == 'begin':
|
||||
self.uid += 1
|
||||
self.touches[t.id] = WM_MotionEvent(
|
||||
self.device, self.uid, [x, y, t.size()])
|
||||
dispatch_fn('begin', self.touches[t.id])
|
||||
|
||||
if t.event_type == 'update' and t.id in self.touches:
|
||||
self.touches[t.id].move([x, y, t.size()])
|
||||
dispatch_fn('update', self.touches[t.id])
|
||||
|
||||
if t.event_type == 'end' and t.id in self.touches:
|
||||
touch = self.touches[t.id]
|
||||
touch.move([x, y, t.size()])
|
||||
touch.update_time_end()
|
||||
dispatch_fn('end', touch)
|
||||
del self.touches[t.id]
|
||||
|
||||
def stop(self):
|
||||
windll.user32.UnregisterTouchWindow(self.hwnd)
|
||||
self.new_windProc = SetWindowLong_WndProc_wrapper(
|
||||
self.hwnd, self.old_windProc)
|
||||
|
||||
# we inject this wndProc into our main window, to process
|
||||
# WM_TOUCH and mouse messages before the window manager does
|
||||
def _touch_wndProc(self, hwnd, msg, wParam, lParam):
|
||||
done = False
|
||||
if msg == WM_TABLET_QUERYSYSTEMGESTURE:
|
||||
return QUERYSYSTEMGESTURE_WNDPROC
|
||||
|
||||
if msg == WM_TOUCH:
|
||||
done = self._touch_handler(msg, wParam, lParam)
|
||||
|
||||
if msg >= WM_MOUSEMOVE and msg <= WM_MOUSELAST:
|
||||
done = self._mouse_handler(msg, wParam, lParam)
|
||||
|
||||
if not done:
|
||||
return windll.user32.CallWindowProcW(self.old_windProc,
|
||||
hwnd, msg, wParam,
|
||||
lParam)
|
||||
return 1
|
||||
|
||||
# this on pushes WM_TOUCH messages onto our event stack
|
||||
def _touch_handler(self, msg, wParam, lParam):
|
||||
touches = (TOUCHINPUT * wParam)()
|
||||
windll.user32.GetTouchInputInfo(HANDLE(lParam),
|
||||
wParam,
|
||||
touches,
|
||||
sizeof(TOUCHINPUT))
|
||||
for i in range(wParam):
|
||||
self.touch_events.appendleft(touches[i])
|
||||
windll.user32.CloseTouchInputHandle(HANDLE(lParam))
|
||||
return True
|
||||
|
||||
# filter fake mouse events, because touch and stylus
|
||||
# also make mouse events
|
||||
def _mouse_handler(self, msg, wparam, lParam):
|
||||
info = windll.user32.GetMessageExtraInfo()
|
||||
# its a touch or a pen
|
||||
if (info & PEN_OR_TOUCH_MASK) == PEN_OR_TOUCH_SIGNATURE:
|
||||
if info & PEN_EVENT_TOUCH_MASK:
|
||||
return True
|
||||
|
||||
MotionEventFactory.register('wm_touch', WM_MotionEventProvider)
|
||||
335
kivy/input/recorder.py
Normal file
335
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/input/shape.py
Normal file
27
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
|
||||
Reference in New Issue
Block a user