diff --git a/winehelper_gui.py b/winehelper_gui.py index fe9d20d..b343626 100644 --- a/winehelper_gui.py +++ b/winehelper_gui.py @@ -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'{message}') + else: + self.log_output.append(message) + self.log_output.moveCursor(QTextCursor.End) + class WineHelperGUI(QMainWindow): def __init__(self): super().__init__() @@ -144,6 +658,7 @@ class WineHelperGUI(QMainWindow): if show_global: self.backup_button.setVisible(False) self.create_log_button.setVisible(False) + self.uninstall_button.setVisible(False) self.current_selected_app = None def on_tab_changed(self, index): @@ -223,15 +738,46 @@ class WineHelperGUI(QMainWindow): installed_action_layout.setContentsMargins(0, 0, 0, 0) installed_action_layout.setSpacing(5) + # --- Верхний ряд кнопок --- top_buttons_layout = QHBoxLayout() self.run_button = QPushButton("Запустить") self.run_button.clicked.connect(self.run_installed_app) - self.uninstall_button = QPushButton("Удалить префикс") - self.uninstall_button.clicked.connect(self.uninstall_app) top_buttons_layout.addWidget(self.run_button) - top_buttons_layout.addWidget(self.uninstall_button) installed_action_layout.addLayout(top_buttons_layout) + # --- Сетка с утилитами --- + utils_grid_layout = QGridLayout() + utils_grid_layout.setSpacing(5) + + # Ряд 0 + self.winetricks_button = QPushButton("Менеджер компонентов") + self.winetricks_button.clicked.connect(self.open_winetricks_manager) + utils_grid_layout.addWidget(self.winetricks_button, 0, 0) + + self.winecfg_button = QPushButton("Редактор настроек") + self.winecfg_button.clicked.connect(lambda: self._run_wine_util('winecfg')) + utils_grid_layout.addWidget(self.winecfg_button, 0, 1) + + # Ряд 1 + self.regedit_button = QPushButton("Редактор реестра") + self.regedit_button.clicked.connect(lambda: self._run_wine_util('regedit')) + utils_grid_layout.addWidget(self.regedit_button, 1, 0) + + self.uninstaller_button = QPushButton("Удаление программ") + self.uninstaller_button.clicked.connect(lambda: self._run_wine_util('uninstaller')) + utils_grid_layout.addWidget(self.uninstaller_button, 1, 1) + + # Ряд 2 + self.cmd_button = QPushButton("Командная строка") + self.cmd_button.clicked.connect(lambda: self._run_wine_util('cmd')) + utils_grid_layout.addWidget(self.cmd_button, 2, 0) + + self.winefile_button = QPushButton("Файловый менеджер") + self.winefile_button.clicked.connect(lambda: self._run_wine_util('winefile')) + utils_grid_layout.addWidget(self.winefile_button, 2, 1) + + installed_action_layout.addLayout(utils_grid_layout) + self.installed_action_widget.setLayout(installed_action_layout) self.info_panel_layout.addWidget(self.installed_action_widget) @@ -249,10 +795,16 @@ class WineHelperGUI(QMainWindow): self.backup_button.clicked.connect(self.backup_prefix_for_app) installed_global_layout.addWidget(self.backup_button) + self.uninstall_button = QPushButton("Удалить префикс") + self.uninstall_button.setIcon(QIcon.fromTheme("user-trash")) + self.uninstall_button.clicked.connect(self.uninstall_app) + installed_global_layout.addWidget(self.uninstall_button) + self.restore_prefix_button_panel = QPushButton("Восстановить префикс из резервной копии") self.restore_prefix_button_panel.setIcon(QIcon.fromTheme("document-revert")) self.restore_prefix_button_panel.clicked.connect(self.restore_prefix) installed_global_layout.addWidget(self.restore_prefix_button_panel) + self.installed_global_action_widget.setLayout(installed_global_layout) self.info_panel_layout.addWidget(self.installed_global_action_widget) @@ -557,12 +1109,6 @@ class WineHelperGUI(QMainWindow): def create_installed_tab(self): """Создает вкладку для отображения установленных программ в виде кнопок""" - installed_tab = QWidget() - installed_layout = QVBoxLayout() - installed_layout.setContentsMargins(0, 0, 0, 0) - installed_layout.setSpacing(5) - installed_tab.setLayout(installed_layout) - installed_tab, self.installed_scroll_layout, self.installed_search_edit, self.installed_scroll_area = self._create_searchable_grid_tab( "Поиск установленной программы...", self.filter_installed_buttons ) @@ -817,6 +1363,7 @@ class WineHelperGUI(QMainWindow): self.installed_global_action_widget.setVisible(True) self.backup_button.setVisible(True) self.create_log_button.setVisible(True) + self.uninstall_button.setVisible(True) self.manual_install_path_widget.setVisible(False) except Exception as e: @@ -861,7 +1408,7 @@ class WineHelperGUI(QMainWindow): yes_button = QPushButton("Да") no_button = QPushButton("Нет") - msg_box = QMessageBox() + msg_box = QMessageBox(self) msg_box.setWindowTitle("Создание резервной копии") msg_box.setText( f"Будет создана резервная копия префикса '{prefix_name}'.\n" @@ -894,7 +1441,8 @@ class WineHelperGUI(QMainWindow): layout.addWidget(self.command_close_button) self.command_dialog.setLayout(layout) - self.command_process = QProcess() + # Устанавливаем родителя, чтобы избежать утечек памяти + self.command_process = QProcess(self.command_dialog) self.command_process.setProcessChannelMode(QProcess.MergedChannels) self.command_process.readyReadStandardOutput.connect(self._handle_command_output) self.command_process.finished.connect(self._handle_command_finished) @@ -937,7 +1485,8 @@ class WineHelperGUI(QMainWindow): layout.addWidget(self.command_close_button) self.command_dialog.setLayout(layout) - self.command_process = QProcess() + # Устанавливаем родителя, чтобы избежать утечек памяти + self.command_process = QProcess(self.command_dialog) self.command_process.setProcessChannelMode(QProcess.MergedChannels) self.command_process.readyReadStandardOutput.connect(self._handle_command_output) self.command_process.finished.connect(self._handle_restore_finished) @@ -955,7 +1504,7 @@ class WineHelperGUI(QMainWindow): yes_button = QPushButton("Да") no_button = QPushButton("Нет") - msg_box = QMessageBox() + msg_box = QMessageBox(self) msg_box.setWindowTitle("Создание лога") msg_box.setText( "Приложение будет запущено в режиме отладки.\n" @@ -969,6 +1518,72 @@ class WineHelperGUI(QMainWindow): if msg_box.clickedButton() == yes_button: self._run_app_launcher(debug=True) + def open_winetricks_manager(self): + """Открывает новый диалог для управления компонентами Winetricks.""" + prefix_name = self._get_prefix_name_for_selected_app() + if not prefix_name: + QMessageBox.warning(self, "Менеджер Winetricks", "Сначала выберите установленное приложение, чтобы открыть менеджер для его префикса.") + return + + prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name) + if not os.path.isdir(prefix_path): + QMessageBox.critical(self, "Ошибка", f"Каталог префикса не найден:\n{prefix_path}") + return + + winehelper_dir = os.path.dirname(self.winehelper_path) + winetricks_path = None + try: + # Ищем файл, который начинается с 'winetricks_' + for filename in os.listdir(winehelper_dir): + if filename.startswith("winetricks_"): + winetricks_path = os.path.join(winehelper_dir, filename) + break # Нашли, выходим из цикла + except OSError as e: + QMessageBox.critical(self, "Ошибка", f"Не удалось прочитать директорию {winehelper_dir}: {e}") + return + + if not winetricks_path: + QMessageBox.critical(self, "Ошибка", f"Скрипт winetricks не найден в директории:\n{winehelper_dir}") + return + + dialog = WinetricksManagerDialog(prefix_path, winetricks_path, self) + dialog.exec_() + + def _run_wine_util(self, util_name): + """Запускает стандартную утилиту Wine для выбранного префикса.""" + prefix_name = self._get_prefix_name_for_selected_app() + if not prefix_name: + QMessageBox.warning(self, "Ошибка", "Сначала выберите установленное приложение.") + return + + prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name) + if not os.path.isdir(prefix_path): + QMessageBox.critical(self, "Ошибка", f"Каталог префикса не найден:\n{prefix_path}") + return + + env = os.environ.copy() + env["WINEPREFIX"] = prefix_path + + # 'wine cmd' - особый случай, требует запуска в терминале + if util_name == 'cmd': + terminal_command = f"env WINEPREFIX='{prefix_path}' wine cmd" + try: + # x-terminal-emulator - стандартный способ вызова терминала по умолчанию + subprocess.Popen(['x-terminal-emulator', '-e', terminal_command]) + except FileNotFoundError: + QMessageBox.critical(self, "Ошибка", "Не удалось найти `x-terminal-emulator`.\nУбедитесь, что у вас установлен терминал по умолчанию (например, mate-terminal или xterm).") + except Exception as e: + QMessageBox.critical(self, "Ошибка", f"Не удалось запустить терминал: {e}") + return + + # Для остальных утилит + command = ['wine', util_name] + try: + subprocess.Popen(command, env=env) + except Exception as e: + QMessageBox.critical(self, "Ошибка запуска", + f"Не удалось запустить команду:\n{' '.join(command)}\n\nОшибка: {str(e)}") + def run_installed_app(self): """Запускает выбранное установленное приложение""" self._run_app_launcher(debug=False) @@ -1035,7 +1650,7 @@ class WineHelperGUI(QMainWindow): yes_button = QPushButton("Да") no_button = QPushButton("Нет") - msg_box = QMessageBox() + msg_box = QMessageBox(self) msg_box.setWindowTitle('Подтверждение') msg_box.setText( f'Вы действительно хотите удалить "{app_name}" и все связанные файлы (включая префикс "{prefix_name}")?' @@ -1240,7 +1855,7 @@ class WineHelperGUI(QMainWindow): html_description = f"
{description}
" if prog_url: html_description += f'Официальный сайт: {prog_url}
' - + self.script_description.setHtml(html_description) self.script_description.setVisible(True) self.install_action_widget.setVisible(True) @@ -1384,7 +1999,8 @@ class WineHelperGUI(QMainWindow): def _start_installation(self, winehelper_path, script_path, install_file=None): """Запускает процесс установки""" - self.install_process = QProcess() + # Устанавливаем родителя для QProcess, чтобы он корректно удалялся вместе с диалогом + self.install_process = QProcess(self.install_dialog) self.install_process.setProcessChannelMode(QProcess.MergedChannels) self.install_process.setWorkingDirectory(os.path.dirname(winehelper_path)) @@ -1519,6 +2135,12 @@ class WineHelperGUI(QMainWindow): # Кнопка прервать self.btn_abort.setEnabled(False) + # Процесс завершен, можно запланировать его удаление и очистить ссылку, + # чтобы избежать утечек и висячих ссылок. + if self.install_process: + self.install_process.deleteLater() + self.install_process = None + def handle_install_dialog_close(self, event): """Обрабатывает событие закрытия диалога установки.""" # Проверяем, запущен ли еще процесс установки @@ -1555,7 +2177,7 @@ class WineHelperGUI(QMainWindow): if hasattr(self, 'install_process') and self.install_process and self.install_process.state() == QProcess.Running: yes_button = QPushButton("Да") no_button = QPushButton("Нет") - msg_box = QMessageBox() + msg_box = QMessageBox(self.install_dialog) msg_box.setWindowTitle("Подтверждение") msg_box.setText("Вы действительно хотите прервать установку?") msg_box.addButton(yes_button, QMessageBox.YesRole) @@ -1580,6 +2202,9 @@ class WineHelperGUI(QMainWindow): self.command_log_output.append(f"\n=== Команда успешно завершена ===") else: self.command_log_output.append(f"\n=== Ошибка выполнения (код: {exit_code}) ===") + if self.command_process: + self.command_process.deleteLater() + self.command_process = None self.command_close_button.setEnabled(True) def _handle_restore_finished(self, exit_code, exit_status): @@ -1590,6 +2215,9 @@ class WineHelperGUI(QMainWindow): self.filter_installed_buttons() else: self.command_log_output.append(f"\n=== Ошибка выполнения (код: {exit_code}) ===") + if self.command_process: + self.command_process.deleteLater() + self.command_process = None self.command_close_button.setEnabled(True) def cleanup_process(self):