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