test-kivy-app/kivy_venv/lib/python3.11/site-packages/asynckivy/_touch.py

197 lines
7.5 KiB
Python
Raw Normal View History

2024-09-15 17:57:02 +00:00
__all__ = ('watch_touch', 'rest_of_touch_moves', 'rest_of_touch_events', 'touch_up_event', )
import typing as T
import types
from functools import partial
from asyncgui import wait_any, current_task
from ._exceptions import MotionEventAlreadyEndedError
from ._sleep import sleep
from ._event import event
class watch_touch:
'''
Returns an async context manager that provides an easy way to handle touch events.
.. code-block::
async with watch_touch(widget, touch) as in_progress:
while await in_progress():
print('on_touch_move')
print('on_touch_up')
The ``await in_progress()`` waits for either an ``on_touch_move`` event or an ``on_touch_up`` event to occur, and
returns True or False respectively when they occurred.
**Caution**
* You are not allowed to perform any kind of async operations inside the with-block except ``await in_progress()``.
.. code-block::
async with watch_touch(widget, touch) as in_progress:
await in_progress() # OK
await something_else # NOT ALLOWED
async with async_context_manager: # NOT ALLOWED
...
async for __ in async_iterator: # NOT ALLOWED
...
* If the ``widget`` is the type of widget that grabs touches by itself, such as :class:`kivy.uix.button.Button`,
you probably want to set the ``stop_dispatching`` parameter to True in most cases.
* There are widgets/behaviors that can simulate a touch (e.g. :class:`kivy.uix.scrollview.ScrollView`,
:class:`kivy.uix.behaviors.DragBehavior` and ``kivy_garden.draggable.KXDraggableBehavior``).
If many such widgets are in the parent stack of the ``widget``, this API might mistakenly raise a
:exc:`asynckivy.MotionEventAlreadyEndedError`. If that happens, increase the ``timeout`` parameter.
'''
__slots__ = ('_widget', '_touch', '_stop_dispatching', '_timeout', '_uid_up', '_uid_move', '_no_cleanup', )
def __init__(self, widget, touch, *, stop_dispatching=False, timeout=1.):
self._widget = widget
self._touch = touch
self._stop_dispatching = stop_dispatching
self._timeout = timeout
self._no_cleanup = False
def _on_touch_up_sd(step, touch, w, t):
if t is touch:
if t.grab_current is w:
t.ungrab(w)
step(False)
return True
def _on_touch_move_sd(step, touch, w, t):
if t is touch:
if t.grab_current is w:
step(True)
return True
def _on_touch_up(step, touch, w, t):
if t.grab_current is w and t is touch:
t.ungrab(w)
step(False)
return True
def _on_touch_move(step, touch, w, t):
if t.grab_current is w and t is touch:
step(True)
return True
_callbacks = ((_on_touch_up_sd, _on_touch_move_sd, ), (_on_touch_up, _on_touch_move, ), )
del _on_touch_up, _on_touch_move, _on_touch_up_sd, _on_touch_move_sd
@staticmethod
@types.coroutine
def _true_if_touch_move_false_if_touch_up() -> bool:
return (yield lambda step_coro: None)[0][0]
@staticmethod
@types.coroutine
def _always_false() -> bool:
return False
yield # just to make this function a generator function
async def __aenter__(self) -> T.Awaitable[T.Callable[[], T.Awaitable[bool]]]:
touch = self._touch
widget = self._widget
if touch.time_end != -1:
# `on_touch_up` might have been already fired so we need to find out it actually was or not.
tasks = await wait_any(
sleep(self._timeout),
event(widget, 'on_touch_up', filter=lambda w, t: t is touch),
)
if tasks[0].finished:
raise MotionEventAlreadyEndedError(f"MotionEvent(uid={touch.uid}) has already ended")
self._no_cleanup = True
return self._always_false
step = (await current_task())._step
on_touch_up, on_touch_move = self._callbacks[not self._stop_dispatching]
touch.grab(widget)
self._uid_up = widget.fbind('on_touch_up', partial(on_touch_up, step, touch))
self._uid_move = widget.fbind('on_touch_move', partial(on_touch_move, step, touch))
assert self._uid_up
assert self._uid_move
return self._true_if_touch_move_false_if_touch_up
async def __aexit__(self, *args):
if self._no_cleanup:
return
w = self._widget
self._touch.ungrab(w)
w.unbind_uid('on_touch_up', self._uid_up)
w.unbind_uid('on_touch_move', self._uid_move)
async def touch_up_event(widget, touch, *, stop_dispatching=False, timeout=1.) -> T.Awaitable:
'''
*(experimental state)*
Returns an awaitable that can be used to wait for the ``on_touch_up`` event of the given ``touch`` to occur.
.. code-block::
__, touch = await event(widget, 'on_touch_down')
...
await touch_up_event(widget, touch)
You might wonder what the differences are compared to the code below.
.. code-block::
:emphasize-lines: 3
__, touch = await event(widget, 'on_touch_down')
...
await event(widget, 'on_touch_up', filter=lambda w, t: t is touch)
The latter has two problems:
If the ``on_touch_up`` event of the ``touch`` occurred before the highlighted line,
the execution will halt indefinitely at that point.
Even if the event didn't occur before that line, the execution still might halt because
`Kivy does not guarantee`_ that all touch events are delivered to all widgets.
This API takes care of both problems in the same way as :func:`watch_touch`.
If the ``on_touch_up`` event has already occurred, it raises a :exc:`MotionEventAlreadyEndedError` exception.
And it grabs/ungrabs the ``touch`` so that it won't miss any touch events.
Needless to say, if you want to wait for both ``on_touch_move`` and ``on_touch_up`` events at the same time,
use :func:`watch_touch` or :func:`rest_of_touch_events` instead.
.. _Kivy does not guarantee: https://kivy.org/doc/stable/guide/inputs.html#grabbing-touch-events
'''
touch.grab(widget)
try:
awaitable = event(
widget, 'on_touch_up', stop_dispatching=stop_dispatching,
filter=lambda w, t: t.grab_current is w and t is touch,
)
if touch.time_end == -1:
await awaitable
else:
tasks = await wait_any(sleep(timeout), awaitable)
if tasks[0].finished:
raise MotionEventAlreadyEndedError(f"MotionEvent(uid={touch.uid}) has already ended")
finally:
touch.ungrab(widget)
async def rest_of_touch_events(widget, touch, *, stop_dispatching=False, timeout=1.) -> T.AsyncIterator[None]:
'''
Returns an async iterator that iterates the number of times ``on_touch_move`` occurs,
and ends the iteration when ``on_touch_up`` occurs.
.. code-block::
async for __ in rest_of_touch_events(widget, touch):
print('on_touch_move')
print('on_touch_up')
This is a wrapper for :class:`watch_touch`. Although this one, I believe, is more intuitive than
:class:`watch_touch`, it has a couple of disadvantages - see :ref:`the-problem-with-async-generators`.
'''
async with watch_touch(widget, touch, stop_dispatching=stop_dispatching, timeout=timeout) as in_progress:
while await in_progress():
yield
rest_of_touch_moves = rest_of_touch_events