forked from CastroFidel/winehelper
added winetricks control buttons
This commit is contained in:
@@ -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):
|
||||
|
Reference in New Issue
Block a user