"""Значок в системном трее и его меню.""" from __future__ import annotations from collections.abc import Callable from typing import Any from PySide6.QtGui import QAction, QActionGroup from PySide6.QtWidgets import QApplication, QMenu, QSystemTrayIcon from agr_assistent import APP_NAME from agr_assistent.config import ConfigError from agr_assistent.core.assistant import Assistant, AssistantState from agr_assistent.core.settings import Settings from agr_assistent.ui.chat_window import ChatWindow from agr_assistent.ui.icons import STATE_TITLES, state_icon _NOTIFICATION_PREVIEW_CHARS = 200 _NOTIFICATION_TIMEOUT_MS = 6000 class TrayIcon(QSystemTrayIcon): def __init__( self, assistant: Assistant, window: ChatWindow, settings: Settings, open_settings: Callable[[], None], hotkey: str = "", ) -> None: super().__init__(state_icon(assistant.state)) self._assistant = assistant self._window = window self._settings = settings menu = QMenu() menu.addAction("Открыть чат", window.show_and_raise) if assistant.voice_available: # Текст после табуляции QMenu выравнивает по правому краю, как подсказку клавиши label = f"Говорить\t{hotkey.title()}" if hotkey else "Говорить" menu.addAction(label, assistant.toggle_listening) if assistant.wake_word_available: phrases = ", ".join(f"«{phrase}»" for phrase in settings.config.wake_word.phrases) wake_action = QAction(f"Слушать слово активации: {phrases}", self, checkable=True) wake_action.setChecked(assistant.wake_word_enabled) # triggered — только действие пользователя: автоотключение после ошибки не сохраняем wake_action.triggered.connect(self._on_wake_word_triggered) assistant.wake_word_enabled_changed.connect(wake_action.setChecked) menu.addAction(wake_action) menu.addSeparator() provider_menu = menu.addMenu("Провайдер LLM") provider_group = QActionGroup(self) self._provider_actions: dict[str, QAction] = {} for name in assistant.provider_names: action = QAction(name, self, checkable=True) action.setChecked(name == assistant.provider_name) action.triggered.connect(lambda _checked=False, n=name: self._on_provider_triggered(n)) provider_group.addAction(action) provider_menu.addAction(action) self._provider_actions[name] = action self._speech_action = QAction("Озвучивать ответы", self, checkable=True) self._speech_action.setChecked(assistant.speech_enabled) self._speech_action.triggered.connect(self._on_speech_triggered) menu.addAction(self._speech_action) menu.addAction("Новый диалог", assistant.clear_history) menu.addAction("Настройки…", open_settings) menu.addSeparator() menu.addAction("Выход", QApplication.quit) # Меню нужно держать в атрибуте, иначе его соберёт сборщик мусора self._menu = menu self.setContextMenu(menu) self.activated.connect(self._on_activated) self.messageClicked.connect(window.show_and_raise) assistant.state_changed.connect(self._on_state_changed) assistant.provider_changed.connect(self._on_provider_changed) assistant.speech_enabled_changed.connect(self._speech_action.setChecked) assistant.reply_finished.connect(self._on_reply_finished) assistant.error_occurred.connect(self._on_error) self._on_state_changed(assistant.state) def _on_activated(self, reason: QSystemTrayIcon.ActivationReason) -> None: if reason in ( QSystemTrayIcon.ActivationReason.Trigger, QSystemTrayIcon.ActivationReason.DoubleClick, ): self._window.toggle_visibility() def _on_provider_triggered(self, name: str) -> None: self._assistant.set_provider(name) self._persist({"llm.provider": name}) def _on_speech_triggered(self, enabled: bool) -> None: self._assistant.set_speech_enabled(enabled) self._persist({"tts.enabled": enabled}) def _on_wake_word_triggered(self, enabled: bool) -> None: self._assistant.set_wake_word_enabled(enabled) self._persist({"wake_word.enabled": enabled}) def _persist(self, updates: dict[str, Any]) -> None: try: self._settings.save(updates) except (ConfigError, OSError) as exc: self._assistant.error_occurred.emit(f"Не удалось сохранить настройки: {exc}") def _on_state_changed(self, state: AssistantState) -> None: self.setIcon(state_icon(state)) self.setToolTip(f"{APP_NAME} — {STATE_TITLES[state]}") def _on_provider_changed(self, name: str) -> None: if action := self._provider_actions.get(name): action.setChecked(True) def _on_reply_finished(self, text: str) -> None: if text.strip() and not self._window.isVisible(): preview = text.strip() if len(preview) > _NOTIFICATION_PREVIEW_CHARS: preview = preview[:_NOTIFICATION_PREVIEW_CHARS].rstrip() + "…" self.showMessage( APP_NAME, preview, QSystemTrayIcon.MessageIcon.Information, _NOTIFICATION_TIMEOUT_MS ) def _on_error(self, message: str) -> None: if not self._window.isVisible(): self.showMessage( APP_NAME, message, QSystemTrayIcon.MessageIcon.Warning, _NOTIFICATION_TIMEOUT_MS )