142 lines
6.4 KiB
Python
142 lines
6.4 KiB
Python
import sys
|
||
from PySide6.QtWidgets import QSystemTrayIcon, QMenu
|
||
from PySide6.QtGui import QIcon, QAction
|
||
from portprotonqt.logger import get_logger
|
||
from portprotonqt.theme_manager import ThemeManager
|
||
import portprotonqt.themes.standart.styles as default_styles
|
||
from portprotonqt.localization import _
|
||
from portprotonqt.config_utils import read_favorites, read_theme_from_config
|
||
|
||
logger = get_logger(__name__)
|
||
|
||
class TrayManager:
|
||
"""Модуль управления системным треем для PortProtonQt.
|
||
|
||
Обеспечивает:
|
||
- Показ/скрытие главного окна по клику на иконку трея.
|
||
- Контекстное меню с опциями: Show/Hide (переключается в зависимости от состояния окна),
|
||
Favorites (быстрый запуск избранных игр), Recent Games (быстрый запуск недавних игр), Exit.
|
||
- Меню Favorites и Recent Games динамически заполняются при показе (через aboutToShow).
|
||
- При закрытии окна (крестик) приложение сворачивается в трей, а не закрывается.
|
||
Полное закрытие только через Exit в меню.
|
||
"""
|
||
|
||
def __init__(self, main_window, app_name: str | None = None, theme=None):
|
||
self.app_name = app_name if app_name is not None else "PortProtonQt"
|
||
self.theme_manager = ThemeManager()
|
||
self.theme = theme if theme is not None else default_styles
|
||
self.current_theme_name = read_theme_from_config()
|
||
self.main_window = main_window
|
||
self.tray_icon = QSystemTrayIcon(self.main_window)
|
||
|
||
icon = self.theme_manager.get_icon("portproton", self.current_theme_name)
|
||
if isinstance(icon, str):
|
||
icon = QIcon(icon)
|
||
elif icon is None:
|
||
icon = QIcon()
|
||
self.tray_icon.setIcon(icon)
|
||
|
||
self.tray_icon.activated.connect(self.toggle_window)
|
||
self.tray_icon.setToolTip(self.app_name)
|
||
|
||
# Контекстное меню
|
||
self.tray_menu = QMenu()
|
||
self.toggle_action = QAction(_("Show"), self.main_window)
|
||
self.toggle_action.triggered.connect(self.toggle_window_action)
|
||
|
||
# Подменю для избранных игр
|
||
self.favorites_menu = QMenu(_("Favorites"))
|
||
self.favorites_menu.aboutToShow.connect(self.populate_favorites_menu)
|
||
|
||
# Подменю для недавних игр (топ-5 по последнему запуску)
|
||
self.recent_menu = QMenu(_("Recent Games"))
|
||
self.recent_menu.aboutToShow.connect(self.populate_recent_menu)
|
||
|
||
# Добавляем действия в меню
|
||
self.tray_menu.addAction(self.toggle_action)
|
||
self.tray_menu.addSeparator()
|
||
self.tray_menu.addMenu(self.favorites_menu)
|
||
self.tray_menu.addMenu(self.recent_menu)
|
||
self.tray_menu.addSeparator()
|
||
exit_action = QAction(_("Exit"), self.main_window)
|
||
exit_action.triggered.connect(self.force_exit)
|
||
self.tray_menu.addAction(exit_action)
|
||
|
||
self.tray_menu.aboutToShow.connect(self.update_toggle_action)
|
||
|
||
self.tray_icon.setContextMenu(self.tray_menu)
|
||
self.tray_icon.show()
|
||
|
||
# Флаг для принудительного выхода
|
||
self.main_window.is_exiting = False
|
||
|
||
def update_toggle_action(self):
|
||
"""Update toggle_action text based on window visibility."""
|
||
if self.main_window.isVisible():
|
||
self.toggle_action.setText(_("Hide"))
|
||
else:
|
||
self.toggle_action.setText(_("Show"))
|
||
|
||
def toggle_window(self, reason):
|
||
"""Переключает видимость окна по клику на иконку трея."""
|
||
if reason == QSystemTrayIcon.ActivationReason.Trigger:
|
||
self.toggle_window_action()
|
||
|
||
def toggle_window_action(self):
|
||
"""Toggle window visibility and update action text."""
|
||
if self.main_window.isVisible():
|
||
self.main_window.hide()
|
||
else:
|
||
self.main_window.show()
|
||
self.main_window.raise_()
|
||
self.main_window.activateWindow()
|
||
|
||
def populate_favorites_menu(self):
|
||
"""Динамически заполняет меню избранных игр с указанием источника."""
|
||
self.favorites_menu.clear()
|
||
favorites = read_favorites()
|
||
if not favorites:
|
||
no_fav_action = QAction(_("No favorites"), self.main_window)
|
||
no_fav_action.setEnabled(False)
|
||
self.favorites_menu.addAction(no_fav_action)
|
||
return
|
||
|
||
game_map = {game[0]: (game[4], game[12]) for game in self.main_window.games}
|
||
|
||
for fav in sorted(favorites):
|
||
game_data = game_map.get(fav)
|
||
if game_data:
|
||
exec_line, source = game_data
|
||
action_text = f"{fav} ({source})"
|
||
action = QAction(action_text, self.main_window)
|
||
action.triggered.connect(lambda checked=False, el=exec_line: self.main_window.toggleGame(el))
|
||
self.favorites_menu.addAction(action)
|
||
else:
|
||
logger.warning(f"Exec line not found for favorite: {fav}")
|
||
|
||
def populate_recent_menu(self):
|
||
"""Динамически заполняет меню недавних игр (топ-5 по timestamp) с указанием источника."""
|
||
self.recent_menu.clear()
|
||
if not self.main_window.games:
|
||
no_recent_action = QAction(_("No recent games"), self.main_window)
|
||
no_recent_action.setEnabled(False)
|
||
self.recent_menu.addAction(no_recent_action)
|
||
return
|
||
|
||
recent_games = sorted(self.main_window.games, key=lambda g: g[10], reverse=True)[:5]
|
||
|
||
for game in recent_games:
|
||
game_name = game[0]
|
||
exec_line = game[4]
|
||
source = game[12]
|
||
action_text = f"{game_name} ({source})"
|
||
action = QAction(action_text, self.main_window)
|
||
action.triggered.connect(lambda checked=False, el=exec_line: self.main_window.toggleGame(el))
|
||
self.recent_menu.addAction(action)
|
||
|
||
def force_exit(self):
|
||
"""Принудительно закрывает приложение."""
|
||
self.main_window.is_exiting = True
|
||
self.main_window.close()
|
||
sys.exit(0)
|