Table Of Contents

Source code for kivy.uix.behaviors.togglebutton

"""
ToggleButton Behavior
=====================

The :class:`~kivy.uix.behaviors.togglebutton.ToggleButtonBehavior`
`mixin <https://en.wikipedia.org/wiki/Mixin>`_ class provides toggle button
behavior for Kivy widgets. You can combine this class with other widgets to add
specialized on/off state management with optional radio button grouping.

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


Quick Overview
--------------

+-----------------------------------------------------------------------------------------+
| TOGGLE BUTTON BEHAVIOR (extends ButtonBehavior)                                         |
+---------------------------------------+-----------+-------------------------------------+
| STATE                                 | TYPE      | DESCRIPTION                         |
+=======================================+===========+=====================================+
| `pressed`                             | read-only | Currently touched?                  |
+---------------------------------------+-----------+-------------------------------------+
| `activated`                           | read/write| Persistent on/off state             |
+---------------------------------------+-----------+-------------------------------------+
| `always_release`                      | read/write| Always fire on_release?             |
+---------------------------------------+-----------+-------------------------------------+
| `allow_no_selection`                  | read/write| Allow button off?                   |
+---------------------------------------+-----------+-------------------------------------+
| **GROUPING**                                                                            |
+---------------------------------------+-----------+-------------------------------------+
| `group`                               | read/write| Group identifier (string or tuple)  |
+---------------------------------------+-----------+-------------------------------------+
| **CONFIGURATION**                                                                       |
+---------------------------------------+-----------+-------------------------------------+
| `toggle_on`                           | read/write| 'press' or 'release'                |
+---------------------------------------+-----------+-------------------------------------+
| **EVENTS**                                                                              |
+---------------------------------------+-----------+-------------------------------------+
| `on_press(touch)`                     |           | First touch down on button          |
+---------------------------------------+-----------+-------------------------------------+
| `on_release(touch)`                   |           | All touches released                |
+---------------------------------------+-----------+-------------------------------------+
| `on_cancel(touch)`                    |           | Touch moved outside bounds          |
+---------------------------------------+-----------+-------------------------------------+
| **METHODS**                                                                             |
+---------------------------------------+-----------+-------------------------------------+
| `<togglebutton_instance>.get_group()` |           | Get widgets in button's group       |
+---------------------------------------+-----------+-------------------------------------+
| `ToggleButtonBehavior.get_group(name)`|           | Get widgets by global group name    |
+---------------------------------------+-----------+-------------------------------------+
| `ToggleButtonBehavior.get_group(      |           | Get widgets by scoped group         |
| (owner, name))`                       |           |                                     |
+---------------------------------------+-----------+-------------------------------------+


Examples
--------

Basic toggle with visual feedback::

    from kivy.app import App
    from kivy.lang import Builder
    from kivy.uix.label import Label
    from kivy.uix.behaviors import ToggleButtonBehavior

    class MyToggle(ToggleButtonBehavior, Label):
        Builder.load_string('''
    <MyToggle>:
        canvas.before:
            Color:
                rgb: self.color
                a: 0.25
            Rectangle:
                size: self.size
                pos: self.pos
        ''')

        def on_activated(self, instance, value):
            self.color = [0, 1, 0, 1] if value else [1, 1, 1, 1]

    class SampleApp(App):
        def build(self):
            return MyToggle()

    SampleApp().run()

Radio button group behavior::

    from kivy.lang import Builder
    from kivy.app import App
    from kivy.uix.boxlayout import BoxLayout
    from kivy.uix.label import Label
    from kivy.uix.behaviors import ToggleButtonBehavior


    class MyToggle(ToggleButtonBehavior, Label):
        Builder.load_string('''
    <MyToggle>:
        canvas.before:
            Color:
                rgb: self.color
                a: 0.25
            Rectangle:
                size: self.size
                pos: self.pos
        ''')

        def on_activated(self, instance, value):
            self.color = [0, 1, 0, 1] if value else [1, 1, 1, 1]


    class SampleApp(App):
        def build(self):
            layout = BoxLayout()
            for i in range(3):
                btn = MyToggle(
                    text=f"Option {i + 1}",
                    group="options",
                    allow_no_selection=False,
                )
                layout.add_widget(btn)
            return layout


    SampleApp().run()


Scoped groups for reusable components::

    from kivy.lang import Builder
    from kivy.app import App
    from kivy.uix.boxlayout import BoxLayout
    from kivy.uix.label import Label
    from kivy.uix.behaviors import ToggleButtonBehavior


    class MyToggle(ToggleButtonBehavior, Label):
        Builder.load_string('''
    <MyToggle>:
        canvas.before:
            Color:
                rgb: self.color
                a: 0.25
            Rectangle:
                size: self.size
                pos: self.pos
        ''')

        def on_activated(self, instance, value):
            self.color = [0, 1, 0, 1] if value else [1, 1, 1, 1]


    class ToggleRow(BoxLayout):
        '''Reusable component with scoped group.'''

        def __init__(self, label_prefix="Option", **kwargs):
            super().__init__(**kwargs)
            for i in range(3):
                btn = MyToggle(
                    text=f"{label_prefix}, Col {i + 1}",
                    # group=("options"),  # Global, not scoped

                    # Scoped to this row (ToggleRow instance)
                    group=(self, "options"),

                    allow_no_selection=False,
                    activated=i == 0,
                )
                self.add_widget(btn)


    class SampleApp(App):
        def build(self):
            layout = BoxLayout(orientation="vertical")
            # Each row has independent "options" group
            layout.add_widget(ToggleRow(label_prefix="Row 1"))
            layout.add_widget(ToggleRow(label_prefix="Row 2"))
            return layout


    SampleApp().run()


See :class:`~kivy.uix.behaviors.ToggleButtonBehavior` for details.
"""  # noqa: E501

__all__ = ("ToggleButtonBehavior",)

from collections import defaultdict
from weakref import WeakSet

from kivy.logger import Logger
from kivy.properties import (
    AliasProperty,
    BooleanProperty,
    ObjectProperty,
    OptionProperty,
)
from kivy.uix.behaviors.button import ButtonBehavior


class _GroupAccessor:
    """Descriptor that provides unified group access as both instance and class
    method.

    When accessed from an instance, returns widgets in that instance's group.
    When accessed from the class, returns a callable that accepts a group
    identifier.

    This provides a single, intuitive API for accessing group widgets regardless
    of whether you have a widget instance or just a group identifier.
    """

    def __get__(self, instance, owner):
        if instance is None:
            # Called as class method: ToggleButtonBehavior.get_group(group_id)
            return lambda group: self._get_by_identifier(owner, group)
        else:
            # Called as instance method: my_button.get_group()
            return lambda: self._get_by_instance(instance)

    def _get_by_instance(self, instance):
        """Get widgets from instance's current group."""
        owner, name = instance._current_group_key
        return self._get_widgets_from_group(instance.__class__, owner, name)

    def _get_by_identifier(self, cls, group):
        """Get widgets by group identifier."""
        owner, name = cls._parse_group_static(group)
        return self._get_widgets_from_group(cls, owner, name)

    def _get_widgets_from_group(self, cls, owner, name):
        """Helper to retrieve widgets from group storage."""
        if name is None:
            return []
        return list(cls._toggle_groups.get(owner, {}).get(name, []))


[docs] class ToggleButtonBehavior(ButtonBehavior): """Mixin to add toggle button behavior to any Kivy widget. This mixin extends :class:`ButtonBehavior` to provide persistent on/off state that survives beyond the press/release interaction. It supports grouping multiple toggles together for radio button behavior. State Management ---------------- Unlike the transient :attr:`pressed` property from ButtonBehavior (which is True only during active touch), the :attr:`activated` property maintains persistent state: - **activated=True**: Button is in "on" state (persists after release) - **activated=False**: Button is in "off" state (default) The :attr:`activated` property can be set programmatically or toggled by user interaction. When :attr:`allow_no_selection` is False, attempts to deactivate the last active button in a group are rejected: programmatic attempts log a warning, while touch interactions are silently blocked. Group Behavior -------------- When buttons share a :attr:`group`, they act like radio buttons: - Activating one button automatically deactivates others in the group - Only one button per group can be active at a time - :attr:`allow_no_selection` controls whether all buttons can be deactivated **Without group**: Buttons toggle independently **With group**: Buttons behave as mutually exclusive options Group Scoping ~~~~~~~~~~~~~ Groups can be either global or scoped to a specific widget owner: **Global groups** (string):: ToggleButton: group: "mygroup" # Shared across entire application **Scoped groups** (tuple):: ToggleButton: group: (root, "mygroup") # Scoped to 'root' widget Scoped groups allow multiple widget hierarchies to use the same group name without interference. This is useful when creating reusable components with internal toggle groups. Example:: <MyComponent>: # These groups won't conflict with other MyComponent instances ToggleButton: group: (root, "options") ToggleButton: group: (root, "options") Events ------ Inherits all events from ButtonBehavior: - :meth:`on_press`: First touch down - :meth:`on_release`: All touches released - :meth:`on_cancel`: Touch moved outside bounds .. versionadded:: 1.8.0 .. versionchanged:: 3.0.0 - Replaced ``state`` OptionProperty with ``activated`` AliasProperty - Improved group management with automatic cleanup - Added scoped group support via tuple syntax - Replaced ``get_widgets(groupname)`` static method with unified ``get_group()`` descriptor that works as both instance and class method """ _toggle_groups = defaultdict(lambda: defaultdict(WeakSet)) group = ObjectProperty(None, allownone=True) """Group of the button. If None, no group will be used (button toggles independently). Accepts two formats: - **Hashable value** (string, int, etc.): Global group shared across entire application - **Tuple (owner, name)**: Scoped group limited to owner's context Global groups (any hashable):: group: "mygroup" group: 42 group: MyEnum.OPTION_A Scoped groups (tuple of any object + hashable name):: group: (root, "mygroup") group: (self.parent, "options") group: (app.current_screen, "filters") **Tuple format**: ``(owner, name)`` - **owner**: Any object to scope the group to (typically a widget). Uses object identity, not equality. Can be weakly referenced. - **name**: Hashable identifier for the group (string, int, enum, etc.) Only one button per group can be active at a time. When a button in a group is activated, all other buttons in that group are automatically deactivated. Scoped groups allow multiple widget instances to use the same group name without interference, making it easier to create reusable components. Example of reusable component:: # Multiple instances won't interfere with each other <FilterPanel>: ToggleButton: group: (root, "size") text: "Small" ToggleButton: group: (root, "size") text: "Large" # Each FilterPanel instance has independent "size" groups FilterPanel: id: panel1 FilterPanel: id: panel2 :attr:`group` is a :class:`~kivy.properties.ObjectProperty` and defaults to None. .. versionchanged:: 3.0.0 Added support for tuple format to enable scoped groups. """ allow_no_selection = BooleanProperty(True) """Specifies whether widgets in a group allow no selection (all deselected). - **True (default)**: Clicking active button deactivates it (all buttons off) - **False**: At least one button must remain active (radio button behavior) When False, attempts to deactivate the last active button are rejected: programmatic ``activated = False`` logs a warning, while touch interactions are silently blocked. Only applies when :attr:`group` is set. .. versionadded:: 1.9.0 :attr:`allow_no_selection` is a :class:`~kivy.properties.BooleanProperty` and defaults to True. """ def _get_activated(self): """Get current active state.""" return self._activated def _set_activated(self, value): """Set the activated state with group validation. Used as the setter for :attr:`activated`. Delegates all logic to :meth:`_change_activated_state` to keep programmatic and interactive changes consistent. :param value: Desired active state. :type value: bool """ self._change_activated_state(value) activated = AliasProperty( _get_activated, _set_activated, bind=["_activated"], cache=True ) """Persistent toggle state of the button. - **True**: Button is in "on" state - **False**: Button is in "off" state Unlike :attr:`pressed` (which is True only during touch), :attr:`activated` maintains state after release. Can be set programmatically or toggled by user interaction. When button is in a group, setting activated=True automatically deactivates other buttons in that group. When :attr:`allow_no_selection` is False, attempts to set activated=False on the last active button in a group log a warning and are rejected. Touch interactions are silently blocked without warnings. :attr:`activated` is an :class:`~kivy.properties.AliasProperty` and defaults to False. .. versionadded:: 3.0.0 """ toggle_on = OptionProperty("release", options=["press", "release"]) """Specifies which :class:`ButtonBehavior` event triggers the toggle state change. - **"release" (default)** — The state toggles when the button is released. - **"press"** — The state toggles immediately when the button is pressed. This property allows adjusting when the toggle action occurs, depending on the desired interaction style. It can be changed dynamically at runtime, and the new behavior takes effect immediately. :attr:`toggle_on` is an :class:`~kivy.properties.OptionProperty` and accepts either ``"press"`` or ``"release"``. Defaults to ``"release"``. """ _activated = BooleanProperty(False) # Internal storage for activated state get_group = _GroupAccessor() """Get widgets in a group. This descriptor works as both an instance method and a class method, providing a unified API for accessing group widgets. **As instance method** (no arguments): Returns all widgets in the same group as this instance, including this instance itself. Example:: # Get all widgets in my_button's group widgets = my_button.get_group() for w in widgets: print(w.text) del widgets # Release immediately **As class method** (pass group identifier): Returns all widgets in the specified group. Useful when you don't have a widget instance but know the group identifier. Example:: # Get widgets by global group name widgets = ToggleButtonBehavior.get_group("options") # Get widgets by scoped group widgets = ToggleButtonBehavior.get_group((panel, "filters")) # Useful after removing a widget my_group = widget.group widget.parent.remove_widget(widget) remaining = ToggleButtonBehavior.get_group(my_group) .. warning:: Always release the result immediately! Holding references prevents garbage collection:: widgets = button.get_group() # use widgets del widgets .. note:: List may contain recently deleted widgets that haven't been garbage collected yet. :return: List of widget instances in the group (empty list if group doesn't exist or has no members) .. versionadded:: 3.0.0 Replaces the old ``get_widgets(groupname)`` static method with a unified descriptor that works as both instance and class method. """ def __init__(self, **kwargs): """Initialize ToggleButtonBehavior mixin. Sets up group tracking and binds property listeners. """ self._current_group_key = (None, None) # Bind early so initial 'group' value triggers handler self.fbind("group", self._handle_group) # Initialize parent behavior super().__init__(**kwargs) # override ButtonBehavior._do_press hook def _do_press(self, touch): if self.toggle_on == "press": self._handle_toggle_on() # override ButtonBehavior._do_release hook def _do_release(self, touch): if self.toggle_on == "release": self._handle_toggle_on() def _change_activated_state(self, new_value, _from_touch=False): """Internal helper to update the active state with full validation and group management. :param _from_touch: True if called from touch interaction, False if programmatic """ owner, name = self._current_group_key # Block deactivation if this would leave the group with no active members if not new_value and not self.allow_no_selection and name: for w in self._toggle_groups[owner][name]: if w is not self and w.activated: break else: # Show warning only for programmatic changes if not _from_touch: Logger.warning( f"ToggleButtonBehavior: Cannot deactivate {self}: last " "active button in group (with allow_no_selection=False)", ) return # Apply state change self._activated = new_value if new_value and name is not None: self._deactivate_group_siblings() def _handle_toggle_on(self, *args): """Toggle the button state in response to a press or release. - Skips toggling if :attr:`allow_no_selection` is False and the button is already activated. - Otherwise, inverts the :attr:`activated` state. - State validation and group updates are handled by :meth:`_change_activated_state`. Called by :meth:`_do_press` or :meth:`_do_release` depending on :attr:`toggle_on`. """ owner, name = self._current_group_key if not self.allow_no_selection and name and self.activated: return self._change_activated_state(not self.activated, _from_touch=True) def _parse_group(self, group): """Parse group into (owner, name) tuple. Supports two formats: - String: Global group → (None, name) - Tuple: Scoped group → (owner, name) :param group: Group identifier (string, tuple, or None) :return: Tuple of (owner, name) or (None, None) if group is None :raises ValueError: If tuple format is invalid """ return self._parse_group_static(group) @staticmethod def _parse_group_static(group): """Static version of _parse_group for class method usage. Parse group into (owner, name) tuple without requiring instance context. :param group: Group identifier (string, tuple, or None) :return: Tuple of (owner, name) or (None, None) if group is None :raises ValueError: If tuple format is invalid """ if group is None: return None, None if isinstance(group, tuple): if len(group) != 2: raise ValueError( f"Group tuple must have exactly 2 elements (owner, name), " f"got {len(group)}" ) owner, name = group if name is None: raise ValueError( "Group name (second tuple element) cannot be None" ) return owner, name return None, group def _validate_group_name(self, name): """Validate that group name is hashable. :param name: Group name to validate :raises ValueError: If name is not hashable """ if name is not None: try: hash(name) except TypeError: raise ValueError( f"Group name must be hashable, got {type(name).__name__}" ) def _handle_group(self, instance, new_group): """Handle group property changes. Manages widget registration in group tracking system: 1. Parses group into (owner, name) tuple 2. Removes widget from previous group 3. Registers widget in new group 4. Updates internal group reference Groups use weak references to prevent memory leaks. :param instance: This button instance :param new_group: New group value (string, tuple, or None) """ owner, name = self._parse_group(new_group) self._validate_group_name(name) # Remove from previous group old_owner, old_name = self._current_group_key if old_name is not None: self._toggle_groups[old_owner][old_name].discard(self) # Cleanup empty groups if not self._toggle_groups[old_owner][old_name]: del self._toggle_groups[old_owner][old_name] # Cleanup empty owner entries if not self._toggle_groups[old_owner]: del self._toggle_groups[old_owner] # Register in new group if name is not None: self._toggle_groups[owner][name].add(self) self._current_group_key = (owner, name) # Revalidate activated state in new group if self.activated and name is not None: self._deactivate_group_siblings() def _deactivate_group_siblings(self): """Deactivate all other buttons in the same group. Only affects buttons in the same scoped group (same owner + name). """ owner, name = self._current_group_key if name is None: return for widget in self._toggle_groups[owner][name]: if widget is not self and widget.activated: widget.activated = False
[docs] def on_activated(self, instance, value): """Event handler called when activated state changes. Override this method to respond to state changes. :param instance: This button instance :param value: New activated state (True/False) """ pass