fix(tray): fenêtres invisibles et coche de profil non mise à jour

- Ajoute _present_window() (deiconify/lift/topmost/focus_force) pour que
  les Toplevel enfants d'une racine withdraw() apparaissent bien au
  premier plan sous Windows (logs, profil, ajout, authentification).
- Rend le sous-menu des profils dynamique via un générateur avec items
  radio + checked=, et appelle icon.update_menu() après changement de
  profil pour que la coche suive l'état courant.
This commit is contained in:
Hermes Agent
2026-09-18 11:00:41 +02:00
parent 2f5c428545
commit 15eb856bf2
+60 -14
View File
@@ -68,31 +68,53 @@ class TrayApp:
root = self.get_root()
root.after(0, fn)
def _present_window(self, win) -> None:
"""Force une fenêtre Tk à apparaître au premier plan.
La racine étant retirée (``withdraw``), les ``Toplevel`` enfants
peuvent apparaître cachés ou derrière les autres fenêtres sous
Windows. On les rend visibles, on leur donne le focus, et on les
place brièvement en topmost pour garantir qu'ils soient au-dessus.
"""
try:
win.deiconify()
win.lift()
win.attributes("-topmost", True)
win.after(200, lambda: win.attributes("-topmost", False))
win.focus_force()
except tk.TclError:
pass
# -- actions du menu -------------------------------------------------------
def _show_logs(self) -> None:
LogsWindow(self.get_root())
win = LogsWindow(self.get_root())
self._present_window(win)
def _edit_profile(self, name: str | None = None) -> None:
"""Ouvre l'éditeur du profil `name`, ou le profil actif si `None`."""
store = self.store
target = store.get(name) if name else store.active()
dlg = ProfileDialog(self.get_root(), existing=target, secure=self.secure)
self._present_window(dlg)
self.get_root().wait_window(dlg)
if dlg.result:
try:
store.upsert(dlg.result)
store.set_active(dlg.result.name)
self._refresh_menu()
except Exception as exc:
LOG.exception("Impossible d'enregistrer le profil : %s", exc)
def _add_profile(self) -> None:
dlg = ProfileDialog(self.get_root(), existing=None, secure=self.secure)
self._present_window(dlg)
self.get_root().wait_window(dlg)
if dlg.result:
try:
self.store.upsert(dlg.result)
LOG.info("Profil « %s » ajouté.", dlg.result.name)
self._refresh_menu()
except Exception as exc:
LOG.exception("Impossible d'ajouter le profil : %s", exc)
@@ -100,11 +122,21 @@ class TrayApp:
try:
self.store.set_active(name)
LOG.info("Profil actif : %s", name)
self._refresh_menu()
except Exception as exc:
LOG.exception("Impossible de sélectionner le profil : %s", exc)
def _refresh_menu(self) -> None:
"""Demande à pystray de reconstruire le menu (met à jour les coches)."""
if self._icon is not None:
try:
self._icon.update_menu()
except Exception:
LOG.debug("update_menu() a échoué", exc_info=True)
def _manage_auth(self) -> None:
dlg = AuthDialog(self.get_root(), self.secure)
self._present_window(dlg)
self.get_root().wait_window(dlg)
# -- construction de l'icône ----------------------------------------------
@@ -126,19 +158,10 @@ class TrayApp:
menu_items.append(
self._menu_item("Ajouter un profil", self._guard("Ajouter un profil", self._add_profile))
)
# Sous-menu des profils
profiles_sub = pystray.Menu(
*[
self._menu_item(
p.name + (" ✓" if p.name == self.store.active_name else ""),
self._guard(
f"sélection du profil « {p.name} »",
self._select_profile_action(p),
),
)
for p in self.store.get_all()
]
)
# Sous-menu des profils — construit dynamiquement à chaque affichage
# (grâce au callable), de sorte que la coche « ✓ » et la liste des
# profils reflètent toujours l'état courant après update_menu().
profiles_sub = pystray.Menu(self._profile_menu_items)
menu_items.append(self._menu_item("Modifier le profil", None, submenu=profiles_sub))
menu_items.append(pystray.Menu.SEPARATOR)
@@ -160,6 +183,29 @@ class TrayApp:
pystray.Menu(*menu_items),
)
def _profile_menu_items(self):
"""Génère dynamiquement les items du sous-menu des profils.
Appelé par pystray à chaque (re)construction du menu : la coche
(via ``checked=``, un item radio) suit donc toujours le profil actif.
"""
import pystray
for p in self.store.get_all():
yield pystray.MenuItem(
p.name,
self._guard(
f"sélection du profil « {p.name} »",
self._select_profile_action(p),
),
checked=self._make_checked(p.name),
radio=True,
)
def _make_checked(self, profile_name: str) -> Callable:
"""Retourne un prédicat évalué à l'affichage du menu."""
return lambda item: self.store.active_name == profile_name
def _menu_item(self, text: str, action, submenu=None):
import pystray