devel #49
@@ -6,9 +6,13 @@ import re
|
|||||||
import shlex
|
import shlex
|
||||||
import shutil
|
import shutil
|
||||||
import html
|
import html
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import hashlib
|
||||||
|
from functools import partial
|
||||||
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,QPushButton, QLabel, QTabWidget,
|
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,QPushButton, QLabel, QTabWidget,
|
||||||
QTextEdit, QFileDialog, QMessageBox, QLineEdit, QCheckBox, QStackedWidget, QScrollArea,
|
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.QtCore import Qt, QProcess, QSize, QTimer, QProcessEnvironment
|
||||||
from PyQt5.QtGui import QIcon, QFont, QTextCursor, QPixmap, QPainter
|
from PyQt5.QtGui import QIcon, QFont, QTextCursor, QPixmap, QPainter
|
||||||
|
|
||||||
@@ -24,6 +28,516 @@ class Var:
|
|||||||
LICENSE_FILE = os.environ.get("LICENSE_FILE")
|
LICENSE_FILE = os.environ.get("LICENSE_FILE")
|
||||||
LICENSE_AGREEMENT_FILE = os.environ.get("AGREEMENT")
|
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):
|
class WineHelperGUI(QMainWindow):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@@ -144,6 +658,7 @@ class WineHelperGUI(QMainWindow):
|
|||||||
if show_global:
|
if show_global:
|
||||||
self.backup_button.setVisible(False)
|
self.backup_button.setVisible(False)
|
||||||
self.create_log_button.setVisible(False)
|
self.create_log_button.setVisible(False)
|
||||||
|
self.uninstall_button.setVisible(False)
|
||||||
self.current_selected_app = None
|
self.current_selected_app = None
|
||||||
|
|
||||||
def on_tab_changed(self, index):
|
def on_tab_changed(self, index):
|
||||||
@@ -223,15 +738,46 @@ class WineHelperGUI(QMainWindow):
|
|||||||
installed_action_layout.setContentsMargins(0, 0, 0, 0)
|
installed_action_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
installed_action_layout.setSpacing(5)
|
installed_action_layout.setSpacing(5)
|
||||||
|
|
||||||
|
# --- Верхний ряд кнопок ---
|
||||||
top_buttons_layout = QHBoxLayout()
|
top_buttons_layout = QHBoxLayout()
|
||||||
self.run_button = QPushButton("Запустить")
|
self.run_button = QPushButton("Запустить")
|
||||||
self.run_button.clicked.connect(self.run_installed_app)
|
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.run_button)
|
||||||
top_buttons_layout.addWidget(self.uninstall_button)
|
|
||||||
installed_action_layout.addLayout(top_buttons_layout)
|
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.installed_action_widget.setLayout(installed_action_layout)
|
||||||
self.info_panel_layout.addWidget(self.installed_action_widget)
|
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)
|
self.backup_button.clicked.connect(self.backup_prefix_for_app)
|
||||||
installed_global_layout.addWidget(self.backup_button)
|
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 = QPushButton("Восстановить префикс из резервной копии")
|
||||||
self.restore_prefix_button_panel.setIcon(QIcon.fromTheme("document-revert"))
|
self.restore_prefix_button_panel.setIcon(QIcon.fromTheme("document-revert"))
|
||||||
self.restore_prefix_button_panel.clicked.connect(self.restore_prefix)
|
self.restore_prefix_button_panel.clicked.connect(self.restore_prefix)
|
||||||
installed_global_layout.addWidget(self.restore_prefix_button_panel)
|
installed_global_layout.addWidget(self.restore_prefix_button_panel)
|
||||||
|
|
||||||
self.installed_global_action_widget.setLayout(installed_global_layout)
|
self.installed_global_action_widget.setLayout(installed_global_layout)
|
||||||
self.info_panel_layout.addWidget(self.installed_global_action_widget)
|
self.info_panel_layout.addWidget(self.installed_global_action_widget)
|
||||||
|
|
||||||
@@ -557,12 +1109,6 @@ class WineHelperGUI(QMainWindow):
|
|||||||
|
|
||||||
def create_installed_tab(self):
|
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(
|
installed_tab, self.installed_scroll_layout, self.installed_search_edit, self.installed_scroll_area = self._create_searchable_grid_tab(
|
||||||
"Поиск установленной программы...", self.filter_installed_buttons
|
"Поиск установленной программы...", self.filter_installed_buttons
|
||||||
)
|
)
|
||||||
@@ -817,6 +1363,7 @@ class WineHelperGUI(QMainWindow):
|
|||||||
self.installed_global_action_widget.setVisible(True)
|
self.installed_global_action_widget.setVisible(True)
|
||||||
self.backup_button.setVisible(True)
|
self.backup_button.setVisible(True)
|
||||||
self.create_log_button.setVisible(True)
|
self.create_log_button.setVisible(True)
|
||||||
|
self.uninstall_button.setVisible(True)
|
||||||
self.manual_install_path_widget.setVisible(False)
|
self.manual_install_path_widget.setVisible(False)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -861,7 +1408,7 @@ class WineHelperGUI(QMainWindow):
|
|||||||
yes_button = QPushButton("Да")
|
yes_button = QPushButton("Да")
|
||||||
no_button = QPushButton("Нет")
|
no_button = QPushButton("Нет")
|
||||||
|
|
||||||
msg_box = QMessageBox()
|
msg_box = QMessageBox(self)
|
||||||
msg_box.setWindowTitle("Создание резервной копии")
|
msg_box.setWindowTitle("Создание резервной копии")
|
||||||
msg_box.setText(
|
msg_box.setText(
|
||||||
f"Будет создана резервная копия префикса '{prefix_name}'.\n"
|
f"Будет создана резервная копия префикса '{prefix_name}'.\n"
|
||||||
@@ -894,7 +1441,8 @@ class WineHelperGUI(QMainWindow):
|
|||||||
layout.addWidget(self.command_close_button)
|
layout.addWidget(self.command_close_button)
|
||||||
self.command_dialog.setLayout(layout)
|
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.setProcessChannelMode(QProcess.MergedChannels)
|
||||||
self.command_process.readyReadStandardOutput.connect(self._handle_command_output)
|
self.command_process.readyReadStandardOutput.connect(self._handle_command_output)
|
||||||
self.command_process.finished.connect(self._handle_command_finished)
|
self.command_process.finished.connect(self._handle_command_finished)
|
||||||
@@ -937,7 +1485,8 @@ class WineHelperGUI(QMainWindow):
|
|||||||
layout.addWidget(self.command_close_button)
|
layout.addWidget(self.command_close_button)
|
||||||
self.command_dialog.setLayout(layout)
|
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.setProcessChannelMode(QProcess.MergedChannels)
|
||||||
self.command_process.readyReadStandardOutput.connect(self._handle_command_output)
|
self.command_process.readyReadStandardOutput.connect(self._handle_command_output)
|
||||||
self.command_process.finished.connect(self._handle_restore_finished)
|
self.command_process.finished.connect(self._handle_restore_finished)
|
||||||
@@ -955,7 +1504,7 @@ class WineHelperGUI(QMainWindow):
|
|||||||
yes_button = QPushButton("Да")
|
yes_button = QPushButton("Да")
|
||||||
no_button = QPushButton("Нет")
|
no_button = QPushButton("Нет")
|
||||||
|
|
||||||
msg_box = QMessageBox()
|
msg_box = QMessageBox(self)
|
||||||
msg_box.setWindowTitle("Создание лога")
|
msg_box.setWindowTitle("Создание лога")
|
||||||
msg_box.setText(
|
msg_box.setText(
|
||||||
"Приложение будет запущено в режиме отладки.\n"
|
"Приложение будет запущено в режиме отладки.\n"
|
||||||
@@ -969,6 +1518,72 @@ class WineHelperGUI(QMainWindow):
|
|||||||
if msg_box.clickedButton() == yes_button:
|
if msg_box.clickedButton() == yes_button:
|
||||||
self._run_app_launcher(debug=True)
|
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):
|
def run_installed_app(self):
|
||||||
"""Запускает выбранное установленное приложение"""
|
"""Запускает выбранное установленное приложение"""
|
||||||
self._run_app_launcher(debug=False)
|
self._run_app_launcher(debug=False)
|
||||||
@@ -1035,7 +1650,7 @@ class WineHelperGUI(QMainWindow):
|
|||||||
yes_button = QPushButton("Да")
|
yes_button = QPushButton("Да")
|
||||||
no_button = QPushButton("Нет")
|
no_button = QPushButton("Нет")
|
||||||
|
|
||||||
msg_box = QMessageBox()
|
msg_box = QMessageBox(self)
|
||||||
msg_box.setWindowTitle('Подтверждение')
|
msg_box.setWindowTitle('Подтверждение')
|
||||||
msg_box.setText(
|
msg_box.setText(
|
||||||
f'Вы действительно хотите удалить "{app_name}" и все связанные файлы (включая префикс "{prefix_name}")?'
|
f'Вы действительно хотите удалить "{app_name}" и все связанные файлы (включая префикс "{prefix_name}")?'
|
||||||
@@ -1384,7 +1999,8 @@ class WineHelperGUI(QMainWindow):
|
|||||||
|
|
||||||
def _start_installation(self, winehelper_path, script_path, install_file=None):
|
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.setProcessChannelMode(QProcess.MergedChannels)
|
||||||
self.install_process.setWorkingDirectory(os.path.dirname(winehelper_path))
|
self.install_process.setWorkingDirectory(os.path.dirname(winehelper_path))
|
||||||
|
|
||||||
@@ -1519,6 +2135,12 @@ class WineHelperGUI(QMainWindow):
|
|||||||
# Кнопка прервать
|
# Кнопка прервать
|
||||||
self.btn_abort.setEnabled(False)
|
self.btn_abort.setEnabled(False)
|
||||||
|
|
||||||
|
# Процесс завершен, можно запланировать его удаление и очистить ссылку,
|
||||||
|
# чтобы избежать утечек и висячих ссылок.
|
||||||
|
if self.install_process:
|
||||||
|
self.install_process.deleteLater()
|
||||||
|
self.install_process = None
|
||||||
|
|
||||||
def handle_install_dialog_close(self, event):
|
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:
|
if hasattr(self, 'install_process') and self.install_process and self.install_process.state() == QProcess.Running:
|
||||||
yes_button = QPushButton("Да")
|
yes_button = QPushButton("Да")
|
||||||
no_button = QPushButton("Нет")
|
no_button = QPushButton("Нет")
|
||||||
msg_box = QMessageBox()
|
msg_box = QMessageBox(self.install_dialog)
|
||||||
msg_box.setWindowTitle("Подтверждение")
|
msg_box.setWindowTitle("Подтверждение")
|
||||||
msg_box.setText("Вы действительно хотите прервать установку?")
|
msg_box.setText("Вы действительно хотите прервать установку?")
|
||||||
msg_box.addButton(yes_button, QMessageBox.YesRole)
|
msg_box.addButton(yes_button, QMessageBox.YesRole)
|
||||||
@@ -1580,6 +2202,9 @@ class WineHelperGUI(QMainWindow):
|
|||||||
self.command_log_output.append(f"\n=== Команда успешно завершена ===")
|
self.command_log_output.append(f"\n=== Команда успешно завершена ===")
|
||||||
else:
|
else:
|
||||||
self.command_log_output.append(f"\n=== Ошибка выполнения (код: {exit_code}) ===")
|
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)
|
self.command_close_button.setEnabled(True)
|
||||||
|
|
||||||
def _handle_restore_finished(self, exit_code, exit_status):
|
def _handle_restore_finished(self, exit_code, exit_status):
|
||||||
@@ -1590,6 +2215,9 @@ class WineHelperGUI(QMainWindow):
|
|||||||
self.filter_installed_buttons()
|
self.filter_installed_buttons()
|
||||||
else:
|
else:
|
||||||
self.command_log_output.append(f"\n=== Ошибка выполнения (код: {exit_code}) ===")
|
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)
|
self.command_close_button.setEnabled(True)
|
||||||
|
|
||||||
def cleanup_process(self):
|
def cleanup_process(self):
|
||||||
|
Reference in New Issue
Block a user