Files
PortProtonQt/portprotonqt/tray_manager.py
2025-08-28 17:12:31 +05:00

209 lines
9.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import sys
import subprocess
from PySide6.QtWidgets import QSystemTrayIcon, QMenu, QApplication
from PySide6.QtGui import QIcon, QAction
from PySide6.QtCore import QTimer
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, save_theme_to_config
logger = get_logger(__name__)
class TrayManager:
"""Модуль управления системным треем для PortProtonQt.
Обеспечивает:
- Показ/скрытие главного окна по двойному клику на иконку трея.
- Контекстное меню с опциями: Show/Hide (переключается в зависимости от состояния окна),
Favorites (быстрый запуск избранных игр), Recent Games (быстрый запуск недавних игр), Themes (быстрая смена тем), Exit.
- Меню Favorites, Recent Games и Themes динамически заполняются при показе (через 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.handle_tray_click)
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)
# Подменю для недавних игр
self.recent_menu = QMenu(_("Recent Games"))
self.recent_menu.aboutToShow.connect(self.populate_recent_menu)
# Подменю для тем
self.themes_menu = QMenu(_("Themes"))
self.themes_menu.aboutToShow.connect(self.populate_themes_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.addMenu(self.themes_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
# Переменные для отслеживания двойного клика
self.click_count = 0
self.click_timer = QTimer()
self.click_timer.setSingleShot(True)
self.click_timer.timeout.connect(self.reset_click_count)
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 handle_tray_click(self, reason):
"""Обрабатывает клики по иконке трея, отслеживая двойной клик."""
if reason == QSystemTrayIcon.ActivationReason.Trigger:
self.click_count += 1
if self.click_count == 1:
# Запускаем таймер для ожидания второго клика (300 мс - стандартное время для двойного клика)
self.click_timer.start(300)
elif self.click_count == 2:
# Двойной клик зафиксирован
self.click_timer.stop()
self.toggle_window_action()
self.click_count = 0
def reset_click_count(self):
"""Сбрасывает счетчик кликов, если таймер истек."""
self.click_count = 0
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 populate_themes_menu(self):
"""Динамически заполняет меню тем, позволяя переключать доступные темы."""
self.themes_menu.clear()
available_themes = self.theme_manager.get_available_themes()
current_theme = read_theme_from_config()
for theme_name in sorted(available_themes):
action = QAction(theme_name, self.main_window)
action.setCheckable(True)
action.setChecked(theme_name == current_theme)
action.triggered.connect(lambda checked=False, tn=theme_name: self.switch_theme(tn))
self.themes_menu.addAction(action)
def switch_theme(self, theme_name: str):
"""Сохраняет выбранную тему и перезапускает приложение для применения изменений."""
try:
# Сохраняем новую тему в конфигурации
save_theme_to_config(theme_name)
logger.info(f"Saved theme {theme_name}, restarting application to apply changes")
# Получаем текущий исполняемый файл и аргументы
executable = sys.executable
args = sys.argv
# Закрываем текущее приложение
self.main_window.is_exiting = True
QApplication.quit()
# Перезапускаем приложение
subprocess.Popen([executable] + args)
except Exception as e:
logger.error(f"Failed to switch theme to {theme_name}: {e}")
# В случае ошибки сохраняем стандартную тему
save_theme_to_config("standart")
# Перезапускаем приложение с дефолтной темой
executable = sys.executable
args = sys.argv
self.main_window.is_exiting = True
QApplication.quit()
subprocess.Popen([executable] + args)
def force_exit(self):
"""Принудительно закрывает приложение."""
self.main_window.is_exiting = True
self.main_window.close()
sys.exit(0)