feat: returned tray and added favorites and recent to it
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
This commit is contained in:
@@ -30,7 +30,7 @@ def main():
|
||||
|
||||
args = parse_args()
|
||||
|
||||
window = MainWindow()
|
||||
window = MainWindow(app_name=__app_name__)
|
||||
|
||||
if args.fullscreen:
|
||||
logger.info("Launching in fullscreen mode due to --fullscreen flag")
|
||||
|
@@ -34,6 +34,7 @@ from portprotonqt.localization import _, get_egs_language, read_metadata_transla
|
||||
from portprotonqt.logger import get_logger
|
||||
from portprotonqt.howlongtobeat_api import HowLongToBeat
|
||||
from portprotonqt.downloader import Downloader
|
||||
from portprotonqt.tray_manager import TrayManager
|
||||
|
||||
from PySide6.QtWidgets import (QLineEdit, QMainWindow, QStatusBar, QWidget, QVBoxLayout, QLabel, QHBoxLayout, QStackedWidget, QComboBox, QScrollArea, QSlider,
|
||||
QDialog, QFormLayout, QFrame, QGraphicsDropShadowEffect, QMessageBox, QApplication, QPushButton, QProgressBar, QCheckBox, QSizePolicy)
|
||||
@@ -52,10 +53,11 @@ class MainWindow(QMainWindow):
|
||||
update_progress = Signal(int) # Signal to update progress bar
|
||||
update_status_message = Signal(str, int) # Signal to update status message
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, app_name: str):
|
||||
super().__init__()
|
||||
# Создаём менеджер тем и читаем, какая тема выбрана
|
||||
self.theme_manager = ThemeManager()
|
||||
self.is_exiting = False
|
||||
selected_theme = read_theme_from_config()
|
||||
self.current_theme_name = selected_theme
|
||||
try:
|
||||
@@ -67,8 +69,9 @@ class MainWindow(QMainWindow):
|
||||
save_theme_to_config("standart")
|
||||
if not self.theme:
|
||||
self.theme = default_styles
|
||||
self.tray_manager = TrayManager(self, app_name, self.current_theme_name)
|
||||
self.card_width = read_card_size()
|
||||
self.setWindowTitle("PortProtonQt")
|
||||
self.setWindowTitle(app_name)
|
||||
self.setMinimumSize(800, 600)
|
||||
|
||||
self.games = []
|
||||
@@ -2266,7 +2269,9 @@ class MainWindow(QMainWindow):
|
||||
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""Завершает все дочерние процессы и сохраняет настройки при закрытии окна."""
|
||||
"""Обработчик закрытия окна: сворачивает приложение в трей, если не требуется принудительный выход."""
|
||||
if hasattr(self, 'is_exiting') and self.is_exiting:
|
||||
# Принудительное закрытие: завершаем процессы и приложение
|
||||
for proc in self.game_processes:
|
||||
try:
|
||||
parent = psutil.Process(proc.pid)
|
||||
@@ -2289,13 +2294,7 @@ class MainWindow(QMainWindow):
|
||||
|
||||
self.game_processes = [] # Очищаем список процессов
|
||||
|
||||
# Сохраняем настройки окна
|
||||
if not read_fullscreen_config():
|
||||
logger.debug(f"Saving window geometry: {self.width()}x{self.height()}")
|
||||
save_window_geometry(self.width(), self.height())
|
||||
save_card_size(self.card_width)
|
||||
|
||||
# Очищаем таймеры и другие ресурсы
|
||||
# Очищаем таймеры
|
||||
if hasattr(self, 'games_load_timer') and self.games_load_timer.isActive():
|
||||
self.games_load_timer.stop()
|
||||
if hasattr(self, 'settingsDebounceTimer') and self.settingsDebounceTimer.isActive():
|
||||
@@ -2307,5 +2306,14 @@ class MainWindow(QMainWindow):
|
||||
self.checkProcessTimer.deleteLater()
|
||||
self.checkProcessTimer = None
|
||||
|
||||
QApplication.quit()
|
||||
# Сохраняем настройки окна
|
||||
if not read_fullscreen_config():
|
||||
logger.debug(f"Saving window geometry: {self.width()}x{self.height()}")
|
||||
save_window_geometry(self.width(), self.height())
|
||||
save_card_size(self.card_width)
|
||||
|
||||
event.accept()
|
||||
else:
|
||||
# Сворачиваем в трей вместо закрытия
|
||||
self.hide()
|
||||
event.ignore()
|
||||
|
141
portprotonqt/tray_manager.py
Normal file
141
portprotonqt/tray_manager.py
Normal file
@@ -0,0 +1,141 @@
|
||||
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)
|
Reference in New Issue
Block a user