From 553d427d66b89a43f9bb2acdf4f79b4217a4bbef Mon Sep 17 00:00:00 2001 From: Sergey Palcheh Date: Sun, 28 Sep 2025 21:26:39 +0600 Subject: [PATCH] added a gui tray --- winehelper_gui.py | 67 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 60 insertions(+), 7 deletions(-) diff --git a/winehelper_gui.py b/winehelper_gui.py index 6d383af..7c5584b 100644 --- a/winehelper_gui.py +++ b/winehelper_gui.py @@ -10,11 +10,11 @@ import time import json import hashlib from functools import partial -from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,QPushButton, QLabel, QTabWidget, QTabBar, +from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QTabWidget, QTabBar, QTextEdit, QFileDialog, QMessageBox, QLineEdit, QCheckBox, QStackedWidget, QScrollArea, QFormLayout, QGroupBox, QRadioButton, QComboBox, - QListWidget, QListWidgetItem, QGridLayout, QFrame, QDialog, QTextBrowser, QInputDialog, QDialogButtonBox) + QListWidget, QListWidgetItem, QGridLayout, QFrame, QDialog, QTextBrowser, QInputDialog, QDialogButtonBox, QSystemTrayIcon, QMenu) from PyQt5.QtCore import Qt, QProcess, QSize, QTimer, QProcessEnvironment, QPropertyAnimation, QEasingCurve -from PyQt5.QtGui import QIcon, QFont, QTextCursor, QPixmap, QPainter, QDesktopServices +from PyQt5.QtGui import QIcon, QFont, QTextCursor, QPixmap, QPainter, QCursor from PyQt5.QtNetwork import QLocalServer, QLocalSocket @@ -1581,6 +1581,7 @@ class WineHelperGUI(QMainWindow): self.current_managed_prefix_name = None # Имя префикса, выбранного в выпадающем списке self.prefixes_before_install = set() + self.is_quitting = False # Флаг для корректного выхода из приложения self.command_output_buffer = "" self.command_last_line_was_progress = False # Создаем главный виджет и layout @@ -1647,6 +1648,39 @@ class WineHelperGUI(QMainWindow): self.raise_() self.activateWindow() + def create_tray_icon(self): + """Создает и настраивает иконку в системном трее.""" + if not QSystemTrayIcon.isSystemTrayAvailable(): + print("Системный трей не доступен.") + return + + self.tray_icon = QSystemTrayIcon(self) + + icon_path = Var.WH_ICON_PATH + if icon_path and os.path.exists(icon_path): + pixmap = QPixmap(icon_path) + if not pixmap.isNull(): + self.tray_icon.setIcon(QIcon(pixmap)) + + # Создаем и сохраняем меню как атрибут класса, чтобы оно не удалялось + self.tray_menu = QMenu(self) + quit_action = self.tray_menu.addAction("Выход") + quit_action.triggered.connect(self.quit_application) + + self.tray_icon.activated.connect(self.on_tray_icon_activated) + self.tray_icon.show() + + def on_tray_icon_activated(self, reason): + """Обрабатывает клики по иконке в трее.""" + # Показываем меню при левом клике + if reason == QSystemTrayIcon.Trigger: + # Получаем позицию курсора и показываем меню + self.tray_menu.popup(QCursor.pos()) + + def toggle_visibility(self): + """Переключает видимость главного окна.""" + self.setVisible(not self.isVisible()) + def add_tab(self, widget, title): """Добавляет вкладку в кастомный TabBar и страницу в StackedWidget.""" self.tab_bar.addTab(title) @@ -3741,8 +3775,14 @@ class WineHelperGUI(QMainWindow): QMessageBox.critical(self, "Ошибка", f"Не удалось обработать команду запуска:\n{command_str}\n\nОшибка: {str(e)}") + def quit_application(self): + """Инициирует процесс выхода из приложения.""" + self.is_quitting = True + self.close() # Инициируем событие закрытия, которое будет обработано в closeEvent + def closeEvent(self, event): """Обрабатывает событие закрытия главного окна.""" + # Теперь любое закрытие окна (крестик или выход из меню) инициирует выход if self.running_apps: msg_box = QMessageBox(self) msg_box.setWindowTitle('Подтверждение выхода') @@ -3758,16 +3798,27 @@ class WineHelperGUI(QMainWindow): msg_box.exec_() if msg_box.clickedButton() == yes_button: + # Отключаем обработчики сигналов от всех запущенных процессов, + # так как мы собираемся их принудительно завершить и выйти. + # Это предотвращает ошибку RuntimeError при закрытии. + for process in self.running_apps.values(): + process.finished.disconnect() + # Используем встроенную команду killall для надежного завершения всех процессов wine print("Завершение всех запущенных приложений через 'winehelper killall'...") - kill_proc = QProcess() - kill_proc.start(self.winehelper_path, ["killall"]) - kill_proc.waitForFinished(5000) # Даем до 5 секунд на завершение + # Используем subprocess.run, который дождется завершения команды + subprocess.run([self.winehelper_path, "killall"], check=False, capture_output=True) + + # Принудительно дожидаемся завершения всех дочерних процессов + for process in self.running_apps.values(): + process.waitForFinished(5000) # Ждем до 5 секунд + + QApplication.instance().quit() event.accept() else: event.ignore() else: - super().closeEvent(event) + QApplication.instance().quit() # Если нет запущенных приложений, просто выходим def uninstall_app(self): """Удаляет выбранное установленное приложение и его префикс""" @@ -4514,6 +4565,8 @@ def main(): # Сохраняем ссылку на сервер, чтобы он не был удален сборщиком мусора window.server = server window.show() + # Создаем иконку в системном трее после создания окна + window.create_tray_icon() return app.exec_() return 1