"""Shared Tk/async helpers used by multiple warehouse windows. The module bundles two concerns used throughout the GUI: * a modal-like busy overlay shown during long-running tasks; * an ``AsyncRunner`` that schedules coroutines on the shared loop and re-enters Tk safely. The shared loop itself is defined only in :mod:`async_loop_singleton` and is reused here instead of being recreated locally. """ from __future__ import annotations import asyncio import tkinter as tk from typing import Any, Callable, Optional import customtkinter as ctk from async_loop_singleton import get_global_loop __VERSION__ = "GestioneAreeFrame v3.3.0-singleloop" try: from async_msssql_query import AsyncMSSQLClient # noqa: F401 except Exception: AsyncMSSQLClient = object # type: ignore class BusyOverlay: """Semi-transparent overlay used to block interaction during async tasks.""" def __init__(self, parent: tk.Misc): """Bind the overlay lifecycle to the given parent widget.""" self.parent = parent self._top: Optional[ctk.CTkToplevel] = None self._pb: Optional[ctk.CTkProgressBar] = None self._lbl: Optional[ctk.CTkLabel] = None self._bind_id = None def _reposition(self): """Resize the overlay so it keeps covering the parent toplevel.""" if not self._top: return root = self.parent.winfo_toplevel() root.update_idletasks() x, y = root.winfo_rootx(), root.winfo_rooty() w, h = root.winfo_width(), root.winfo_height() self._top.geometry(f"{w}x{h}+{x}+{y}") def show(self, message="Attendere..."): """Display the overlay or just update its message if already visible.""" if self._top: if self._lbl: self._lbl.configure(text=message) return root = self.parent.winfo_toplevel() top = ctk.CTkToplevel(root) self._top = top top.overrideredirect(True) try: top.attributes("-alpha", 0.22) except tk.TclError: pass try: top.configure(fg_color="#000000") except Exception: top.configure(bg="#000000") top.attributes("-topmost", True) wrap = ctk.CTkFrame(top, corner_radius=8) wrap.place(relx=0.5, rely=0.5, anchor="center") self._lbl = ctk.CTkLabel(wrap, text=message, font=("Segoe UI", 11, "bold")) self._lbl.pack(pady=(0, 10)) self._pb = ctk.CTkProgressBar(wrap, mode="indeterminate", width=260) self._pb.pack(fill="x") try: self._pb.start() except Exception: pass self._reposition() self._bind_id = root.bind("", lambda e: self._reposition(), add="+") def hide(self): """Dismiss the overlay and unregister resize bindings.""" if self._pb: try: self._pb.stop() except Exception: pass self._pb = None if self._top: try: self._top.destroy() except Exception: pass self._top = None root = self.parent.winfo_toplevel() if self._bind_id: try: root.unbind("", self._bind_id) except Exception: pass self._bind_id = None class AsyncRunner: """Run awaitables on the shared loop and callback on Tk's main thread.""" def __init__(self, widget: tk.Misc): """Capture the widget used to marshal callbacks back to Tk.""" self.widget = widget self.loop = get_global_loop() def run( self, awaitable, on_success: Callable[[Any], None], on_error: Optional[Callable[[BaseException], None]] = None, busy: Optional[BusyOverlay] = None, message: str = "Operazione in corso...", ): """Schedule ``awaitable`` and dispatch completion callbacks in Tk.""" if busy: busy.show(message) fut = asyncio.run_coroutine_threadsafe(awaitable, self.loop) def _poll(): if fut.done(): if busy: busy.hide() try: res = fut.result() except BaseException as ex: if on_error: self.widget.after(0, lambda e=ex: on_error(e)) else: print("[AsyncRunner] Unhandled error:", repr(ex)) else: self.widget.after(0, lambda r=res: on_success(r)) else: self.widget.after(60, _poll) _poll() def close(self): """No-op kept for API compatibility with older callers.""" pass