Source code for ae.kivy.behaviors

"""
ae.kivy.behaviors module
------------------------

this module provides the following behavior classes:

    * :class:`~ae.kivy.behaviors.HelpBehavior` extends and prepares any Kivy widget to show an
      individual help text for it.
    * :class:`~ae.kivy.behaviors.ModalBehavior` is a generic mix-in class that provides modal behavior to any container
      widget.
    * :class:`~ae.kivy.behaviors.SlideSelectBehavior`: quickly navigate in elliptically shaped sub-/menus, alternatively
      starting with a long touch, then slide to the menu item to select and release.
    * :class:`~ae.kivy.behaviors.TouchableBehavior`: extends toggle-/touch-behavior of
      :class:`~kivy.uix.behaviors.ButtonBehavior`.


help behavior mixin
^^^^^^^^^^^^^^^^^^^

to show an i18n translatable help text for a Kivy widget, create a subclass of the widget and add the
mixin-/behavior-class :class:`~ae.kivy.behavior.HelpBehavior`. the following example is attaching a help text to the
Kivy :class:`~kivy.uix.button.Button` widget::

    from kivy.uix.button import Button
    from ae.kivy.widgets import HelpBehavior

    class ButtonWithHelpText(HelpBehavior, Button):
        ...

alternatively, you can archive this via the definition of a new kv-lang rule, like shown underneath::

    <ButtonWithHelpText@HelpBehavior+Button>

.. note::
    to automatically lock and mark the widget you want to add help texts for, this mixin class has to be specified
    as the first inheriting class in the class or rule declaration.


modal behavior mixin
^^^^^^^^^^^^^^^^^^^^

to convert a container widget into a modal dialog, add the :class:`~ae.kivy.behaviors.ModalBehavior` mix-in class,
provided by this ae namespace portion.

the following code snippet demonstrates a typical implementation::

    class MyContainer(ModalBehavior, BoxLayout):
        def __init__(self, **kwargs):
            super().__init__(**kwargs)

        def open(self):
            self.activate_esc_key_close()
            self.activate_modal()

        def close(self):
            self.deactivate_esc_key_close()
            self.deactivate_modal()


calling the method :meth:`~ae.kivy.behaviors.ModalBehavior.activate_esc_key_close` in the `open` method of a container
class allows the user to close the popup by pressing the Escape key (or Back on Android). this optional feature can
be reverted by calling the :meth:`~ae.kivy.behaviors.ModalBehavior.deactivate_esc_key_close` method in your
`close` method.

to additionally activate the modal mode, call the method :meth:`~ae.kivy.behaviors.ModalBehavior.activate_modal`.
the modal mode can be deactivated by calling the :meth:`~ae.kivy.behaviors.ModalBehavior.deactivate_modal` method.

after activating the modal mode, most of the user interactions (like touches, or mouse and keyboard events) will be
consumed or filtered. therefore, it is recommended to also visually change the GUI while in the modal mode, which
has to be implemented by the mixing-in container widget.

.. hint::
    usage examples of the :class:`~ae.kivy.behaviors.ModalBehavior` mix-in are e.g., the classes
    :class:`~ae.kivy.tours.TourOverlay` and :class:`~ae.kivy.widgets.FlowPopup`.

"""
from functools import partial
from typing import Any, Callable, Optional, Self, Union

from kivy.animation import Animation                                                                    # type: ignore
from kivy.app import App                                                                                # type: ignore
from kivy.clock import Clock                                                                            # type: ignore
from kivy.core.window import Window                                                                     # type: ignore
from kivy.graphics import Ellipse                                                                       # type: ignore
from kivy.input import MotionEvent                                                                      # type: ignore
from kivy.properties import (                                                                           # type: ignore
    BooleanProperty, DictProperty, NumericProperty, ObjectProperty, StringProperty)
from kivy.uix.dropdown import DropDown                                                                  # type: ignore
from kivy.uix.widget import Widget                                                                      # type: ignore

from ae.system import stack_var                                                                         # type: ignore
from ae.gui.app import MainAppBase                                                                      # type: ignore
from ae.gui.utils import flow_action                                                                    # type: ignore
from ae.kivy_glsl import ShaderIdType                                                                   # type: ignore


TOUCH_VIBRATE_PATTERN = (0.0, 0.09, 0.09, 0.06, 0.03, 0.03)
""" very short/~0.3s vibrate pattern for button and toggler touch. """


[docs] def grab_touch(touch: MotionEvent, widget: Widget, exclusive: bool = False, main_app: Optional[MainAppBase] = None ) -> bool: """ temporal helper function to debug occasionally happening exclusive grab conflicts """ if not main_app: main_app = App.get_running_app().main_app wid_info = repr(widget) if widget != (caller := stack_var('self')): wid_info += f" via {caller=}" try: main_app.vpo(f"grab_touch: {exclusive=} {wid_info=} {touch=}") touch.grab(widget, exclusive=exclusive) return True except Exception as exception: # pylint: disable=broad-exception-caught main_app.po(f"grab_touch FAILED with {exception=} for {exclusive=} {wid_info=} {touch=}") return False
[docs] class HelpBehavior: # pylint: disable=too-few-public-methods """ behavior mixin class for widgets providing help texts. """ help_id = StringProperty() """ unique help id of the widget. The correct identification of each help-aware widget presuppose that the attribute :attr:`~HelpBehavior.help_id` has a unique value for each widget instance. This is done automatically for the widgets provided by the module :mod:`ae.kivy.widgets` by converting the app flow or app state of these widgets into a help id (see e.g. the implementation of the class :class:`~ae.kivy.widgets.FlowButton`). :attr:`help_id` is a :class:`~kivy.properties.StringProperty` and defaults to an empty string. """ help_lock = BooleanProperty(False) """ this property is True if the help mode is active and this widget is not the help target. :attr:`help_lock` is a :class:`~kivy.properties.BooleanProperty` and defaults to the value `False`. """ help_vars = DictProperty() """ dict of extra data to displayed/render the help text of this widget. The :attr:`~HelpBehavior.help_vars` is a dict which can be used to provide extra context data to dynamically generate, translate and display individual help texts. :attr:`help_vars` is a :class:`~kivy.properties.DictProperty` and defaults to an empty dict. """ _shader_args = ObjectProperty() #: shader internal data / id # abstract attributes and methods provided by the class to be mixed into collide_point: Callable
[docs] def on_touch_down(self, touch: MotionEvent) -> bool: # pragma: no cover """ prevent any processing if touch is done on the help activator widget or in active help mode. :param touch: motion/touch event data. :return: a boolean True value if the event got processed/used, else False. """ main_app = App.get_running_app().main_app if main_app.help_activator.collide_point(*touch.pos): return False # allow the help activator button to process this touch-down event if self.help_lock and self.collide_point(*touch.pos) and main_app.help_display(self.help_id, self.help_vars): return True # main_app.help_layout is not None return super().on_touch_down(touch) # type: ignore # pylint: disable=no-member
[docs] class ModalBehavior: # pragma: no cover """ mix-in to allow close on press of the Escape/Back key, to optionally provide a modal mode to a container widget. to make the container widget's modal state more obvious, add in your container widget an overlay color with an alpha between 0.3 and 0.9, together with the following canvas instructions: canvas: Color: rgba: root.my_overlay_color[:3] + [root.my_overlay_color[3] if self.is_modal else 0] Rectangle: size: Window.size if self.is_modal else (0, 0) two rectangles will be needed to not overlay/fade-out the help activator button:: canvas: Color: rgba: self.my_overlay_color[:3] + [self.my_overlay_color[-1] if self.is_modal else 0] Rectangle: size: Window.width if self.is_modal else 0, \ Window.height - app.main_app.help_activator.height if self.is_modal else 0 Rectangle: pos: app.main_app.help_activator.right, app.main_app.help_activator.y size: Window.width - app.main_app.help_activator.width if self.is_modal else 0, \ app.main_app.help_activator.height """ # abstracts provided by Kivy's :class:`~kivy.uix.widget.Widget` class or by the mixing-in container widget class. center: list #: center position of :class:`~kivy.uix.widget.Widget` close: Callable #: method to dismiss the container widget (provided by self/container-widget) collide_point: Callable #: method to detect collisions with other widgets of :class:`~kivy.uix.widget.Widget` disabled: bool #: disabled property of :class:`~kivy.uix.widget.Widget` fbind: Callable #: fast binding method of :class:`~kivy.uix.widget.Widget` funbind: Callable #: fast unbinding method of :class:`~kivy.uix.widget.Widget` unbind_uid: Callable #: the faster unbinding method of :class:`~kivy.uix.widget.Widget` auto_dismiss = BooleanProperty() """ determines if the container is automatically dismissed when the user hits the Esc/Back key or clicks outside it. :attr:`auto_dismiss` is a :class:`~kivy.properties.BooleanProperty` and defaults to True. """ is_modal = BooleanProperty(defaultvalue=False) """ flag if modal mode is active. use :meth:`.activate_modal` and :meth:`.deactivate_modal` to change this value. :attr:`is_modal` is a :class:`~kivy.properties.BooleanProperty` and defaults to False. """ _center_aligned: bool = False #: True if self will be repositioned to the Window center _fast_bound_center_uid: int = 0 #: fbind/unbind_uid of the center property (pos and size) _touch_started_inside: Optional[bool] = None #: flag if touch started inside this widget or group
[docs] def _align_center(self, *_args): """ reposition the container to the center of the app window. :param _args: unused (passed only on bound window resize events) """ if self._center_aligned and self.is_modal: self.center = Window.center
[docs] def _on_key_down(self, _window, key, _scancode, _codepoint, _modifiers) -> Optional[bool]: """ close/dismiss this popup if back/Esc key get pressed - allowing stacking with DropDown/FlowDropDown. """ if key == 27 and self.auto_dismiss and self.is_modal: if not App.get_running_app().tour_layout: # prevent close/dismiss by the Esc key if an app tour is active self.close() return True return None
[docs] def activate_esc_key_close(self): """ activate the key press handler, calling self.close() if Escape/Back key get pressed. """ Window.bind(on_key_down=self._on_key_down)
[docs] def activate_modal(self, align_center: bool = True): """ activate or renew modal mode for the mixing-in container widget. :param align_center: pass False to prevent the automatic alignment of :attr:`~kivy.uix.widget.Widget.center` to :attr:`~kivy.core.window.Window.center` on reposition or resize of self or on resize of :class:`~kivy.core.window.Window`. """ self.deactivate_modal() Window.add_widget(self) if align_center: Window.bind(on_resize=self._align_center) self._center_aligned = align_center # binding center includes a notification event on change of :attr:`~kivy.uix.widget.Widget.pos` and `size` self._fast_bound_center_uid = self.fbind('center', self._align_center) self.is_modal = True
[docs] def deactivate_esc_key_close(self): """ deactivate the keyboard event handler, activated via :meth:`.activate_esc_key_close`. """ Window.unbind(on_key_down=self._on_key_down)
[docs] def deactivate_modal(self): """ deactivate modal mode for the mixing-in container. """ if self._fast_bound_center_uid: self.unbind_uid('center', self._fast_bound_center_uid) Window.unbind(on_resize=self._align_center) self._fast_bound_center_uid = 0 if self._center_aligned: Window.unbind(on_resize=self._align_center) self._center_aligned = False if self.is_modal: Window.remove_widget(self) self.is_modal = False
[docs] def on_touch_down(self, touch: MotionEvent) -> bool: """ a touch-down event handler prevents the processing of a touch on the help activator widget by this popup. :param touch: motion/touch event data. :return: a boolean True value if the event got processed/used, else False. """ self._touch_started_inside = self.touch_pos_is_inside(touch.pos) if App.get_running_app().main_app.help_activator.collide_point(*touch.pos): return False # allow the help activator button to process this touch-down event # .. and leave self._touch_started_inside == None to not initiate popup.close/dismiss in on_touch_up if self.disabled if self._touch_started_inside else self.auto_dismiss: return self.is_modal return super().on_touch_down(touch) # type: ignore # pylint: disable=no-member
[docs] def on_touch_move(self, touch: MotionEvent) -> bool: """ touch move event handler. """ if self.disabled if self._touch_started_inside else self.auto_dismiss: return self.is_modal # noinspection PyUnresolvedReferences return super().on_touch_move(touch) # type: ignore # pylint: disable=no-member
[docs] def on_touch_up(self, touch: MotionEvent) -> bool: """ touch up event handler. """ if self.auto_dismiss and self._touch_started_inside is False: self.close(touch) ret = True else: # noinspection PyUnresolvedReferences ret = super().on_touch_up(touch) # type: ignore # pylint: disable=no-member self._touch_started_inside = None return ret
[docs] def touch_pos_is_inside(self, pos: list[float]) -> bool: """ check if the touch pos is inside this widget or a group of sub-widgets. :param pos: touch position (x, y) in window coordinates. :return: a boolean value True if this widget or group processes a touch event at the touch position specified in the :paramref:`~touch_pos_is_inside.pos` argument. """ return self.collide_point(*pos)
[docs] class SlideSelectBehavior: # pragma: no cover """ quickly navigate in sub-/menus, starting with a long touch, then slide to the menu item to select and release. the slide-select feature of this class allows quicker selections of any menu item, by opening any popup via the :meth:`~ae.kivy.behaviors.TouchableBehavior.on_long_tap` event, then move the pointer/finger onto the menu item to select to finally release the touch. to enable this feature, specify the touch event in the `touch_event` key of the `popup_kwargs` dict in the :meth:`~ae.gui.app.MainAppBase.change_flow` call, e.g., by adding the following lines in your kv code onto the :class:`~ae.kivy.widgets.FlowButton`/:class:`~ae.kivy.widgets.FlowToggler` that is opening the popup:: on_long_tap: app.main_app.change_flow(id_of_flow('open', 'my_menu'), **update_tap_kwargs(self, popup_kwargs=dict(touch_event=args[1]))) .. note:: has to be inherited (to be in the MRO) before :class:`~kivy.uix.behaviors.ButtonBehavior`, respectively :class:`~kivy.uix.behaviors.ToggleButtonBehavior`, for the touch event gets grabbed properly. """ # abstracts of mixing-in class; e.g., from :class:`~kivy.widget.Widget`, :class:`~ae.kivy_glsl.ShadersMixin`, # :class:`~kivy.uix.dropdown.DropDown` and :class:`~kivy.uix.behaviors.ButtonBehavior`. attach_to: Optional[Widget] close: Callable collide_point: Callable dispatch: Callable to_widget: Callable
[docs] def __init__(self, **kwargs): """ set normal pressed state shader on widget initialization. """ self._layout_finished: bool = True self._opened_item: Optional[Widget] = None self._touch_moved_outside: bool = False self.main_app = App.get_running_app().main_app # noinspection PyUnresolvedReferences super().__init__(**kwargs)
[docs] @staticmethod def _cancel_slide_select_closer(touch): slide_select_closer = touch.ud.pop('slide_select_closer', None) if slide_select_closer: Clock.unschedule(slide_select_closer) # alternatively: slide_select_closer.cancel()
[docs] @staticmethod def _cancel_slide_select_opener(touch): slide_select_opener = touch.ud.pop('slide_select_opener', None) if slide_select_opener: Clock.unschedule(slide_select_opener) # alternatively: slide_select_opener.cancel()
[docs] def _grab_and_open(self, touch: MotionEvent, item: Widget, first_close: Widget, *_args): if first_close: # moved over another menu item of the parent menu then close touch.ungrab(first_close) first_close.close() # the foremost submenu and open the sibling submenu instead if not self.main_app.change_flow(item.tap_flow_id, **item.tap_kwargs): return self._opened_item = item sub_menu = Window.children[0] # the submenu just opened above via change_flow # touch.grab(sub_menu) grab_touch(touch, sub_menu, main_app=self.main_app) # allow dispatching of :meth:`ModalBehavior.on_touch_move` events for slide_select sub_menu._touch_started_inside = True # pylint: disable=protected-access
[docs] @staticmethod def _ungrab_and_close(touch: MotionEvent, popup: Union[Widget, 'SlideSelectBehavior'], *_args): touch.ungrab(popup) # noinspection PyProtectedMember Window.children[1]._opened_item = None # pylint: disable=protected-access popup.close()
[docs] def on_touch_move(self, touch: MotionEvent) -> bool: # pylint: disable=too-many-locals """ disable long touch on mouse/finger moves. :param touch: motion/touch event data. :return: a boolean True value if the event got processed/used. """ is_dropdown = isinstance(self, DropDown) opener: Widget | Self | None = self.attach_to if is_dropdown else self in_opener = opener and opener.collide_point(*touch.pos) if opener and not in_opener: opener._touch_moved_outside = True # pylint: disable=protected-access # slide_select of menu-items/children of :class:`FlowDropDown`, :class:`FlowSelector` and :class:`FlowPopup` self._cancel_slide_select_closer(touch) self._cancel_slide_select_opener(touch) mnu_items = getattr(self, 'menu_items', None) if mnu_items and self._layout_finished: win_chi = Window.children[:2] foremost_popup = self is win_chi[0] # pylint: disable=protected-access if foremost_popup and in_opener and opener._touch_moved_outside: # type: ignore touch.ud['slide_select_closer'] = slide_select_closer = partial(self._ungrab_and_close, touch, self) Clock.schedule_once(slide_select_closer, 0.69) if self in win_chi: wid_pos = self.to_widget(*touch.pos) col_items = [item for item in mnu_items # pylint: disable=not-an-iterable if item != self._opened_item and item.collide_point(*wid_pos) and flow_action(getattr(item, 'tap_flow_id', "")) == 'open'] if len(col_items) == 1: # single non-overlapping item found touch.ud['slide_select_opener'] = slide_select_opener = partial( self._grab_and_open, touch, col_items[0], None if foremost_popup else win_chi[0]) Clock.schedule_once(slide_select_opener, 0.39) # return True # returning True prevents touch-initiated scrolls, e.g., in UserPrefs Colors dropdown elif foremost_popup: widgets = mnu_items + [self.attach_to if is_dropdown else self] min_x, min_y, width, height = self.main_app.widgets_enclosing_rectangle(widgets) if not (min_x <= touch.x <= min_x + width and min_y <= touch.y <= min_y + height): self._ungrab_and_close(touch, self) return True # noinspection PyUnresolvedReferences return super().on_touch_move(touch) # type: ignore # pylint: disable=no-member
[docs] def on_touch_up(self, touch: MotionEvent) -> bool: """ disable long touch on mouse/finger up. :param touch: motion/touch event data. :return: boolean True value if the event got processed/used. """ self._cancel_slide_select_closer(touch) self._cancel_slide_select_opener(touch) self._opened_item = None if touch.ud.pop('is_long_tap', False): # pylint: disable=too-many-nested-blocks items = getattr(self, 'menu_items', None) if items and self._layout_finished and self == Window.children[0]: for item in items: # pylint: disable=not-an-iterable if item.collide_point(*item.to_widget(*touch.pos)): # slide_select touch released on a menu item if hasattr(item, 'on_release'): if item not in touch.ud: # prevent multiple dispatch of on_release item.dispatch('on_release') return True elif hasattr(item, 'focus'): item.unfocus_on_touch = False item.focus = True return True elif hasattr(item, 'value_pos'): item.value_pos = touch.pos return True else: break # noinspection PyUnresolvedReferences return super().on_touch_up(touch) # type: ignore # pylint: disable=no-member # does touch.ungrab(self)
[docs] class TouchableBehavior: # pragma: no cover """ touch-/toggle-button mix-in class with shaders, animations and additional events for double/triple/long touches. :Events: `on_double_tap`: fired with the touch-down MotionEvent instance arg when a button gets tapped twice within a short time. `on_triple_tap`: fired with the touch-down MotionEvent instance arg when a button gets tapped three times within a short time. `on_long_tap`: fired with the touch-down MotionEvent instance arg when a button gets tapped more than 2.4 seconds. `on_alt_tap`: fired with the touch-down MotionEvent instance arg when a button gets either double, triple or long tapped. .. note:: has to be inherited (to be in the MRO) before the class :class:`~kivy.uix.behaviors.ButtonBehavior`, respectively :class:`~kivy.uix.behaviors.ToggleButtonBehavior`, for the touch event gets grabbed properly. """ # abstracts of mixing-in class; e.g., from :class:`~kivy.widget.Widget`, :class:`~ae.kivy_glsl.ShadersMixin`, # :class:`~ae.kivy.behaviors.SlideSelectBehavior`, and :class:`~kivy.uix.behaviors.ButtonBehavior` add_shader: Callable center_x: float center_y: float collide_point: Callable del_shader: Callable disabled: bool dispatch: Callable state: str # Kivy properties and events down_shader = DictProperty(allownone=True) """ shader running if button is in pressed state `'down'`. :attr:`down_shader` is a :class:`~kivy.properties.DictProperty` and defaults to the :data:`firestorm shader <ae.kivy_glsl.FIRE_STORM_SHADER_CODE>`. set to `None` to not render the default shader on button press/down. """ normal_shader = DictProperty(allownone=True) """ shader running if button is in pressed state `'normal'`. :attr:`normal_shader` is a :class:`~kivy.properties.DictProperty` and defaults to the :data:`plunge wave shader <ae.kivy_glsl.PLUNGE_WAVES_SHADER_CODE>`. set to `None` to not render a shader on button release/up. """ _touch_anim = NumericProperty(1.0) #: widget-got-touched-animation _touch_x = NumericProperty() #: x pos moving in touch animation from initial touch to center pos _touch_y = NumericProperty() #: y pos moving in touch animation from initial touch to center pos __events__ = ('on_alt_tap', 'on_double_tap', 'on_long_tap', 'on_triple_tap')
[docs] def __init__(self, **kwargs): """ set normal pressed state shader on widget initialization. """ self._state_shader_id: ShaderIdType = {} # noinspection PyUnresolvedReferences super().__init__(**kwargs) main_app = App.get_running_app().main_app if self.down_shader is not None: self.down_shader = {'shader_code': '=fire_storm', 'render_shape': Ellipse, 'tint_ink': main_app.flow_path_ink} if self.normal_shader is not None: self.normal_shader = {'shader_code': '=plunge_waves', 'render_shape': Ellipse, 'add_to': 'before', 'alpha': 0.36, 'contrast': 0.09, 'tex_col_mix': 0.87, 'time': lambda: -Clock.get_boottime(), 'tint_ink': main_app.flow_id_ink}
[docs] @staticmethod def _cancel_long_touch_clock(touch: MotionEvent) -> bool: long_touch_handler = touch.ud.pop('long_touch_handler', None) if long_touch_handler: Clock.unschedule(long_touch_handler) # alternatively: long_touch_handler.cancel() return bool(long_touch_handler)
[docs] def on_alt_tap(self, touch: MotionEvent): """ default handler for alternative tap (double, triple or long tap/click). :param touch: motion/touch event data with the touched widget in `touch.grab_current`. """
[docs] def on_double_tap(self, touch: MotionEvent): """ default event handler for double tap/click events. :param touch: motion/touch event data with the touched widget in `touch.grab_current`. """
[docs] def on_down_shader(self, *_args): """ event handler called when button down state shader changed. """ self._update_shader()
[docs] def on_long_tap(self, touch: MotionEvent): """ long tap/click default handler. :param touch: motion/touch event data with the touched widget in `touch.grab_current`. """ # remove 'long_touch_handler' key from touch.ud dict although just fired to signalize that # the long tap event got handled in self.on_touch_up (to return True) self._cancel_long_touch_clock(touch) touch.ud['is_long_tap'] = True # reset the button state to normal - if the state is still down (to replace down_shader with normal_shader) if self.state == 'down': self.state = 'normal' # to prevent dismiss via super().on_touch_up: exclusive receive of this touch-up event in self.on_touch_up # later uncommented again, because long tap dropdowns did not stay open and selected menu item on slide to it # touch.grab(self) # without exclusive submenu gets selected on long touch release of dropdown-opening button # if touch.grab_exclusive_class is None: # check if already grabbed, to prevent exception in touch.grab() call # touch.grab(self, exclusive=True) # was commented because already grabbed/exclusive prevents slide_select if not grab_touch(touch, self, exclusive=True): grab_touch(touch, self) # also dispatch as an alternative tap self.dispatch('on_alt_tap', touch)
[docs] def on_normal_shader(self, *_args): """ button normal state shader changed event handler. """ self._update_shader()
[docs] def on_state(self, _widget: Any, _value: str): """ the button press-state-changed event handler, switching between `'normal'` and `'down'` state shader. :param _widget: button widget (is self). :param _value: new state value (either 'normal' or 'down'). """ self._update_shader()
[docs] def on_touch_down(self, touch: MotionEvent) -> bool: """ add sound, vibration and animation, check for a running tour and additional double/triple/alt touch events. :param touch: motion/touch event data. :return: a boolean True if the event got processed/used. """ if not self.disabled and self.collide_point(touch.x, touch.y): main_app = App.get_running_app().main_app if main_app.tour_layout and self is not main_app.help_activator: return True # suppress on_release event if the app tour is running (except for the activator button) self._touch_anim = 0.0 self._touch_x, self._touch_y = touch.pos Animation(_touch_anim=1.0, _touch_x=self.center_x, _touch_y=self.center_y, t='out_quad', d=0.69).start(self) is_triple = touch.is_triple_tap if is_triple or touch.is_double_tap: # pylint: disable=maybe-no-member self.dispatch('on_triple_tap' if is_triple else 'on_double_tap', touch) self.dispatch('on_alt_tap', touch) return True # pylint: disable=maybe-no-member touch.ud['long_touch_handler'] = long_touch_handler = lambda dt: self.dispatch('on_long_tap', touch) Clock.schedule_once(long_touch_handler, 0.99) main_app.play_vibrate(TOUCH_VIBRATE_PATTERN) main_app.play_sound('touched') # noinspection PyUnresolvedReferences return super().on_touch_down(touch) # type: ignore # pylint: disable=no-member # does touch.grab(self)
[docs] def on_touch_move(self, touch: MotionEvent) -> bool: """ disable long touch on mouse/finger moves. :param touch: motion/touch event data. :return: a boolean True if the event got processed/used. """ # if moved, then cancel long touch detection, an alternative method to calc touch.pos distances is: # Vector.distance(Vector(ref.sx, ref.sy), Vector(touch.osx, touch.osy)) > 0.009 if abs(touch.ox - touch.x) > 9 and abs(touch.oy - touch.y) > 9: self._cancel_long_touch_clock(touch) # noinspection PyUnresolvedReferences return super().on_touch_move(touch) # type: ignore # pylint: disable=no-member
[docs] def on_touch_up(self, touch: MotionEvent) -> bool: """ disable long touch on mouse/finger up. :param touch: motion/touch event data. :return: True if the event got processed/used. """ if touch.grab_current is self: touch.ungrab(self) # cancel the long touch clock callback (if still running respectively if not on_long_tap) if not self._cancel_long_touch_clock(touch): return True # prevent popup/dropdown dismiss # noinspection PyUnresolvedReferences return super().on_touch_up(touch) # type: ignore # pylint: disable=no-member # does touch.ungrab(self)
[docs] def on_triple_tap(self, touch: MotionEvent): """ the triple tap/click default handler. :param touch: motion/touch event data with the touched widget in `touch.grab_current`. """
[docs] def _update_shader(self): """ update shader on changed shader or button state. """ if self._state_shader_id: self.del_shader(self._state_shader_id) self._state_shader_id = {} add_shader_kwargs = self.down_shader if self.state == 'down' else self.normal_shader if add_shader_kwargs: self._state_shader_id = self.add_shader(**add_shader_kwargs)