added winetricks control buttons

This commit is contained in:
Sergey Palcheh
2025-08-11 15:41:48 +06:00
parent e766b4dba2
commit 100ffa22ba

View File

@@ -6,9 +6,13 @@ import re
import shlex
import shutil
import html
import time
import json
import hashlib
from functools import partial
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,QPushButton, QLabel, QTabWidget,
QTextEdit, QFileDialog, QMessageBox, QLineEdit, QCheckBox, QStackedWidget, QScrollArea,
QGridLayout, QFrame, QDialog, QTextBrowser)
QListWidget, QListWidgetItem, QGridLayout, QFrame, QDialog, QTextBrowser)
from PyQt5.QtCore import Qt, QProcess, QSize, QTimer, QProcessEnvironment
from PyQt5.QtGui import QIcon, QFont, QTextCursor, QPixmap, QPainter
@@ -24,6 +28,516 @@ class Var:
LICENSE_FILE = os.environ.get("LICENSE_FILE")
LICENSE_AGREEMENT_FILE = os.environ.get("AGREEMENT")
class WinetricksManagerDialog(QDialog):
"""Диалог для управления компонентами Winetricks."""
INFO_TEXT = (
"Компоненты можно только установить либо переустановить.\n"
"Удаление компонентов не реализовано в Winetricks.\n"
"Для установки нового компонента: Поставьте галочку и нажмите «Применить».\n"
"Для переустановки компонента: Выделите его в списке и нажмите кнопку «Переустановить»."
)
def __init__(self, prefix_path, winetricks_path, parent=None):
super().__init__(parent)
self.prefix_path = prefix_path
self.winetricks_path = winetricks_path
self.initial_states = {}
self.apply_process = None
self.installation_finished = False
self.user_cancelled = False
self.processes = {}
self.category_statuses = {}
self.previous_tab_widget = None
self.cache_dir = os.path.join(os.path.expanduser("~"), ".cache", "winehelper", "winetricks")
os.makedirs(self.cache_dir, exist_ok=True)
self.setWindowTitle(f"Менеджер компонентов для префикса: {os.path.basename(prefix_path)}")
self.setMinimumSize(800, 500)
# Основной layout
main_layout = QVBoxLayout(self)
# Табы для категорий
self.tabs = QTabWidget()
main_layout.addWidget(self.tabs)
# Создаем табы
self.categories = {
"Библиотеки": "dlls",
"Шрифты": "fonts",
"Настройки": "settings"
}
self.list_widgets = {}
self.search_edits = {}
for display_name, internal_name in self.categories.items():
list_widget, search_edit = self._create_category_tab(display_name)
self.list_widgets[internal_name] = list_widget
self.search_edits[internal_name] = search_edit
# Лог для вывода команд
self.log_output = QTextEdit()
self.log_output.setReadOnly(True)
self.log_output.setFont(QFont('DejaVu Sans Mono', 10))
self.log_output.setMaximumHeight(150)
self.log_output.setText(self.INFO_TEXT)
main_layout.addWidget(self.log_output)
# Кнопки управления
button_layout = QHBoxLayout()
self.status_label = QLabel("Загрузка компонентов...")
button_layout.addWidget(self.status_label, 1)
self.apply_button = QPushButton("Применить")
self.apply_button.setEnabled(False)
self.apply_button.clicked.connect(self.apply_changes)
button_layout.addWidget(self.apply_button)
self.reinstall_button = QPushButton("Переустановить")
self.reinstall_button.setEnabled(False)
self.reinstall_button.clicked.connect(self.reinstall_selected)
button_layout.addWidget(self.reinstall_button)
self.close_button = QPushButton("Закрыть")
self.close_button.clicked.connect(self.close)
button_layout.addWidget(self.close_button)
main_layout.addLayout(button_layout)
# Подключаем сигнал после создания всех виджетов, чтобы избежать ошибки атрибута
self.tabs.currentChanged.connect(self.on_tab_switched)
# Загружаем данные
self.load_all_categories()
# Устанавливаем начальное состояние для отслеживания покинутой вкладки
self.previous_tab_widget = self.tabs.currentWidget()
def on_tab_switched(self, index):
"""
Обрабатывает переключение вкладок.
Если установка только что завершилась, сбрасывает лог к информационному тексту.
"""
# Очищаем поле поиска на той вкладке, которую покинули.
if self.previous_tab_widget:
search_edit = self.previous_tab_widget.findChild(QLineEdit)
if search_edit:
search_edit.clear()
if self.installation_finished:
self.log_output.setText(self.INFO_TEXT)
self.installation_finished = False
self._update_ui_state()
# Сохраняем текущую вкладку для следующего переключения
self.previous_tab_widget = self.tabs.widget(index)
def _create_category_tab(self, title):
"""Создает вкладку с поиском и списком."""
tab = QWidget()
layout = QVBoxLayout(tab)
search_edit = QLineEdit()
search_edit.setPlaceholderText("Поиск...")
layout.addWidget(search_edit)
list_widget = QListWidget()
list_widget.itemChanged.connect(self._on_item_changed)
list_widget.currentItemChanged.connect(self._update_ui_state)
layout.addWidget(list_widget)
search_edit.textChanged.connect(lambda text, lw=list_widget: self.filter_list(text, lw))
self.tabs.addTab(tab, title)
return list_widget, search_edit
def filter_list(self, text, list_widget):
"""Фильтрует элементы в списке."""
for i in range(list_widget.count()):
item = list_widget.item(i)
item.setHidden(text.lower() not in item.text().lower())
def load_all_categories(self):
"""Запускает загрузку всех категорий."""
self.loading_count = len(self.categories)
self.category_statuses = {name: "загрузка..." for name in self.categories.keys()}
for internal_name in self.categories.values():
self._start_load_process(internal_name)
def _get_cache_path(self, category):
"""Возвращает путь к файлу кэша для указанной категории."""
return os.path.join(self.cache_dir, f"{category}.json")
def _get_winetricks_hash(self):
"""Вычисляет хэш файла winetricks для проверки его обновления."""
try:
hasher = hashlib.sha256()
with open(self.winetricks_path, 'rb') as f:
while chunk := f.read(4096):
hasher.update(chunk)
return hasher.hexdigest()
except (IOError, OSError):
return None
def _start_load_process(self, category):
"""Запускает QProcess для получения списка компонентов, используя кэш."""
cache_path = self._get_cache_path(category)
cache_ttl_seconds = 86400 # 24 часа
# Попытка прочитать из кэша
if os.path.exists(cache_path):
try:
with open(cache_path, 'r', encoding='utf-8') as f:
cache_data = json.load(f)
cache_age = time.time() - cache_data.get("timestamp", 0)
winetricks_hash = self._get_winetricks_hash()
if cache_age < cache_ttl_seconds and cache_data.get("hash") == winetricks_hash:
QTimer.singleShot(0, lambda: self._on_load_finished(
category, 0, QProcess.NormalExit, from_cache=cache_data.get("output")
))
return
except (json.JSONDecodeError, IOError, KeyError):
self._log(f"--- Кэш для '{category}' поврежден, будет выполнена перезагрузка. ---")
process = QProcess(self)
self.processes[category] = process
process.setProcessChannelMode(QProcess.MergedChannels)
env = QProcessEnvironment.systemEnvironment()
env.insert("WINEPREFIX", self.prefix_path)
# Отключаем winemenubuilder, чтобы избежать зависаний, связанных с 'wineserver -w'.
# Это известная проблема при запуске winetricks из ГУИ.
process.setProcessEnvironment(env)
# Используем functools.partial для надежной привязки категории к слоту.
# Это стандартный и самый надежный способ избежать проблем с замыканием в цикле.
process.finished.connect(partial(self._on_load_finished, category))
process.start(self.winetricks_path, [category, "list"])
def _update_status_label(self):
"""Обновляет текстовую метку состояния загрузки."""
status_parts = []
for name, status in self.category_statuses.items():
status_parts.append(f"{name}: {status}")
self.status_label.setText(" | ".join(status_parts))
def _parse_winetricks_log(self):
"""Читает winetricks.log и возвращает множество установленных компонентов."""
installed_verbs = set()
log_path = os.path.join(self.prefix_path, "winetricks.log")
if not os.path.exists(log_path):
return installed_verbs
try:
with open(log_path, 'r', encoding='utf-8') as f:
for line in f:
verb = line.split('#', 1)[0].strip()
if verb:
installed_verbs.add(verb)
except Exception as e:
self._log(f"--- Предупреждение: не удалось прочитать {log_path}: {e} ---")
return installed_verbs
def _parse_winetricks_list_output(self, output, installed_verbs, list_widget):
"""Парсит вывод 'winetricks list' и заполняет QListWidget."""
# Regex, который обрабатывает строки как с префиксом статуса '[ ]', так и без него.
# 1. `(?:\[(.)]\s+)?` - опциональная группа для статуса (напр. '[x]').
# 2. `([^\s]+)` - имя компонента (без пробелов).
# 3. `(.*)` - оставшаяся часть строки (описание).
line_re = re.compile(r"^\s*(?:\[(.)]\s+)?([^\s]+)\s*(.*)")
found_items = False
for line in output.splitlines():
match = line_re.match(line)
if not match:
continue
found_items = True
_status, name, description = match.groups()
# Удаляем из описания информацию о доступности для скачивания, так как она избыточна
description = re.sub(r'\[\s*доступно для скачивания[^]]*]', '', description)
description = re.sub(r'\[\s*в кэше\s*]', '', description)
# Фильтруем служебные строки, которые могут быть ошибочно распознаны.
# Имена компонентов winetricks не содержат слэшей и не являются командами.
if '/' in name or '\\' in name or name.lower() in ('executing', 'using', 'warning:') or name.endswith(':'):
continue
is_checked = name in installed_verbs
item_text = f"{name.ljust(27)}{description.strip()}"
item = QListWidgetItem(item_text)
item.setData(Qt.UserRole, name)
item.setFont(QFont("DejaVu Sans Mono", 10))
item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
item.setCheckState(Qt.Checked if is_checked else Qt.Unchecked)
list_widget.addItem(item)
self.initial_states[name] = is_checked
return found_items
def _on_load_finished(self, category, exit_code, exit_status, from_cache=None):
"""Обрабатывает завершение загрузки списка компонентов."""
if from_cache is not None:
output = from_cache
process = None
else:
process = self.processes[category]
output = process.readAllStandardOutput().data().decode('utf-8', 'ignore')
list_widget = self.list_widgets[category]
category_display_name = next(k for k, v in self.categories.items() if v == category)
# Очищаем список перед заполнением.
list_widget.clear()
if exit_code != 0 or exit_status != QProcess.NormalExit:
error_string = process.errorString() if process else "N/A"
self._log(f"--- Ошибка загрузки категории '{category}' (код: {exit_code}) ---", "red")
self.category_statuses[category_display_name] = "ошибка"
self._update_status_label() # Показываем ошибку в статусе
if exit_status == QProcess.CrashExit:
self._log("--- Процесс winetricks завершился аварийно. ---", "red")
# По умолчанию используется "Неизвестная ошибка", которая не очень полезна.
if error_string != "Неизвестная ошибка":
self._log(f"--- Системная ошибка: {error_string} ---", "red")
self._log(output if output.strip() else "Winetricks не вернул вывод. Проверьте, что он работает корректно.")
self._log("--------------------------------------------------", "red")
else:
self.category_statuses[category_display_name] = "готово"
installed_verbs = self._parse_winetricks_log()
# Обновляем статус только если это была сетевая загрузка
if from_cache is None:
self._update_status_label()
found_items = self._parse_winetricks_list_output(output, installed_verbs, list_widget)
if from_cache is None: # Только если мы не читали из кэша
# Сохраняем успешный результат в кэш
cache_path = self._get_cache_path(category)
winetricks_hash = self._get_winetricks_hash()
if winetricks_hash:
cache_data = {
"timestamp": time.time(),
"hash": winetricks_hash,
"output": output
}
try:
with open(cache_path, 'w', encoding='utf-8') as f:
json.dump(cache_data, f, ensure_ascii=False, indent=2)
except (IOError, OSError) as e:
self._log(f"--- Не удалось сохранить кэш для '{category}': {e} ---")
if not found_items and output.strip():
self._log(f"--- Не удалось распознать вывод для категории '{category}' ---")
self._log(output)
self._log("--------------------------------------------------")
self.loading_count -= 1
if self.loading_count == 0:
self.status_label.setText("Готово.")
self._update_ui_state()
def _on_item_changed(self, item):
"""Обрабатывает изменение состояния чекбокса, предотвращая снятие галочки с установленных."""
name = item.data(Qt.UserRole)
# Если компонент был изначально установлен и пользователь пытается его снять
if name in self.initial_states and self.initial_states.get(name) is True:
if item.checkState() == Qt.Unchecked:
# Блокируем сигналы, чтобы избежать рекурсии, и возвращаем галочку на место.
list_widget = item.listWidget()
if list_widget:
list_widget.blockSignals(True)
item.setCheckState(Qt.Checked)
if list_widget:
list_widget.blockSignals(False)
self._update_ui_state()
def _update_ui_state(self, *args):
"""Централизованно обновляет состояние кнопок 'Применить' и 'Переустановить'."""
# 1. Проверяем, есть ли изменения в чекбоксах (установка новых или снятие галочек с новых)
has_changes = False
for list_widget in self.list_widgets.values():
for i in range(list_widget.count()):
item = list_widget.item(i)
name = item.data(Qt.UserRole)
if name in self.initial_states:
initial_state = self.initial_states[name]
current_state = item.checkState() == Qt.Checked
if current_state != initial_state:
has_changes = True
break
if has_changes:
break
self.apply_button.setEnabled(has_changes)
# 2. Проверяем, можно ли переустановить выбранный компонент
is_reinstallable = False
# Переустановка возможна только если нет других изменений
if not has_changes:
current_list_widget = self.tabs.currentWidget().findChild(QListWidget)
if current_list_widget:
current_item = current_list_widget.currentItem()
if current_item:
name = current_item.data(Qt.UserRole)
# Компонент можно переустановить, если он был изначально установлен
if self.initial_states.get(name, False):
is_reinstallable = True
self.reinstall_button.setEnabled(is_reinstallable)
def reinstall_selected(self):
"""Переустанавливает выбранный компонент."""
current_list_widget = self.tabs.currentWidget().findChild(QListWidget)
if not current_list_widget:
return
current_item = current_list_widget.currentItem()
if not current_item:
return
name = current_item.data(Qt.UserRole)
if not name:
return
self.log_output.setText(self.INFO_TEXT)
self.apply_button.setEnabled(False)
self.reinstall_button.setEnabled(False)
self.close_button.setEnabled(False)
# Установка будет форсированной
verbs_to_reinstall = [name]
self._start_install_process(verbs_to_reinstall)
def apply_changes(self):
"""Применяет выбранные изменения."""
# Собираем все компоненты, которые были отмечены для установки.
verbs_to_install = []
for list_widget in self.list_widgets.values():
for i in range(list_widget.count()):
item = list_widget.item(i)
name = item.data(Qt.UserRole)
if name not in self.initial_states:
continue
initial_state = self.initial_states[name]
current_state = item.checkState() == Qt.Checked
if current_state != initial_state:
verbs_to_install.append(name)
if not verbs_to_install:
QMessageBox.information(self, "Нет изменений", "Не выбрано ни одного компонента для установки.")
return
self.log_output.setText(self.INFO_TEXT)
self.apply_button.setEnabled(False)
self.reinstall_button.setEnabled(False)
self.close_button.setEnabled(False)
self._start_install_process(verbs_to_install)
def _start_install_process(self, verbs_to_install):
"""Запускает процесс установки/переустановки winetricks."""
# Добавляем флаг --force, чтобы разрешить переустановку
self._log(f"Выполнение установки: winetricks --unattended --force {' '.join(verbs_to_install)}")
self.apply_process = QProcess(self)
self.apply_process.setProcessChannelMode(QProcess.MergedChannels)
env = QProcessEnvironment.systemEnvironment()
env.insert("WINEPREFIX", self.prefix_path)
self.apply_process.setProcessEnvironment(env)
self.apply_process.readyReadStandardOutput.connect(lambda: self.log_output.append(self.apply_process.readAllStandardOutput().data().decode('utf-8', 'ignore').strip()))
self.apply_process.finished.connect(self.on_apply_finished)
self.apply_process.start(self.winetricks_path, ["--unattended", "--force"] + verbs_to_install)
def on_apply_finished(self, exit_code, exit_status):
"""Обрабатывает завершение применения изменений."""
# 1. Проверяем, была ли отмена пользователем
if self.user_cancelled:
self._log("\n=== Установка прервана пользователем. ===")
self._show_message_box("Отмена", "Установки компонентов прервана пользователем.",
QMessageBox.Warning, {"buttons": {"Да": QMessageBox.AcceptRole}})
# Сбрасываем флаг и восстанавливаем UI
self.user_cancelled = False
self.apply_button.setEnabled(True)
self.close_button.setEnabled(True)
return
# 2. Обрабатываем реальную ошибку
if exit_code != 0 or exit_status != QProcess.NormalExit:
self._log(f"\n=== Ошибка во время выполнения операций (код: {exit_code}). ===", "red")
self._show_message_box("Ошибка",
"Произошла ошибка во время выполнения операций.\n"
"Подробности смотрите в логе.",
QMessageBox.Warning,
{"buttons": {"OK": QMessageBox.AcceptRole}})
self.apply_button.setEnabled(True)
self.close_button.setEnabled(True)
return
# 3. Обрабатываем успех
self._log("\n=== Все операции успешно завершены ===")
self._show_message_box("Успех",
"Операции с компонентами были успешно выполнены.",
QMessageBox.Information,
{"buttons": {"Да": QMessageBox.AcceptRole}})
self.apply_button.setEnabled(True)
self.reinstall_button.setEnabled(False) # Сбрасываем в неактивное состояние
self.close_button.setEnabled(True)
# Очищаем все поля поиска.
for search_edit in self.search_edits.values():
search_edit.clear()
# Перезагружаем данные, чтобы обновить состояние
self.status_label.setText("Обновление данных...")
self.initial_states.clear()
self.load_all_categories()
self.installation_finished = True
def closeEvent(self, event):
"""Обрабатывает закрытие окна, чтобы предотвратить выход во время установки."""
# Проверяем, запущен ли процесс установки/переустановки
if self.apply_process and self.apply_process.state() == QProcess.Running:
reply = self._show_message_box('Подтверждение',
"Процесс установки еще не завершен. Вы уверены, что хотите прервать его?",
QMessageBox.Question,
{"buttons": {"Да": QMessageBox.YesRole, "Нет": QMessageBox.NoRole}, "default": "Нет"})
if reply == "Да":
self.user_cancelled = True
self.apply_process.terminate() # Попытка мягкого завершения
event.accept() # Разрешаем закрытие
else:
event.ignore() # Запрещаем закрытие
else:
event.accept() # Процесс не запущен, можно закрывать
def _show_message_box(self, title, text, icon, config):
"""Централизованный метод для создания и показа QMessageBox."""
msg_box = QMessageBox(self)
msg_box.setWindowTitle(title)
msg_box.setText(text)
msg_box.setIcon(icon)
buttons = {}
for btn_text, role in config.get("buttons", {}).items():
buttons[btn_text] = msg_box.addButton(btn_text, role)
default_btn_text = config.get("default")
if default_btn_text and default_btn_text in buttons:
msg_box.setDefaultButton(buttons[default_btn_text])
msg_box.exec_()
clicked_button = msg_box.clickedButton()
return clicked_button.text() if clicked_button else None
def _log(self, message, color=None):
"""Добавляет сообщение в лог с возможностью указания цвета."""
if color:
self.log_output.append(f'<span style="color:{color};">{message}</span>')
else:
self.log_output.append(message)
self.log_output.moveCursor(QTextCursor.End)
class WineHelperGUI(QMainWindow):
def __init__(self):
super().__init__()
@@ -144,6 +658,7 @@ class WineHelperGUI(QMainWindow):
if show_global:
self.backup_button.setVisible(False)
self.create_log_button.setVisible(False)
self.uninstall_button.setVisible(False)
self.current_selected_app = None
def on_tab_changed(self, index):
@@ -223,15 +738,46 @@ class WineHelperGUI(QMainWindow):
installed_action_layout.setContentsMargins(0, 0, 0, 0)
installed_action_layout.setSpacing(5)
# --- Верхний ряд кнопок ---
top_buttons_layout = QHBoxLayout()
self.run_button = QPushButton("Запустить")
self.run_button.clicked.connect(self.run_installed_app)
self.uninstall_button = QPushButton("Удалить префикс")
self.uninstall_button.clicked.connect(self.uninstall_app)
top_buttons_layout.addWidget(self.run_button)
top_buttons_layout.addWidget(self.uninstall_button)
installed_action_layout.addLayout(top_buttons_layout)
# --- Сетка с утилитами ---
utils_grid_layout = QGridLayout()
utils_grid_layout.setSpacing(5)
# Ряд 0
self.winetricks_button = QPushButton("Менеджер компонентов")
self.winetricks_button.clicked.connect(self.open_winetricks_manager)
utils_grid_layout.addWidget(self.winetricks_button, 0, 0)
self.winecfg_button = QPushButton("Редактор настроек")
self.winecfg_button.clicked.connect(lambda: self._run_wine_util('winecfg'))
utils_grid_layout.addWidget(self.winecfg_button, 0, 1)
# Ряд 1
self.regedit_button = QPushButton("Редактор реестра")
self.regedit_button.clicked.connect(lambda: self._run_wine_util('regedit'))
utils_grid_layout.addWidget(self.regedit_button, 1, 0)
self.uninstaller_button = QPushButton("Удаление программ")
self.uninstaller_button.clicked.connect(lambda: self._run_wine_util('uninstaller'))
utils_grid_layout.addWidget(self.uninstaller_button, 1, 1)
# Ряд 2
self.cmd_button = QPushButton("Командная строка")
self.cmd_button.clicked.connect(lambda: self._run_wine_util('cmd'))
utils_grid_layout.addWidget(self.cmd_button, 2, 0)
self.winefile_button = QPushButton("Файловый менеджер")
self.winefile_button.clicked.connect(lambda: self._run_wine_util('winefile'))
utils_grid_layout.addWidget(self.winefile_button, 2, 1)
installed_action_layout.addLayout(utils_grid_layout)
self.installed_action_widget.setLayout(installed_action_layout)
self.info_panel_layout.addWidget(self.installed_action_widget)
@@ -249,10 +795,16 @@ class WineHelperGUI(QMainWindow):
self.backup_button.clicked.connect(self.backup_prefix_for_app)
installed_global_layout.addWidget(self.backup_button)
self.uninstall_button = QPushButton("Удалить префикс")
self.uninstall_button.setIcon(QIcon.fromTheme("user-trash"))
self.uninstall_button.clicked.connect(self.uninstall_app)
installed_global_layout.addWidget(self.uninstall_button)
self.restore_prefix_button_panel = QPushButton("Восстановить префикс из резервной копии")
self.restore_prefix_button_panel.setIcon(QIcon.fromTheme("document-revert"))
self.restore_prefix_button_panel.clicked.connect(self.restore_prefix)
installed_global_layout.addWidget(self.restore_prefix_button_panel)
self.installed_global_action_widget.setLayout(installed_global_layout)
self.info_panel_layout.addWidget(self.installed_global_action_widget)
@@ -557,12 +1109,6 @@ class WineHelperGUI(QMainWindow):
def create_installed_tab(self):
"""Создает вкладку для отображения установленных программ в виде кнопок"""
installed_tab = QWidget()
installed_layout = QVBoxLayout()
installed_layout.setContentsMargins(0, 0, 0, 0)
installed_layout.setSpacing(5)
installed_tab.setLayout(installed_layout)
installed_tab, self.installed_scroll_layout, self.installed_search_edit, self.installed_scroll_area = self._create_searchable_grid_tab(
"Поиск установленной программы...", self.filter_installed_buttons
)
@@ -817,6 +1363,7 @@ class WineHelperGUI(QMainWindow):
self.installed_global_action_widget.setVisible(True)
self.backup_button.setVisible(True)
self.create_log_button.setVisible(True)
self.uninstall_button.setVisible(True)
self.manual_install_path_widget.setVisible(False)
except Exception as e:
@@ -861,7 +1408,7 @@ class WineHelperGUI(QMainWindow):
yes_button = QPushButton("Да")
no_button = QPushButton("Нет")
msg_box = QMessageBox()
msg_box = QMessageBox(self)
msg_box.setWindowTitle("Создание резервной копии")
msg_box.setText(
f"Будет создана резервная копия префикса '{prefix_name}'.\n"
@@ -894,7 +1441,8 @@ class WineHelperGUI(QMainWindow):
layout.addWidget(self.command_close_button)
self.command_dialog.setLayout(layout)
self.command_process = QProcess()
# Устанавливаем родителя, чтобы избежать утечек памяти
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)
@@ -937,7 +1485,8 @@ class WineHelperGUI(QMainWindow):
layout.addWidget(self.command_close_button)
self.command_dialog.setLayout(layout)
self.command_process = QProcess()
# Устанавливаем родителя, чтобы избежать утечек памяти
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_restore_finished)
@@ -955,7 +1504,7 @@ class WineHelperGUI(QMainWindow):
yes_button = QPushButton("Да")
no_button = QPushButton("Нет")
msg_box = QMessageBox()
msg_box = QMessageBox(self)
msg_box.setWindowTitle("Создание лога")
msg_box.setText(
"Приложение будет запущено в режиме отладки.\n"
@@ -969,6 +1518,72 @@ class WineHelperGUI(QMainWindow):
if msg_box.clickedButton() == yes_button:
self._run_app_launcher(debug=True)
def open_winetricks_manager(self):
"""Открывает новый диалог для управления компонентами Winetricks."""
prefix_name = self._get_prefix_name_for_selected_app()
if not prefix_name:
QMessageBox.warning(self, "Менеджер Winetricks", "Сначала выберите установленное приложение, чтобы открыть менеджер для его префикса.")
return
prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name)
if not os.path.isdir(prefix_path):
QMessageBox.critical(self, "Ошибка", f"Каталог префикса не найден:\n{prefix_path}")
return
winehelper_dir = os.path.dirname(self.winehelper_path)
winetricks_path = None
try:
# Ищем файл, который начинается с 'winetricks_'
for filename in os.listdir(winehelper_dir):
if filename.startswith("winetricks_"):
winetricks_path = os.path.join(winehelper_dir, filename)
break # Нашли, выходим из цикла
except OSError as e:
QMessageBox.critical(self, "Ошибка", f"Не удалось прочитать директорию {winehelper_dir}: {e}")
return
if not winetricks_path:
QMessageBox.critical(self, "Ошибка", f"Скрипт winetricks не найден в директории:\n{winehelper_dir}")
return
dialog = WinetricksManagerDialog(prefix_path, winetricks_path, self)
dialog.exec_()
def _run_wine_util(self, util_name):
"""Запускает стандартную утилиту Wine для выбранного префикса."""
prefix_name = self._get_prefix_name_for_selected_app()
if not prefix_name:
QMessageBox.warning(self, "Ошибка", "Сначала выберите установленное приложение.")
return
prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name)
if not os.path.isdir(prefix_path):
QMessageBox.critical(self, "Ошибка", f"Каталог префикса не найден:\n{prefix_path}")
return
env = os.environ.copy()
env["WINEPREFIX"] = prefix_path
# 'wine cmd' - особый случай, требует запуска в терминале
if util_name == 'cmd':
terminal_command = f"env WINEPREFIX='{prefix_path}' wine cmd"
try:
# x-terminal-emulator - стандартный способ вызова терминала по умолчанию
subprocess.Popen(['x-terminal-emulator', '-e', terminal_command])
except FileNotFoundError:
QMessageBox.critical(self, "Ошибка", "Не удалось найти `x-terminal-emulator`.\nУбедитесь, что у вас установлен терминал по умолчанию (например, mate-terminal или xterm).")
except Exception as e:
QMessageBox.critical(self, "Ошибка", f"Не удалось запустить терминал: {e}")
return
# Для остальных утилит
command = ['wine', util_name]
try:
subprocess.Popen(command, env=env)
except Exception as e:
QMessageBox.critical(self, "Ошибка запуска",
f"Не удалось запустить команду:\n{' '.join(command)}\n\nОшибка: {str(e)}")
def run_installed_app(self):
"""Запускает выбранное установленное приложение"""
self._run_app_launcher(debug=False)
@@ -1035,7 +1650,7 @@ class WineHelperGUI(QMainWindow):
yes_button = QPushButton("Да")
no_button = QPushButton("Нет")
msg_box = QMessageBox()
msg_box = QMessageBox(self)
msg_box.setWindowTitle('Подтверждение')
msg_box.setText(
f'Вы действительно хотите удалить "{app_name}" и все связанные файлы (включая префикс "{prefix_name}")?'
@@ -1240,7 +1855,7 @@ class WineHelperGUI(QMainWindow):
html_description = f"<p>{description}</p>"
if prog_url:
html_description += f'<p><b>Официальный сайт:</b> <a href="{prog_url}">{prog_url}</a></p>'
self.script_description.setHtml(html_description)
self.script_description.setVisible(True)
self.install_action_widget.setVisible(True)
@@ -1384,7 +1999,8 @@ class WineHelperGUI(QMainWindow):
def _start_installation(self, winehelper_path, script_path, install_file=None):
"""Запускает процесс установки"""
self.install_process = QProcess()
# Устанавливаем родителя для QProcess, чтобы он корректно удалялся вместе с диалогом
self.install_process = QProcess(self.install_dialog)
self.install_process.setProcessChannelMode(QProcess.MergedChannels)
self.install_process.setWorkingDirectory(os.path.dirname(winehelper_path))
@@ -1519,6 +2135,12 @@ class WineHelperGUI(QMainWindow):
# Кнопка прервать
self.btn_abort.setEnabled(False)
# Процесс завершен, можно запланировать его удаление и очистить ссылку,
# чтобы избежать утечек и висячих ссылок.
if self.install_process:
self.install_process.deleteLater()
self.install_process = None
def handle_install_dialog_close(self, event):
"""Обрабатывает событие закрытия диалога установки."""
# Проверяем, запущен ли еще процесс установки
@@ -1555,7 +2177,7 @@ class WineHelperGUI(QMainWindow):
if hasattr(self, 'install_process') and self.install_process and self.install_process.state() == QProcess.Running:
yes_button = QPushButton("Да")
no_button = QPushButton("Нет")
msg_box = QMessageBox()
msg_box = QMessageBox(self.install_dialog)
msg_box.setWindowTitle("Подтверждение")
msg_box.setText("Вы действительно хотите прервать установку?")
msg_box.addButton(yes_button, QMessageBox.YesRole)
@@ -1580,6 +2202,9 @@ class WineHelperGUI(QMainWindow):
self.command_log_output.append(f"\n=== Команда успешно завершена ===")
else:
self.command_log_output.append(f"\n=== Ошибка выполнения (код: {exit_code}) ===")
if self.command_process:
self.command_process.deleteLater()
self.command_process = None
self.command_close_button.setEnabled(True)
def _handle_restore_finished(self, exit_code, exit_status):
@@ -1590,6 +2215,9 @@ class WineHelperGUI(QMainWindow):
self.filter_installed_buttons()
else:
self.command_log_output.append(f"\n=== Ошибка выполнения (код: {exit_code}) ===")
if self.command_process:
self.command_process.deleteLater()
self.command_process = None
self.command_close_button.setEnabled(True)
def cleanup_process(self):