forked from CastroFidel/winehelper
Merge branch 'minergenon-devel'
This commit is contained in:
@@ -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,50 @@ 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)
|
||||
|
||||
toggle_visibility_action = self.tray_menu.addAction("Показать/Скрыть")
|
||||
toggle_visibility_action.triggered.connect(self.toggle_visibility)
|
||||
self.tray_menu.addSeparator()
|
||||
|
||||
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):
|
||||
"""Переключает видимость главного окна."""
|
||||
if self.isVisible() and self.isActiveWindow():
|
||||
self.hide()
|
||||
else:
|
||||
# Сначала скрываем, чтобы "сбросить" состояние, затем активируем.
|
||||
# Это помогает обойти проблемы с фокусом и переключением рабочих столов.
|
||||
self.hide()
|
||||
self.activate()
|
||||
|
||||
def add_tab(self, widget, title):
|
||||
"""Добавляет вкладку в кастомный TabBar и страницу в StackedWidget."""
|
||||
self.tab_bar.addTab(title)
|
||||
@@ -2097,6 +2142,13 @@ class WineHelperGUI(QMainWindow):
|
||||
self.created_prefix_selector.currentIndexChanged.connect(self.on_created_prefix_selected)
|
||||
selector_layout.addWidget(self.created_prefix_selector, 1)
|
||||
|
||||
self.create_base_pfx_button = QPushButton()
|
||||
self.create_base_pfx_button.setIcon(QIcon.fromTheme("document-export"))
|
||||
self.create_base_pfx_button.setToolTip("Создать шаблон из выбранного префикса (для опытных пользователей)")
|
||||
self.create_base_pfx_button.setEnabled(False)
|
||||
self.create_base_pfx_button.clicked.connect(self.create_base_prefix_from_selected)
|
||||
selector_layout.addWidget(self.create_base_pfx_button)
|
||||
|
||||
self.delete_prefix_button = QPushButton()
|
||||
self.delete_prefix_button.setIcon(QIcon.fromTheme("user-trash"))
|
||||
self.delete_prefix_button.setToolTip("Удалить выбранный префикс")
|
||||
@@ -2293,6 +2345,7 @@ class WineHelperGUI(QMainWindow):
|
||||
self.current_managed_prefix_name = None
|
||||
self._setup_prefix_management_panel(None)
|
||||
self.delete_prefix_button.setEnabled(False)
|
||||
self.create_base_pfx_button.setEnabled(False)
|
||||
else:
|
||||
# Прокручиваем к выбранному элементу, чтобы он был виден в списке
|
||||
self.created_prefix_selector.view().scrollTo(
|
||||
@@ -2302,6 +2355,7 @@ class WineHelperGUI(QMainWindow):
|
||||
self.current_managed_prefix_name = prefix_name
|
||||
self._setup_prefix_management_panel(prefix_name)
|
||||
self.delete_prefix_button.setEnabled(True)
|
||||
self.create_base_pfx_button.setEnabled(True)
|
||||
|
||||
def delete_selected_prefix(self):
|
||||
"""Удаляет префикс, выбранный в выпадающем списке на вкладке 'Менеджер префиксов'."""
|
||||
@@ -2366,6 +2420,50 @@ class WineHelperGUI(QMainWindow):
|
||||
else:
|
||||
QMessageBox.critical(self, "Ошибка удаления", f"Не удалось удалить префикс '{prefix_name}'.\nПодробности смотрите в логе.")
|
||||
|
||||
def create_base_prefix_from_selected(self):
|
||||
"""Создает шаблон префикса из выбранного в выпадающем списке."""
|
||||
prefix_name = self.current_managed_prefix_name
|
||||
if not prefix_name:
|
||||
return
|
||||
|
||||
msg_box = QMessageBox(self)
|
||||
msg_box.setIcon(QMessageBox.Question)
|
||||
msg_box.setWindowTitle("Создание шаблона префикса")
|
||||
msg_box.setText(
|
||||
f"Будет создан 'шаблон' из префикса '{prefix_name}'.\n"
|
||||
"Это продвинутая функция для создания базовых архивов префиксов.\n\n"
|
||||
"Продолжить?"
|
||||
)
|
||||
|
||||
yes_button = msg_box.addButton("Да, создать", QMessageBox.YesRole)
|
||||
no_button = msg_box.addButton("Нет", QMessageBox.NoRole)
|
||||
msg_box.setDefaultButton(no_button)
|
||||
|
||||
msg_box.exec_()
|
||||
|
||||
if msg_box.clickedButton() != yes_button:
|
||||
return
|
||||
|
||||
self.command_dialog = QDialog(self)
|
||||
self.command_dialog.setWindowTitle(f"Создание шаблона: {prefix_name}")
|
||||
self.command_dialog.setMinimumSize(750, 400)
|
||||
self.command_dialog.setModal(True)
|
||||
self.command_dialog.setWindowFlags(self.command_dialog.windowFlags() & ~Qt.WindowCloseButtonHint)
|
||||
|
||||
layout = QVBoxLayout()
|
||||
self.command_log_output = QTextEdit()
|
||||
self.command_log_output.setReadOnly(True)
|
||||
layout.addWidget(self.command_log_output)
|
||||
|
||||
self.command_close_button = QPushButton("Закрыть")
|
||||
self.command_close_button.setEnabled(False)
|
||||
self.command_close_button.clicked.connect(self.command_dialog.close)
|
||||
layout.addWidget(self.command_close_button)
|
||||
self.command_dialog.setLayout(layout)
|
||||
|
||||
self._run_simple_command("create-base-pfx", [prefix_name])
|
||||
self.command_dialog.exec_()
|
||||
|
||||
def _setup_prefix_management_panel(self, prefix_name):
|
||||
"""Настраивает панель управления префиксом на основе текущего состояния."""
|
||||
is_prefix_selected = bool(prefix_name)
|
||||
@@ -2585,11 +2683,12 @@ class WineHelperGUI(QMainWindow):
|
||||
# Для удаления лицензия не нужна, запускаем сразу.
|
||||
self.run_component_install_command(prefix_name, command, version)
|
||||
else:
|
||||
# Установка: сначала показываем лицензионное соглашение.
|
||||
if not self._show_license_agreement_dialog():
|
||||
return # Пользователь отклонил лицензию
|
||||
# Установка: для DXVK и VKD3D лицензию не показываем.
|
||||
if component not in ['dxvk', 'vkd3d-proton']:
|
||||
if not self._show_license_agreement_dialog():
|
||||
return # Пользователь отклонил лицензию
|
||||
|
||||
# Если лицензия принята, запускаем установку.
|
||||
# Запускаем установку.
|
||||
self.run_component_install_command(prefix_name, command, version)
|
||||
|
||||
def open_wine_version_manager(self):
|
||||
@@ -2620,9 +2719,6 @@ class WineHelperGUI(QMainWindow):
|
||||
new_version = dialog.selected_version
|
||||
new_version_display = dialog.selected_display_text
|
||||
|
||||
if not self._show_license_agreement_dialog():
|
||||
return # Пользователь отклонил лицензию
|
||||
|
||||
self.run_change_wine_version_command(prefix_name, new_version, new_version_display)
|
||||
|
||||
def run_change_wine_version_command(self, prefix_name, new_version, new_version_display):
|
||||
@@ -2743,17 +2839,43 @@ class WineHelperGUI(QMainWindow):
|
||||
QMessageBox.warning(self, "Ошибка", "Сначала выберите префикс.")
|
||||
return
|
||||
|
||||
current_associations = self._get_prefix_component_version(prefix_name, "WH_XDG_OPEN") or ""
|
||||
current_associations = self._get_prefix_component_version(prefix_name, "WH_XDG_OPEN") or "0"
|
||||
|
||||
dialog = FileAssociationsDialog(current_associations, self)
|
||||
dialog = FileAssociationsDialog(current_associations if current_associations != "0" else "", self)
|
||||
if dialog.exec_() == QDialog.Accepted:
|
||||
new_associations = dialog.new_associations
|
||||
|
||||
# Запускаем обновление, только если значение изменилось
|
||||
if new_associations != current_associations:
|
||||
if new_associations != (current_associations if current_associations != "0" else "0"):
|
||||
self.run_update_associations_command(prefix_name, new_associations)
|
||||
|
||||
def run_update_associations_command(self, prefix_name, new_associations):
|
||||
"""Выполняет команду обновления ассоциаций файлов."""
|
||||
# --- Прямое редактирование last.conf, чтобы обойти перезапись переменных в winehelper ---
|
||||
last_conf_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name, "last.conf")
|
||||
if not os.path.exists(last_conf_path):
|
||||
QMessageBox.critical(self, "Ошибка", f"Файл конфигурации last.conf не найден для префикса '{prefix_name}'.")
|
||||
return
|
||||
|
||||
try:
|
||||
with open(last_conf_path, 'r', encoding='utf-8') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
updated = False
|
||||
for i, line in enumerate(lines):
|
||||
if line.strip().startswith("export WH_XDG_OPEN="):
|
||||
lines[i] = f'export WH_XDG_OPEN="{new_associations}"\n'
|
||||
updated = True
|
||||
break
|
||||
if not updated:
|
||||
lines.append(f'export WH_XDG_OPEN="{new_associations}"\n')
|
||||
|
||||
with open(last_conf_path, 'w', encoding='utf-8') as f:
|
||||
f.writelines(lines)
|
||||
except IOError as e:
|
||||
QMessageBox.critical(self, "Ошибка записи", f"Не удалось обновить файл last.conf: {e}")
|
||||
return
|
||||
|
||||
prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name)
|
||||
|
||||
self.command_dialog = QDialog(self)
|
||||
@@ -2779,16 +2901,14 @@ class WineHelperGUI(QMainWindow):
|
||||
self.command_process.readyReadStandardOutput.connect(self._handle_command_output)
|
||||
self.command_process.finished.connect(
|
||||
lambda exit_code, exit_status: self._handle_component_install_finished(
|
||||
prefix_name, exit_code, exit_status
|
||||
)
|
||||
)
|
||||
prefix_name, exit_code, exit_status))
|
||||
|
||||
env = QProcessEnvironment.systemEnvironment()
|
||||
env.insert("WINEPREFIX", prefix_path)
|
||||
# Устанавливаем новую переменную окружения для скрипта
|
||||
env.insert("WH_XDG_OPEN", new_associations)
|
||||
# Переменная WH_XDG_OPEN теперь читается из измененного last.conf
|
||||
self.command_process.setProcessEnvironment(env)
|
||||
|
||||
# Вызываем init-prefix, который теперь прочитает правильное значение из last.conf
|
||||
args = ["init-prefix"]
|
||||
self.command_log_output.append(f"Выполнение: {shlex.quote(self.winehelper_path)} {' '.join(shlex.quote(a) for a in args)}")
|
||||
self.command_process.start(self.winehelper_path, args)
|
||||
@@ -3666,8 +3786,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('Подтверждение выхода')
|
||||
@@ -3683,18 +3809,27 @@ class WineHelperGUI(QMainWindow):
|
||||
msg_box.exec_()
|
||||
|
||||
if msg_box.clickedButton() == yes_button:
|
||||
# Корректно завершаем все дочерние процессы
|
||||
for desktop_path, process in list(self.running_apps.items()):
|
||||
if process.state() == QProcess.Running:
|
||||
print(f"Завершение процесса для {desktop_path}...")
|
||||
process.terminate()
|
||||
if not process.waitForFinished(2000): # Ждем 2 сек
|
||||
process.kill() # Если не закрылся, убиваем
|
||||
# Отключаем обработчики сигналов от всех запущенных процессов,
|
||||
# так как мы собираемся их принудительно завершить и выйти.
|
||||
# Это предотвращает ошибку RuntimeError при закрытии.
|
||||
for process in self.running_apps.values():
|
||||
process.finished.disconnect()
|
||||
|
||||
# Используем встроенную команду killall для надежного завершения всех процессов wine
|
||||
print("Завершение всех запущенных приложений через 'winehelper killall'...")
|
||||
# Используем 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):
|
||||
"""Удаляет выбранное установленное приложение и его префикс"""
|
||||
@@ -4325,13 +4460,22 @@ class WineHelperGUI(QMainWindow):
|
||||
self.install_process.terminate()
|
||||
|
||||
def _handle_command_output(self):
|
||||
"""Обрабатывает вывод для диалога команды"""
|
||||
"""Обрабатывает вывод для общих команд в модальном диалоге."""
|
||||
if hasattr(self, 'command_process') and self.command_process:
|
||||
output = self.command_process.readAllStandardOutput().data().decode('utf-8', errors='ignore').strip()
|
||||
# Используем readAll, чтобы получить и stdout, и stderr
|
||||
output_bytes = self.command_process.readAll()
|
||||
output = output_bytes.data().decode('utf-8', errors='ignore').strip()
|
||||
if output and hasattr(self, 'command_log_output'):
|
||||
self.command_log_output.append(output)
|
||||
QApplication.processEvents()
|
||||
|
||||
def _run_simple_command(self, command, args=None):
|
||||
"""Запускает простую команду winehelper и выводит лог."""
|
||||
self.command_process = QProcess(self.command_dialog)
|
||||
self.command_process.setProcessChannelMode(QProcess.MergedChannels)
|
||||
self.command_process.readyReadStandardOutput.connect(self._handle_command_output)
|
||||
self.command_process.finished.connect(self._handle_command_finished)
|
||||
self.command_process.start(self.winehelper_path, [command] + (args or []))
|
||||
def _handle_command_finished(self, exit_code, exit_status):
|
||||
"""Обрабатывает завершение для диалога команды"""
|
||||
if exit_code == 0:
|
||||
@@ -4432,6 +4576,8 @@ def main():
|
||||
# Сохраняем ссылку на сервер, чтобы он не был удален сборщиком мусора
|
||||
window.server = server
|
||||
window.show()
|
||||
# Создаем иконку в системном трее после создания окна
|
||||
window.create_tray_icon()
|
||||
return app.exec_()
|
||||
|
||||
return 1
|
||||
|
Reference in New Issue
Block a user