first commit

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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