Source code for mytk.draganddropcapable

"""Drag-and-drop mixin class for widget behavior.

Provides the `DragAndDropCapable` class, a mixin designed to add OS-level
drag-and-drop (files dropped from the system file manager) to GUI widgets that
expose a `widget` attribute compatible with `tk.Widget`. It mirrors
`EventCapable`: a small mixin combined into `Base`, never used on its own.

OS drag-and-drop is not part of core Tk; the heavy lifting (loading the tkdnd
extension onto the window and parsing the dropped payload) lives in `mytk.dnd`.
This mixin is the per-widget API on top of it.
"""

from .dnd import dropped_paths, ensure_tkdnd
from .eventcapable import HasWidget


[docs] class DragAndDropCapable: """Mixin adding drag-and-drop methods for classes exposing a `widget`. Designed to be used alongside base classes that define `self.widget` as a `tk.Widget` (and, like the dropped-file callback here, alongside `EventCapable` for `self.after`). This class should not be used on its own. Responsibilities: - Checking whether drag-and-drop can be enabled (`is_drag_and_drop_available`) - Registering a widget as a target for dropped files (`accept_dropped_files`) """ widget: "object" # Hint for static type checkers; a tk.Widget at runtime def __init__(self, *args, **kwargs): """Initialize internal drop state for cooperative multiple inheritance.""" self._drop_callbacks = [] # keep strong refs to registered callbacks super().__init__() # cooperative! 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"DragAndDropCapable requires {self.__class__.__name__} to " f"provide a 'widget' attribute" )
[docs] def is_drag_and_drop_available(self): """Whether OS drag-and-drop can be enabled on this widget's window. Loads the tkdnd extension on first call (pulling in the optional `tkinterdnd2` dependency); returns False if it cannot be enabled. """ self._valid_mixin_class() if self.widget is None: return False return ensure_tkdnd(self.widget.winfo_toplevel()) is not None
[docs] def accept_dropped_files(self, callback): """Accept files dropped onto this widget from the OS file manager. ``callback(paths)`` is invoked on each drop with a list of filesystem paths. Every dropped file is delivered — it is up to the callback to try to use them and report anything it cannot handle. Call this after the widget exists (e.g. after grid_into / pack_into / place_into). Returns True if drag-and-drop is available in this environment, or False if it could not be enabled — in which case the widget keeps working, just without drops. Enabling it pulls in the optional ``tkinterdnd2`` dependency on first use (see :mod:`mytk.dnd`). """ self._valid_mixin_class() if self.widget is None: raise RuntimeError( "accept_dropped_files() needs the widget to exist; place it " "(grid_into/pack_into/place_into) first." ) root = self.widget.winfo_toplevel() tkdnd = ensure_tkdnd(root) if tkdnd is None: return False self._drop_callbacks.append(callback) def on_drop(event): # Run the callback on the next event-loop tick (via the tracked # self.after), not inline: the OS drag is still in progress during # <<Drop>>, so a callback that blocks (e.g. opens a modal dialog) # would deadlock the handshake. paths = dropped_paths(root, event.data) self.after(0, lambda: callback(paths)) return getattr(event, "action", None) self.widget.drop_target_register(tkdnd.DND_FILES) self.widget.dnd_bind("<<Drop>>", on_drop) return True