Checkpoint before ghost pallet cleanup workflow
This commit is contained in:
150
gestione_aree.py
Normal file
150
gestione_aree.py
Normal file
@@ -0,0 +1,150 @@
|
||||
"""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("<Configure>", 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("<Configure>", 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
|
||||
Reference in New Issue
Block a user