Table Of Contents
Source code for kivy.uix.behaviors.button
"""
Button Behavior
===============
The :class:`~kivy.uix.behaviors.button.ButtonBehavior`
`mixin <https://en.wikipedia.org/wiki/Mixin>`_ class provides button behavior
for Kivy widgets. You can combine this class with other widgets to add
specialized press/release/cancel events and reactive pressed state tracking.
For an overview of behaviors, please refer to the :mod:`~kivy.uix.behaviors`
documentation.
Quick overview
--------------
+-----------------------------------------------------------------------------------------+
| BUTTON BEHAVIOR |
+---------------------------------------+-----------+-------------------------------------+
| STATE | TYPE | DESCRIPTION |
+=======================================+===========+=====================================+
| `pressed` | read-only | Is button currently pressed? |
+---------------------------------------+-----------+-------------------------------------+
| `always_release` | read/write| Always fire on_release? |
+---------------------------------------+-----------+-------------------------------------+
| **EVENTS** |
+---------------------------------------+-----------+-------------------------------------+
| `on_press(touch)` | | First touch down on button |
+---------------------------------------+-----------+-------------------------------------+
| `on_release(touch)` | | All touches released |
+---------------------------------------+-----------+-------------------------------------+
| `on_cancel(touch)` | | Touch moved outside bounds |
+---------------------------------------+-----------+-------------------------------------+
Examples
--------
Basic button with visual feedback::
from kivy.app import App
from kivy.uix.label import Label
from kivy.uix.behaviors import ButtonBehavior
class MyButton(ButtonBehavior, Label):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.text = "Press me!"
def on_pressed(self, instance, is_pressed):
# Visual feedback: red when pressed, white otherwise
self.color = [1, 0, 0, 1] if is_pressed else [1, 1, 1, 1]
def on_press(self, touch):
print(f"Button pressed at ({touch.x}, {touch.y})")
def on_release(self, touch):
print(f"Button released at ({touch.x}, {touch.y})")
class SampleApp(App):
def build(self):
return MyButton()
SampleApp().run()
Handling button cancellation::
from kivy.app import App
from kivy.uix.label import Label
from kivy.uix.behaviors import ButtonBehavior
class MyButton(ButtonBehavior, Label):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.text = 'Press me!'
self.always_release = False # Enable cancellation
def on_press(self, touch):
self.text = f'Pressed at {touch.pos} - drag outside to cancel'
def on_release(self, touch):
self.text = f'Released at {touch.pos}!'
def on_cancel(self, touch):
self.text = f'Action cancelled! (left at {touch.pos})'
class SampleApp(App):
def build(self):
return MyButton()
SampleApp().run()
See :class:`~kivy.uix.behaviors.ButtonBehavior` for details.
""" # noqa: E501
__all__ = ("ButtonBehavior",)
from kivy.properties import BooleanProperty, AliasProperty, ObjectProperty
[docs]
class ButtonBehavior:
"""Mixin to add button behavior to any Kivy widget.
This mixin enables widgets to respond to press/release interactions with
automatic multi-touch support. It provides three specialized events:
:meth:`on_press`, :meth:`on_release`, and :meth:`on_cancel`, along with
a reactive :attr:`pressed` property.
Button State Management
-----------------------
Tracks active touches internally using a set-based approach. Each touch
gets a unique ID (supports multi-touch). When first touch enters widget
bounds, button becomes pressed. When ALL touches are released or cancelled,
button returns to unpressed state. The :attr:`pressed` property is True
when any touches are active.
Multi-Touch Behavior
--------------------
The button automatically handles multiple simultaneous touches:
- **First touch down**: Triggers :meth:`on_press`, sets :attr:`pressed` to True
- **Additional touches**: Tracked but don't trigger :meth:`on_press` again
- **Touch release**: Only triggers :meth:`on_release` after ALL touches released
- **Touch cancel**: Removes touch from tracking, updates :attr:`pressed` state
Release Modes
-------------
Controlled by :attr:`always_release` property:
- **False (default)**: Standard button behavior. :meth:`on_release` only fires
if touch ends within button bounds. :meth:`on_cancel` fires when touch
moves outside bounds during drag (touch move).
- **True**: Always fires :meth:`on_release` regardless of touch position.
:meth:`on_cancel` never fires. Useful for drag-and-drop or gesture-based
interfaces.
Events
------
:meth:`on_press`
First touch down on button
:meth:`on_release`
All touches released (respects :attr:`always_release` mode)
:meth:`on_cancel`
Touch moved outside bounds during drag (only when :attr:`always_release`
is False)
.. versionchanged:: 3.0.0
- Replaced ``state`` OptionProperty with ``pressed`` BooleanProperty
- Made ``pressed`` read-only via AliasProperty
- Added :meth:`on_cancel` event
- Improved multi-touch handling with explicit touch sets
- Added touch argument to on_press, on_release, and on_cancel events
"""
always_release = BooleanProperty(False)
"""Determines whether the widget fires :meth:`on_release` when touch ends
outside widget bounds.
When False (default):
- :meth:`on_release` only fires if touch ends within button bounds
- :meth:`on_cancel` fires when touch moves outside bounds during drag
- Provides standard button behavior with cancellation feedback
When True:
- :meth:`on_release` always fires regardless of final touch position
- :meth:`on_cancel` never fires
- Useful for drag-and-drop or gesture-based interfaces where release
position is irrelevant
:attr:`always_release` is a :class:`~kivy.properties.BooleanProperty` and
defaults to False.
.. versionadded:: 1.9.0
.. versionchanged:: 1.10.0
The default value changed from True to False.
"""
def _get_pressed(self):
"""Compute pressed state based on active touches.
:return: True if any touches are currently active
"""
return bool(self._active_touches)
pressed = AliasProperty(_get_pressed, bind=["_active_touches"], cache=True)
"""Read-only boolean indicating if button is currently pressed.
The button is pressed only when currently touched/clicked. This property
automatically handles multi-touch scenarios, remaining True as long as at
least one active touch is on the button.
Updates automatically as touches are added/removed from ``_active_touches``.
:attr:`pressed` is an :class:`~kivy.properties.AliasProperty` and defaults
to False.
.. versionchanged:: 3.0.0
Changed from editable BooleanProperty to read-only AliasProperty
"""
_active_touches = ObjectProperty()
def __init__(self, **kwargs):
"""Initialize ButtonBehavior mixin.
Registers button-specific events and initializes touch tracking sets.
"""
self.register_event_type("on_press")
self.register_event_type("on_release")
self.register_event_type("on_cancel")
# Track touches: active vs cancelled
self._active_touches = set()
self._cancelled_touches = set()
super().__init__(**kwargs)
# NOTE: Internal hooks for subclassing
# ====================================
# These methods are called internally before dispatching the corresponding
# public events. They allow subclasses to implement internal state changes
# (e.g., ToggleButtonBehavior updating its state) separately from
# user-facing event handlers, maintaining a clean separation between
# internal logic and external event dispatch.
# Do NOT call these directly or bind to them - use
# on_press/on_release/on_cancel events instead.
def _do_press(self, touch):
"""Internal hook for subclasses. Called before on_press event dispatch.
:param touch: The :class:`~kivy.input.motionevent.MotionEvent` that
triggered the press
.. note::
It's not recommended to override this unless subclassing for
internal state management.
Use :meth:`on_press` event for application logic instead.
"""
pass
def _do_release(self, touch):
"""Internal hook for subclasses. Called before on_release event dispatch.
:param touch: The :class:`~kivy.input.motionevent.MotionEvent` that
triggered the release (the last touch being released)
.. note::
It's not recommended to override this unless subclassing for
internal state management.
Use :meth:`on_release` event for application logic instead.
"""
pass
def _do_cancel(self, touch):
"""Internal hook for subclasses. Called before on_cancel event dispatch.
:param touch: The :class:`~kivy.input.motionevent.MotionEvent` that
moved outside bounds and triggered the cancellation
.. note::
It's not recommended to override this unless subclassing for
internal state management.
Use :meth:`on_cancel` event for application logic instead.
"""
pass
def _add_active_touch(self, touch):
"""Add touch to active tracking and trigger property update.
:param touch: Touch to add to active set
"""
self._active_touches = self._active_touches | {touch}
def _remove_active_touch(self, touch):
"""Remove touch from active tracking and trigger property update.
:param touch: Touch to remove from active set
"""
self._active_touches = self._active_touches - {touch}
[docs]
def on_touch_down(self, touch):
"""Handle touch down events.
Implements core press detection:
1. Test collision with widget bounds
2. Grab touch if colliding (marks widget as touch owner)
3. Add to active touches tracking
4. Dispatch :meth:`on_press` on first touch
:param touch: :class:`~kivy.input.motionevent.MotionEvent` instance
:return: True if event was handled (collided with widget)
"""
# Let parent handle first
if super().on_touch_down(touch):
return True
# Ignore scroll events
if touch.is_mouse_scrolling:
return False
# Only handle touches within bounds
if not self.collide_point(touch.x, touch.y):
return False
# Prevent double-handling
if self in touch.ud:
return False
# Grab touch to track its lifecycle
touch.grab(self)
touch.ud[self] = True
# Check if this is the first touch before adding
is_first_touch = len(self._active_touches) == 0
self._add_active_touch(touch)
# Only dispatch press event on first touch
if is_first_touch:
self._do_press(touch)
self.dispatch("on_press", touch)
return True
[docs]
def on_touch_move(self, touch):
"""Handle touch move events.
Detects when touch moves outside button bounds during drag and
triggers cancellation if :attr:`always_release` is False.
Cancellation flow:
1. Touch owned by this widget moves outside bounds
2. Remove from active touches, add to cancelled touches
3. Dispatch :meth:`on_cancel` if this was the last active touch
:param touch: :class:`~kivy.input.motionevent.MotionEvent` instance
:return: True if event was handled
"""
# We own this touch
if touch.grab_current is self:
# Cancel if moved outside and cancellation is enabled
if (
not self.always_release
and not self.collide_point(touch.x, touch.y)
and touch not in self._cancelled_touches
):
# Move from active to cancelled
if touch in self._active_touches:
self._remove_active_touch(touch)
self._cancelled_touches.add(touch)
# Dispatch cancel event if this was the last active touch
if not self._active_touches:
self._do_cancel(touch)
self.dispatch("on_cancel", touch)
return True
# Let parent handle
if super().on_touch_move(touch):
return True
# We touched this widget before
return self in touch.ud
[docs]
def on_touch_up(self, touch) -> bool:
"""Handle touch up events.
Implements release detection:
1. Verify we own this touch
2. Ungrab touch to release ownership
3. Remove from active/cancelled tracking
4. Dispatch :meth:`on_release` if appropriate:
- Touch wasn't cancelled, AND
- (Touch ended within bounds OR :attr:`always_release` is True), AND
- This was the last active touch
:param touch: :class:`~kivy.input.motionevent.MotionEvent` instance
:return: True if event was handled
"""
# Not our touch
if touch.grab_current is not self:
return super().on_touch_up(touch)
# Sanity check
assert self in touch.ud
# Release ownership
touch.ungrab(self)
# Remove from active tracking
self._remove_active_touch(touch)
# Check if this touch was cancelled
is_cancelled = touch in self._cancelled_touches
# Dispatch release if not cancelled and conditions met
if not is_cancelled and (
self.always_release or self.collide_point(*touch.pos)
):
# Only dispatch release after ALL touches are released
if not self._active_touches:
self._do_release(touch)
self.dispatch("on_release", touch)
# Cleanup cancelled touch tracking
self._cancelled_touches.discard(touch)
return True
[docs]
def on_press(self, touch):
"""Event handler called when the button is pressed.
This event is fired when the first touch down occurs on the button.
In multi-touch scenarios, only the first touch triggers this event.
:param touch: The :class:`~kivy.input.motionevent.MotionEvent` that
triggered the press. Contains position (touch.x, touch.y),
timestamp, and other touch attributes.
Example::
def on_press(self, touch):
print(f"Pressed at position: {touch.pos}")
print(f"Touch ID: {touch.id}")
"""
pass
[docs]
def on_release(self, touch):
"""Event handler called when the button is released.
This event is fired when the last active touch is released, and only if:
- The touch is released within button bounds, OR
- The `always_release` property is True
:param touch: The :class:`~kivy.input.motionevent.MotionEvent` of the
last touch being released. In multi-touch scenarios, this
is the touch that completed the release action.
Example::
def on_release(self, touch):
print(f"Released at position: {touch.pos}")
duration = touch.time_end - touch.time_start
print(f"Touch duration: {duration:.2f}s")
"""
pass
[docs]
def on_cancel(self, touch):
"""Event handler called when touch leaves button bounds during drag.
This event is only fired when `always_release` is False and a touch
moves outside the button's bounds during a drag operation. It provides
an opportunity to give visual feedback that the button action has been
cancelled.
Use this to restore the button's original appearance or cancel any
pending actions that would have occurred on release.
:param touch: The :class:`~kivy.input.motionevent.MotionEvent` that
moved outside the button bounds. Access touch.pos to get
the position where the touch left the bounds.
Example::
def on_cancel(self, touch):
print(f"Action cancelled at position: {touch.pos}")
self.background_color = [1, 1, 1, 1] # Reset appearance
.. versionadded:: 3.0.0
"""
pass