added winetricks control buttons
This commit is contained in:
		| @@ -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}")?' | ||||||
| @@ -1240,7 +1855,7 @@ class WineHelperGUI(QMainWindow): | |||||||
|         html_description = f"<p>{description}</p>" |         html_description = f"<p>{description}</p>" | ||||||
|         if prog_url: |         if prog_url: | ||||||
|             html_description += f'<p><b>Официальный сайт:</b> <a href="{prog_url}">{prog_url}</a></p>' |             html_description += f'<p><b>Официальный сайт:</b> <a href="{prog_url}">{prog_url}</a></p>' | ||||||
|          |  | ||||||
|         self.script_description.setHtml(html_description) |         self.script_description.setHtml(html_description) | ||||||
|         self.script_description.setVisible(True) |         self.script_description.setVisible(True) | ||||||
|         self.install_action_widget.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): |     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