migrazione verso gitea
This commit is contained in:
@@ -1,39 +1,52 @@
|
||||
# gestione_aree_frame_async.py
|
||||
"""Shared Tk/async helpers used by multiple warehouse windows.
|
||||
|
||||
The module bundles three concerns used throughout the GUI:
|
||||
|
||||
* lifecycle of the shared background asyncio loop;
|
||||
* a modal-like busy overlay shown during long-running tasks;
|
||||
* an ``AsyncRunner`` that schedules coroutines and re-enters Tk safely.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import threading
|
||||
import tkinter as tk
|
||||
import customtkinter as ctk
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
import customtkinter as ctk
|
||||
|
||||
__VERSION__ = "GestioneAreeFrame v3.2.5-singleloop"
|
||||
#print("[GestioneAreeFrame] loaded", __VERSION__)
|
||||
|
||||
try:
|
||||
from async_msssql_query import AsyncMSSQLClient # noqa: F401
|
||||
except Exception:
|
||||
AsyncMSSQLClient = object # type: ignore
|
||||
|
||||
# ========================
|
||||
# Global asyncio loop
|
||||
# ========================
|
||||
|
||||
class _LoopHolder:
|
||||
"""Keep references to the shared event loop and its worker thread."""
|
||||
|
||||
def __init__(self):
|
||||
self.loop: Optional[asyncio.AbstractEventLoop] = None
|
||||
self.thread: Optional[threading.Thread] = None
|
||||
self.ready = threading.Event()
|
||||
|
||||
|
||||
_GLOBAL = _LoopHolder()
|
||||
|
||||
|
||||
def _run_loop():
|
||||
"""Create and run the shared event loop inside the worker thread."""
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
_GLOBAL.loop = loop
|
||||
_GLOBAL.ready.set()
|
||||
loop.run_forever()
|
||||
|
||||
|
||||
def get_global_loop() -> asyncio.AbstractEventLoop:
|
||||
"""Return the shared background event loop, creating it if needed."""
|
||||
if _GLOBAL.loop is not None:
|
||||
return _GLOBAL.loop
|
||||
_GLOBAL.thread = threading.Thread(target=_run_loop, name="warehouse-asyncio", daemon=True)
|
||||
@@ -43,7 +56,9 @@ def get_global_loop() -> asyncio.AbstractEventLoop:
|
||||
raise RuntimeError("Impossibile avviare l'event loop globale")
|
||||
return _GLOBAL.loop
|
||||
|
||||
|
||||
def stop_global_loop():
|
||||
"""Stop the shared event loop and release thread references."""
|
||||
if _GLOBAL.loop and _GLOBAL.loop.is_running():
|
||||
_GLOBAL.loop.call_soon_threadsafe(_GLOBAL.loop.stop)
|
||||
if _GLOBAL.thread:
|
||||
@@ -52,11 +67,12 @@ def stop_global_loop():
|
||||
_GLOBAL.thread = None
|
||||
_GLOBAL.ready.clear()
|
||||
|
||||
# ========================
|
||||
# Busy overlay
|
||||
# ========================
|
||||
|
||||
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
|
||||
@@ -64,6 +80,7 @@ class BusyOverlay:
|
||||
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()
|
||||
@@ -72,7 +89,8 @@ class BusyOverlay:
|
||||
w, h = root.winfo_width(), root.winfo_height()
|
||||
self._top.geometry(f"{w}x{h}+{x}+{y}")
|
||||
|
||||
def show(self, message="Attendere…"):
|
||||
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)
|
||||
@@ -106,6 +124,7 @@ class BusyOverlay:
|
||||
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()
|
||||
@@ -126,12 +145,12 @@ class BusyOverlay:
|
||||
pass
|
||||
self._bind_id = None
|
||||
|
||||
# ========================
|
||||
# AsyncRunner (single-loop)
|
||||
# ========================
|
||||
|
||||
class AsyncRunner:
|
||||
"""Run awaitables on the single global loop and callback on Tk main thread."""
|
||||
"""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()
|
||||
|
||||
@@ -141,8 +160,9 @@ class AsyncRunner:
|
||||
on_success: Callable[[Any], None],
|
||||
on_error: Optional[Callable[[BaseException], None]] = None,
|
||||
busy: Optional[BusyOverlay] = None,
|
||||
message: str = "Operazione in corso…",
|
||||
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)
|
||||
@@ -166,5 +186,5 @@ class AsyncRunner:
|
||||
_poll()
|
||||
|
||||
def close(self):
|
||||
# no-op: loop is global
|
||||
"""No-op kept for API compatibility with older callers."""
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user