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