From 15eb856bf2c8fa897584ae86941ca74296b9ea3c Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Fri, 18 Sep 2026 11:00:41 +0200 Subject: [PATCH] =?UTF-8?q?fix(tray):=20fen=C3=AAtres=20invisibles=20et=20?= =?UTF-8?q?coche=20de=20profil=20non=20mise=20=C3=A0=20jour?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- src/ai_typewriter/tray.py | 74 +++++++++++++++++++++++++++++++-------- 1 file changed, 60 insertions(+), 14 deletions(-) diff --git a/src/ai_typewriter/tray.py b/src/ai_typewriter/tray.py index acbd17b..7e4f026 100644 --- a/src/ai_typewriter/tray.py +++ b/src/ai_typewriter/tray.py @@ -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