Source code for mytk.eventcapable
"""Event mixin class for widget behavior.
Provides the `EventCapable` class, a mixin designed to add event-related
capabilities to GUI widgets that expose a `widget` attribute compatible with
`tk.Widget`. This includes scheduling timed callbacks, cancelling them, and
binding or generating Tkinter events.
It is used for Base and App, which handle their widgets differently.
Also includes the `HasWidget` protocol to allow static checking of widget
presence for type-checkers.
"""
import tkinter as tk
from typing import Callable, Protocol, Sequence, runtime_checkable
[docs]
class EventCapable:
"""Mixin class providing event-related methods for classes exposing a `widget` attribute.
Designed to be used alongside other base classes that define `self.widget`
as a `tk.Widget`. This class should not be used on its own.
Responsibilities:
- Scheduling and cancelling timed callbacks (`after`, `after_cancel`, etc.)
- Event binding and generation via `widget.bind` and `widget.event_generate`
- Lifecycle cleanup via `__del__`
"""
widget: "tk.Widget" # Hint for static type checkers and linters like pylint
def __init__(self, *args, **kwargs):
"""Initialize internal scheduling structures for cooperative multiple inheritance."""
self.scheduled_tasks = []
super().__init__() # cooperative!
# def __del__(self):
# """
# Cancels all registered scheduled tasks to prevent dangling callbacks.
# Also invokes superclass `__del__` if defined.
# """
# for task_id in self.scheduled_tasks:
# self.after_cancel(task_id)
# super_del = getattr(super(), "__del__", None)
# if callable(super_del):
# with suppress(Exception):
# super_del() # pylint: disable=not-callable
def _valid_mixin_class(self):
"""Ensures that `self.widget` exists before performing widget operations.
Raises:
AttributeError: If `self` does not define a `widget` attribute.
"""
if not isinstance(self, HasWidget):
raise AttributeError(
f"EventCapable requires {self.__class__.__name__} to provide a 'widget' attribute"
)
[docs]
def after(self, delay: int, function: Callable) -> int:
"""Schedules a function to be called after a given time delay.
Args:
delay (int): Delay in milliseconds.
function (Callable): Function to invoke.
Returns:
int: Identifier of the scheduled task, which can be used with `after_cancel`.
"""
self._valid_mixin_class()
task_id = None
if self.widget is not None and function is not None:
task_id = self.widget.after(delay, function)
self.scheduled_tasks.append(task_id)
return task_id
[docs]
def after_cancel(self, task_id: int):
"""Cancels a previously scheduled task by its ID.
Args:
task_id (int): ID of the task returned by `after()`.
"""
self._valid_mixin_class()
if self.widget is not None:
self.widget.after_cancel(task_id)
self.scheduled_tasks.remove(task_id)
[docs]
def after_cancel_many(self, task_ids: Sequence[int]):
"""Cancels multiple tasks given a sequence of IDs.
Args:
task_ids (Sequence[int]): List or tuple of task IDs to cancel.
"""
for task_id in task_ids:
self.after_cancel(task_id)
[docs]
def after_cancel_all(self):
"""Cancel all currently scheduled tasks for this object."""
self.after_cancel_many(list(self.scheduled_tasks))
def _bind_destroy_cancel(self):
"""Bind a Destroy handler that cancels all scheduled tasks on widget destruction.
Called automatically after create_widget() in grid_into/pack_into/place_into.
"""
if self.widget is not None:
self.widget.bind(
'<Destroy>',
lambda e: self.after_cancel_all() if e.widget is self.widget else None
)
[docs]
def bind_event(self, event: str, callback: Callable):
"""Binds a callback function to a specific event on the underlying widget.
Args:
event (str): Tkinter event string (e.g. "<Button-1>").
callback (Callable): Function to be called when the event occurs.
"""
self._valid_mixin_class()
self.widget.bind(event, callback)
[docs]
def event_generate(self, event: str):
"""Triggers an event on the underlying widget programmatically.
Args:
event (str): Event name to trigger (e.g., "<<CustomEvent>>").
"""
self._valid_mixin_class()
self.widget.event_generate(event)