Version

Quick search

Table Of Contents

Source code for kivy.uix.behaviors.motion

"""
Motion Event Behaviors
======================

The :class:`~kivy.uix.behaviors.motion.MotionCollideBehavior` and
:class:`~kivy.uix.behaviors.motion.MotionBlockBehavior`
`mixin <https://en.wikipedia.org/wiki/Mixin>`_ classes provide generic
motion event filtering for any motion event type (hover, drag, custom events).

These behaviors are commonly used with :class:`~kivy.uix.behaviors.hover.HoverBehavior`
but are designed to be generic and reusable with any motion event system.

For an overview of behaviors, please refer to the :mod:`~kivy.uix.behaviors`
documentation.

Components
----------

**MotionCollideBehavior**:
    Filters motion events to only process those within widget bounds or grabbed.
    Essential for :class:`~kivy.uix.recycleview.RecycleView` and
    :class:`~kivy.uix.scrollview.ScrollView` to prevent conflicts outside
    visible area.

**MotionBlockBehavior**:
    Blocks unregistered motion events from passing through widget bounds.
    Makes widgets "opaque" to motion events, preventing leak-through to
    background widgets.

Examples
--------

Using MotionCollideBehavior with HoverBehavior::

    from kivy.uix.scrollview import ScrollView
    from kivy.uix.label import Label
    from kivy.uix.behaviors.hover import HoverBehavior
    from kivy.uix.behaviors.motion import MotionCollideBehavior

    class HoverScrollView(MotionCollideBehavior, ScrollView):
        # Filters hover events outside stencil bounds
        pass

    class HoverItem(HoverBehavior, MotionCollideBehavior, Label):
        def on_enter(self):
            self.color = [1, 0, 0, 1]  # Red on hover

        def on_leave(self):
            self.color = [1, 1, 1, 1]  # White normally

Using MotionBlockBehavior to prevent event leak-through::

    from kivy.uix.button import Button
    from kivy.uix.floatlayout import FloatLayout
    from motion_behaviors import MotionBlockBehavior

    class OpaqueButton(MotionBlockBehavior, Button):
        # Blocks unregistered motion events from passing through
        pass

    layout = FloatLayout()
    layout.add_widget(HoverScrollView())  # Background
    layout.add_widget(OpaqueButton())     # Foreground
    # Hovering over button won't trigger ScrollView hover

Important Notes
---------------

**DO NOT** use :class:`~kivy.uix.behaviors.motion.MotionCollideBehavior` and
:class:`~kivy.uix.behaviors.motion.MotionBlockBehavior` together on the same
widget. They serve different purposes:

- **MotionCollideBehavior**: Filters events outside widget bounds
    * Use for: Items in RecycleView/ScrollView
    * Purpose: Ignore events when widget is outside visible area

- **MotionBlockBehavior**: Blocks unregistered event types from passing through
    * Use for: Overlay widgets, modal dialogs, foreground elements
    * Purpose: Prevent events from leaking to widgets behind

Combining them creates redundant or conflicting logic.

See :class:`~kivy.uix.behaviors.motion.MotionCollideBehavior` and
:class:`~kivy.uix.behaviors.motion.MotionBlockBehavior` for details.
"""

__all__ = ("MotionCollideBehavior", "MotionBlockBehavior")


[docs]class MotionCollideBehavior: """Mixin to filter motion events based on collision detection. This mixin overrides :meth:`on_motion` to only process events that either: 1. Collide with the widget's bounds, OR 2. Are grabbed events (motion_event.grab_current is self) Generic Design -------------- Works with ANY motion event type (hover, drag, custom events). Commonly used with :class:`~kivy.uix.behaviors.hover.HoverBehavior` but not limited to hover events. Primary Use Case - StencilView ------------------------------- Essential for widgets that use stencil clipping (:class:`~kivy.uix.recycleview.RecycleView`, :class:`~kivy.uix.scrollview.ScrollView`). Without this mixin, widgets outside the stencil bounds can still receive motion events, leading to confusing behavior where invisible items respond. Technical Note -------------- The collision check uses :meth:`widget.collide_point(*motion_event.pos)`, which tests against the widget's bounding box. For complex shapes, override :meth:`collide_point()` in your widget class for custom collision logic. Grabbed Events -------------- Grabbed events always pass through, even if they don't collide. This ensures proper event completion - if a widget grabbed an event (e.g., on hover enter), it must receive the end event even if the cursor moved outside bounds. """
[docs] def on_motion(self, event_type: str, motion_event) -> bool: """Filter motion events by collision or grab status. Only processes events that: - Are currently grabbed by this widget, OR - Have position data and collide with widget bounds :param event_type: Motion event type ("begin", "update", "end") :param motion_event: MotionEvent to potentially filter :return: Result of super().on_motion() if passed filter, False otherwise """ is_grabbed = motion_event.grab_current is self has_position = "pos" in motion_event.profile collides = has_position and self.collide_point(*motion_event.pos) # Process only grabbed events or events within bounds if is_grabbed or collides: return super().on_motion(event_type, motion_event) # Filter out non-qualifying events return False
[docs]class MotionBlockBehavior: """Mixin to block unregistered motion events from passing through. Makes widgets "opaque" to motion events they haven't registered to handle. When a motion event collides with the widget but the widget hasn't registered for that event type, the event is blocked from propagating to widgets behind it. Generic Design -------------- Works with ANY motion event type (hover, drag, custom events). Prevents motion events from "leaking through" foreground widgets to background widgets. Comparison with HoverBehavior ------------------------------ - **MotionBlockBehavior**: Blocks ALL unregistered motion event types - **HoverBehavior** (hover_mode='self'): Only blocks hover events with more fine-grained control For hover-specific blocking with more control, consider using :class:`~kivy.uix.behaviors.hover.HoverBehavior` with hover_mode='self' instead. Technical Note -------------- The collision check uses :meth:`widget.collide_point(*motion_event.pos)`. For custom collision shapes, override :meth:`collide_point()` in your widget. Grabbed Events -------------- Grabbed events always pass through, even if not registered. This ensures proper event lifecycle - if a widget grabs an event, it must receive all subsequent updates and the end event. """
[docs] def on_motion(self, event_type: str, motion_event) -> bool: """Block unregistered motion events that collide with widget. Processing logic: 1. If grabbed by this widget -> pass to super() (process) 2. If collides but NOT registered -> return True (block) 3. Otherwise -> pass to super() (continue propagation) :param event_type: Motion event type ("begin", "update", "end") :param motion_event: MotionEvent to potentially block :return: True if event was blocked or handled, False otherwise """ # Grabbed events always pass through if motion_event.grab_current is self: return super().on_motion(event_type, motion_event) # Check collision and registration has_position = "pos" in motion_event.profile collides = has_position and self.collide_point(*motion_event.pos) is_registered = motion_event.type_id in self.motion_filter # Block if collides but not registered if collides and not is_registered: return True # Pass through to normal processing return super().on_motion(event_type, motion_event)