Files
winehelper/winehelper_gui.py
2025-08-01 15:22:53 +06:00

1531 lines
77 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
import os
import subprocess
import sys
import shlex
import shutil
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,QPushButton, QLabel, QTabWidget,
QTextEdit, QFileDialog, QMessageBox, QLineEdit, QCheckBox, QStackedWidget, QScrollArea,
QGridLayout, QFrame, QDialog, QTextBrowser)
from PyQt5.QtCore import Qt, QProcess, QSize, QTimer, QProcessEnvironment
from PyQt5.QtGui import QIcon, QFont, QTextCursor, QPixmap, QPainter
class Var:
# Переменные определяемые в скрипте winehelper
SCRIPT_NAME = os.environ.get("SCRIPT_NAME")
USER_WORK_PATH = os.environ.get("USER_WORK_PATH")
RUN_SCRIPT = os.environ.get("RUN_SCRIPT")
DATA_PATH = os.environ.get("DATA_PATH")
CHANGELOG_FILE = os.environ.get("CHANGELOG_FILE")
WH_ICON_PATH = os.environ.get("WH_ICON_PATH")
LICENSE_FILE = os.environ.get("LICENSE_FILE")
class WineHelperGUI(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("WineHelper")
self.setGeometry(100, 100, 950, 500)
if Var.WH_ICON_PATH and os.path.exists(Var.WH_ICON_PATH):
self.setWindowIcon(QIcon(Var.WH_ICON_PATH))
# Центрирование окна
screen = QApplication.primaryScreen()
screen_geometry = screen.availableGeometry()
self.move(
(screen_geometry.width() - self.width()) // 2,
(screen_geometry.height() - self.height()) // 2
)
# Стиль для кнопок в списках
self.BUTTON_LIST_STYLE = """
QPushButton {
text-align: left;
padding-left: 10px;
padding-right: 10px;
height: 42px; min-height: 42px; max-height: 42px;
}
QPushButton::icon {
padding-left: 10px;
}
"""
self.INSTALLED_BUTTON_LIST_STYLE = self.BUTTON_LIST_STYLE.replace(
"padding-left: 10px;", "padding-left: 15px;"
)
# Стили для оберток кнопок (для рамки выделения)
self.FRAME_STYLE_DEFAULT = "QFrame { border: 2px solid transparent; border-radius: 2px; padding: 0px; }"
self.FRAME_STYLE_SELECTED = "QFrame { border: 2px solid #0078d7; border-radius: 2px; padding: 0px; }"
# Основные переменные
self.winehelper_path = Var.RUN_SCRIPT
self.process = None
self.current_script = None
self.install_process = None
self.install_dialog = None
self.current_active_button = None
self.installed_buttons = []
self.current_selected_app = None
self.icon_animators = {}
# Создаем главный виджет и layout
self.main_widget = QWidget()
self.setCentralWidget(self.main_widget)
self.main_layout = QHBoxLayout()
self.main_widget.setLayout(self.main_layout)
# Создаем табы
self.tabs = QTabWidget()
self.main_layout.addWidget(self.tabs, stretch=2)
# Создаем панель информации о скрипте
self.create_info_panel()
# Вкладки
self.create_auto_install_tab()
self.create_manual_install_tab()
self.create_installed_tab()
self.create_help_tab()
# Обновляем список установленных приложений
self.update_installed_apps()
# Соединяем сигнал смены вкладок с функцией
self.tabs.currentChanged.connect(self.on_tab_changed)
# Устанавливаем начальное состояние видимости панели
self.on_tab_changed(self.tabs.currentIndex())
def _reset_info_panel_to_default(self, tab_name):
"""Сбрасывает правую панель в состояние по умолчанию для указанной вкладки."""
if tab_name == "Автоматическая установка":
title = "Автоматическая установка"
html_content = ("<h3>Автоматическая установка</h3>"
"<p>Скрипты из этого списка скачают, установят и настроят приложение за вас.</p>"
"<p>Просто выберите программу и нажмите «Установить».</p>")
show_global = False
elif tab_name == "Ручная установка":
title = "Ручная установка"
html_content = ("<h3>Ручная установка</h3>"
"<p>Эти скрипты подготовят окружение для установки.</p>"
"<p>Вам нужно будет указать только путь к установочному файлу (<code>.exe</code> или <code>.msi</code>), который вы скачали самостоятельно и нажать «Установить».</p>")
show_global = False
elif tab_name == "Установленные":
title = "Установленные программы"
html_content = ("<h3>Установленные программы</h3>"
"<p>Здесь отображаются все приложения, установленные с помощью WineHelper.</p>"
"<p>Выберите программу, чтобы увидеть доступные действия.</p>"
"<p>Также на этой вкладке можно восстановить префикс из резервной копии с помощью соответствующей кнопки.</p>")
show_global = True
else:
return
self.script_title.setText(title)
self.script_description.setHtml(html_content)
self.script_description.setVisible(True)
self.script_title.setPixmap(QPixmap())
self.install_action_widget.setVisible(False)
self.installed_action_widget.setVisible(False)
self.installed_global_action_widget.setVisible(show_global)
self.manual_install_path_widget.setVisible(False)
if show_global:
self.backup_button.setVisible(False)
self.create_log_button.setVisible(False)
self.current_selected_app = None
def on_tab_changed(self, index):
"""Скрывает или показывает панель информации в зависимости от активной вкладки."""
current_tab_text = self.tabs.tabText(index)
# Сбрасываем растяжение к состоянию по умолчанию:
# растягивается виджет с описанием (индекс 1), а не виджет с действиями (индекс 4)
self.info_panel_layout.setStretch(1, 1)
self.info_panel_layout.setStretch(4, 0)
if current_tab_text in ["Автоматическая установка", "Ручная установка", "Установленные"]:
self.info_panel.setVisible(True)
self._reset_info_panel_to_default(current_tab_text)
if current_tab_text == "Установленные":
self.filter_installed_buttons()
else:
self.info_panel.setVisible(False)
def create_info_panel(self):
"""Создает правую панель с информацией о скрипте"""
self.info_panel = QFrame()
self.info_panel.setFrameShape(QFrame.StyledPanel)
self.info_panel.setMinimumWidth(400) # Размер инф. панели
self.info_panel.setFont(QFont('Arial', 10)) # Шрифт и размер шрифта в инф. панели
self.info_panel_layout = QVBoxLayout()
self.info_panel.setLayout(self.info_panel_layout)
# Заголовок
self.script_title = QLabel("Выберите программу")
self.script_title.setFont(QFont('Arial', 14, QFont.Bold)) # Шрифт и размер шрифта в заголовке инф. панели
self.script_title.setAlignment(Qt.AlignCenter)
self.info_panel_layout.addWidget(self.script_title)
# Описание
self.script_description = QTextBrowser()
self.script_description.setReadOnly(True)
self.script_description.setOpenExternalLinks(True)
self.info_panel_layout.addWidget(self.script_description)
# Строка для ввода пути установочного файла
self.manual_install_path_layout = QVBoxLayout()
self.install_path_label = QLabel("Путь к установочному файлу:")
self.manual_install_path_layout.addWidget(self.install_path_label)
path_input_layout = QHBoxLayout()
self.install_path_edit = QLineEdit()
self.install_path_edit.setPlaceholderText("Укажите путь к установочному файлу...")
self.browse_button = QPushButton("Обзор...")
self.browse_button.clicked.connect(self.browse_install_file)
path_input_layout.addWidget(self.install_path_edit)
path_input_layout.addWidget(self.browse_button)
self.manual_install_path_layout.addLayout(path_input_layout)
self.manual_install_path_widget = QWidget()
self.manual_install_path_widget.setLayout(self.manual_install_path_layout)
self.manual_install_path_widget.setVisible(False)
self.info_panel_layout.addWidget(self.manual_install_path_widget)
# --- ВИДЖЕТЫ ДЛЯ ДЕЙСТВИЙ ---
# Виджет для действий установщика
self.install_action_widget = QWidget()
install_action_layout = QVBoxLayout()
install_action_layout.setContentsMargins(0, 0, 0, 0)
self.install_button = QPushButton("Установить")
self.install_button.setFont(QFont('Arial', 13, QFont.Bold))
self.install_button.setStyleSheet("background-color: #4CAF50; color: white;")
self.install_button.clicked.connect(self.install_current_script)
install_action_layout.addWidget(self.install_button)
self.install_action_widget.setLayout(install_action_layout)
self.info_panel_layout.addWidget(self.install_action_widget)
# Виджет для действий с установленным приложением
self.installed_action_widget = QWidget()
installed_action_layout = QVBoxLayout()
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)
self.installed_action_widget.setLayout(installed_action_layout)
self.info_panel_layout.addWidget(self.installed_action_widget)
self.installed_global_action_widget = QWidget()
installed_global_layout = QVBoxLayout()
installed_global_layout.setContentsMargins(0, 10, 0, 0)
self.create_log_button = QPushButton("Создать лог запуска программы")
self.create_log_button.setIcon(QIcon.fromTheme("view-list-text"))
self.create_log_button.clicked.connect(self.run_installed_app_with_debug)
installed_global_layout.addWidget(self.create_log_button)
self.backup_button = QPushButton("Создать резервную копию префикса")
self.backup_button.setIcon(QIcon.fromTheme("document-save"))
self.backup_button.clicked.connect(self.backup_prefix_for_app)
installed_global_layout.addWidget(self.backup_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)
# Изначально скрыть все виджеты действий
self.install_action_widget.setVisible(False)
self.installed_action_widget.setVisible(False)
self.installed_global_action_widget.setVisible(False)
self.main_layout.addWidget(self.info_panel, stretch=1)
def browse_install_file(self):
"""Открывает диалог выбора файла для ручной установки"""
file_path, _ = QFileDialog.getOpenFileName(
self,
"Выберите установочный файл",
os.path.expanduser("~"),
"Все файлы (*);;Исполняемые файлы (*.exe *.msi)"
)
if file_path:
self.install_path_edit.setText(file_path)
def extract_icons_from_script(self, script_path):
"""
Извлекает иконку для скрипта.
Сначала ищет переменную 'export PROG_ICON=', если не находит,
то ищет все вызовы 'create_desktop' и берет иконки из третьего аргумента.
Возвращает список имен иконок.
"""
try:
with open(script_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
# 1. Приоритет у PROG_ICON
for line in lines:
if line.strip().startswith('export PROG_ICON='):
icon_name = line.split('=', 1)[1].strip().strip('"\'')
if icon_name:
return [icon_name]
# 2. Если PROG_ICON не найден, ищем все вызовы create_desktop
icon_names = []
for line in lines:
line = line.strip()
# Пропускаем закомментированные строки и пустые строки
if not line or line.startswith('#'):
continue
if 'create_desktop' in line:
try:
parts = shlex.split(line)
# Ищем все вхождения, а не только первое
for i, part in enumerate(parts):
if part == 'create_desktop':
if len(parts) > i + 3:
icon_name = parts[i + 3]
if icon_name:
icon_names.append(icon_name)
except (ValueError, IndexError):
continue
return icon_names
except Exception as e:
print(f"Ошибка чтения файла для извлечения иконки: {str(e)}")
return []
def extract_prog_name_from_script(self, script_path):
"""Извлекает имя программы из строки PROG_NAME= в скрипте"""
try:
with open(script_path, 'r', encoding='utf-8') as f:
for line in f:
# Ищем строку, которая начинается с PROG_NAME= или export PROG_NAME=
if line.strip().startswith(('export PROG_NAME=', 'PROG_NAME=')):
# Отделяем имя переменной от значения и убираем кавычки
name = line.split('=', 1)[1].strip().strip('"\'')
if name:
return name
return None
except Exception as e:
print(f"Ошибка чтения файла для извлечения PROG_NAME: {str(e)}")
return None
def extract_prog_url_from_script(self, script_path):
"""Извлекает URL из строки export PROG_URL= в скрипте"""
try:
with open(script_path, 'r', encoding='utf-8') as f:
for line in f:
if line.startswith('export PROG_URL='):
return line.replace('export PROG_URL=', '').strip().strip('"\'')
return None
except Exception as e:
print(f"Ошибка чтения файла для извлечения PROG_URL: {str(e)}")
return None
def _ease_in_out_quad(self, t):
"""Простая квадратичная функция сглаживания (ease-in-out)."""
# t - значение от 0.0 до 1.0
if t < 0.5:
return 2 * t * t
return 1 - pow(-2 * t + 2, 2) / 2
def _start_icon_fade_animation(self, button):
"""Запускает анимацию плавного перехода для иконки на кнопке."""
if button not in self.icon_animators:
return
anim_data = self.icon_animators[button]
# Останавливаем предыдущую анимацию, если она еще идет
if anim_data.get('fade_timer') and anim_data['fade_timer'].isActive():
anim_data['fade_timer'].stop()
# Определяем текущую и следующую иконки
current_icon_path = anim_data['icons'][anim_data['current_index']]
next_icon_index = (anim_data['current_index'] + 1) % len(anim_data['icons'])
next_icon_path = anim_data['icons'][next_icon_index]
current_pixmap = QPixmap(current_icon_path)
next_pixmap = QPixmap(next_icon_path)
# Запускаем таймер для покадровой анимации
anim_data['fade_step'] = 0
fade_timer = QTimer(button)
anim_data['fade_timer'] = fade_timer
fade_timer.timeout.connect(
lambda b=button, p1=current_pixmap, p2=next_pixmap: self._update_fade_frame(b, p1, p2)
)
fade_timer.start(16) # ~60 кадров в секунду
def _update_fade_frame(self, button, old_pixmap, new_pixmap):
"""Обновляет кадр анимации перехода иконок."""
if button not in self.icon_animators:
return
anim_data = self.icon_animators[button]
# Длительность анимации: 44 шага * 16 мс ~= 700 мс
FADE_DURATION_STEPS = 44
anim_data['fade_step'] += 1
progress_linear = anim_data['fade_step'] / FADE_DURATION_STEPS
# Применяем функцию сглаживания для более плавного старта и завершения
progress = self._ease_in_out_quad(progress_linear)
if progress_linear >= 1.0:
anim_data['fade_timer'].stop()
anim_data['current_index'] = (anim_data['current_index'] + 1) % len(anim_data['icons'])
button.setIcon(QIcon(new_pixmap))
return
canvas = QPixmap(button.iconSize())
canvas.fill(Qt.transparent)
painter = QPainter(canvas)
painter.setRenderHint(QPainter.Antialiasing, True)
painter.setRenderHint(QPainter.SmoothPixmapTransform, True)
painter.setOpacity(1.0 - progress)
painter.drawPixmap(canvas.rect(), old_pixmap)
painter.setOpacity(progress)
painter.drawPixmap(canvas.rect(), new_pixmap)
painter.end()
button.setIcon(QIcon(canvas))
def _create_app_button(self, text, icon_paths, style_sheet):
"""Создает и настраивает стандартную кнопку для списков приложений."""
# Добавляем пробелы перед текстом для создания отступа от иконки
btn = QPushButton(" " + text)
btn.setStyleSheet(style_sheet)
# Отфильтровываем несуществующие пути
existing_icon_paths = [path for path in icon_paths if path and os.path.exists(path)]
if existing_icon_paths:
if len(existing_icon_paths) > 1:
# Устанавливаем первую иконку и запускаем анимацию
btn.setIcon(QIcon(existing_icon_paths[0]))
main_timer = QTimer(btn)
self.icon_animators[btn] = {'main_timer': main_timer, 'icons': existing_icon_paths, 'current_index': 0}
main_timer.timeout.connect(lambda b=btn: self._start_icon_fade_animation(b))
main_timer.start(2500) # Интервал между сменами иконок
else:
# Устанавливаем одну иконку
btn.setIcon(QIcon(existing_icon_paths[0]))
btn.setIconSize(QSize(32, 32))
else:
# Устанавливаем пустую иконку, если ничего не найдено, для сохранения отступов
btn.setIcon(QIcon())
btn.setIconSize(QSize(32, 32))
return btn
def _populate_install_grid(self, grid_layout, scripts_list, script_folder, button_list):
"""
Заполняет QGridLayout кнопками установщиков.
Кнопки создаются только для скриптов, в которых найдена переменная PROG_NAME.
:param grid_layout: QGridLayout для заполнения.
:param scripts_list: Список имен скриптов.
:param script_folder: Имя папки со скриптами ('autoinstall' или 'manualinstall').
:param button_list: Список для хранения созданных кнопок.
"""
button_index = 0
for script in scripts_list:
script_path = os.path.join(Var.DATA_PATH, script_folder, script)
prog_name = self.extract_prog_name_from_script(script_path)
# Создаем кнопку, только если для скрипта указано имя программы
if not prog_name:
continue
icon_names = self.extract_icons_from_script(script_path)
icon_paths = [os.path.join(Var.DATA_PATH, "image", f"{name}.png") for name in icon_names]
btn = self._create_app_button(prog_name, icon_paths, self.BUTTON_LIST_STYLE)
# Обертка для рамки выделения
frame = QFrame()
frame.setStyleSheet(self.FRAME_STYLE_DEFAULT)
layout = QVBoxLayout(frame)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(btn)
btn.clicked.connect(lambda _, s=script, b=btn: self.show_script_info(s, b))
row, column = divmod(button_index, 2)
grid_layout.addWidget(frame, row, column)
button_list.append(btn)
button_index += 1
def _create_searchable_grid_tab(self, placeholder_text, filter_slot):
"""
Создает стандартную вкладку с полем поиска и сеточным макетом с прокруткой.
Возвращает кортеж (главный виджет вкладки, сеточный макет, поле поиска, область прокрутки).
"""
tab_widget = QWidget()
layout = QVBoxLayout(tab_widget)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(5)
search_edit = QLineEdit()
search_edit.setPlaceholderText(placeholder_text)
search_edit.textChanged.connect(filter_slot)
layout.addWidget(search_edit)
scroll_area = QScrollArea()
scroll_area.setWidgetResizable(True)
scroll_area.setContentsMargins(0, 0, 0, 0)
layout.addWidget(scroll_area)
scroll_content_widget = QWidget()
scroll_area.setWidget(scroll_content_widget)
v_scroll_layout = QVBoxLayout(scroll_content_widget)
v_scroll_layout.setContentsMargins(0, 0, 0, 0)
v_scroll_layout.setSpacing(0)
grid_layout = QGridLayout()
grid_layout.setContentsMargins(5, 5, 5, 5)
grid_layout.setSpacing(2)
grid_layout.setColumnStretch(0, 1)
grid_layout.setColumnStretch(1, 1)
v_scroll_layout.addLayout(grid_layout)
v_scroll_layout.addStretch(1)
return tab_widget, grid_layout, search_edit, scroll_area
def _create_and_populate_install_tab(self, tab_title, script_folder, search_placeholder, filter_slot):
"""
Создает и заполняет вкладку для установки (автоматической или ручной).
Возвращает кортеж со скриптами, кнопками и виджетами.
"""
tab_widget, grid_layout, search_edit, scroll_area = self._create_searchable_grid_tab(
search_placeholder, filter_slot
)
scripts = []
script_path = os.path.join(Var.DATA_PATH, script_folder)
if os.path.isdir(script_path):
try:
scripts = sorted(os.listdir(script_path))
except OSError as e:
print(f"Не удалось прочитать директорию {script_path}: {e}")
buttons_list = []
self._populate_install_grid(grid_layout, scripts, script_folder, buttons_list)
self.tabs.addTab(tab_widget, tab_title)
return scripts, buttons_list, grid_layout, search_edit, scroll_area
def create_auto_install_tab(self):
"""Создает вкладку для автоматической установки программ"""
(
self.autoinstall_scripts, self.autoinstall_buttons, self.scroll_layout,
self.search_edit, self.auto_scroll_area
) = self._create_and_populate_install_tab(
"Автоматическая установка", "autoinstall", "Поиск скрипта автоматической установки...", self.filter_autoinstall_buttons
)
def create_manual_install_tab(self):
"""Создает вкладку для ручной установки программ"""
(
self.manualinstall_scripts, self.manualinstall_buttons, self.manual_scroll_layout,
self.manual_search_edit, self.manual_scroll_area
) = self._create_and_populate_install_tab(
"Ручная установка", "manualinstall", "Поиск скрипта ручной установки...", self.filter_manual_buttons
)
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
)
self.tabs.addTab(installed_tab, "Установленные")
def create_help_tab(self):
"""Создает вкладку 'Справка' с подвкладками"""
help_tab = QWidget()
help_layout = QVBoxLayout(help_tab)
help_layout.setContentsMargins(5, 5, 5, 5)
help_subtabs = QTabWidget()
help_layout.addWidget(help_subtabs)
# Подвкладка "Руководство"
guide_tab = QWidget()
guide_layout = QVBoxLayout(guide_tab)
guide_text = QTextBrowser()
guide_text.setOpenExternalLinks(True)
guide_text.setHtml("""
<h2>Руководство пользователя</h2>
<p>Подробное и актуальное руководство по использованию WineHelper смотрите на <a href="https://www.altlinux.org/Winehelper">https://www.altlinux.org/Winehelper</a></p>
""")
guide_layout.addWidget(guide_text)
help_subtabs.addTab(guide_tab, "Руководство")
# Подвкладка "Авторы"
authors_tab = QWidget()
authors_layout = QVBoxLayout(authors_tab)
authors_text = QTextEdit()
authors_text.setReadOnly(True)
authors_text.setHtml("""
<div style="text-align: center;">
<h2>Разработчики</h2>
Михаил Тергоев (fidel)<br>
Сергей Пальчех (minergenon)</p>
<p><b>Помощники</b><br>
Иван Мажукин (vanomj)</p>
<p><b>Идея и поддержка:</b><br>
сообщество ALT Linux</p>
<br>
<p>Отдельная благодарность всем, кто вносит свой вклад в развитие проекта,<br>
тестирует и сообщает об ошибках!</p>
</div>
""")
authors_layout.addWidget(authors_text)
help_subtabs.addTab(authors_tab, "Авторы")
# Подвкладка "Лицензия"
license_tab = QWidget()
license_layout = QVBoxLayout(license_tab)
import html
license_text = QTextBrowser()
license_text.setOpenExternalLinks(True)
try:
if not Var.LICENSE_FILE or not os.path.exists(Var.LICENSE_FILE):
raise FileNotFoundError
with open(Var.LICENSE_FILE, 'r', encoding='utf-8') as f:
license_content = f.read()
escaped_license_content = html.escape(license_content)
license_html = f"""
<h2>Лицензия</h2>
<pre style="font-family: 'DejaVu Sans Mono', monospace; font-size: 9pt; padding: 10px; border: 1px solid #a0a0a0; border-radius: 5px;">{escaped_license_content}</pre>
<hr>
<h2>Сторонние компоненты</h2>
<p>Некоторые компоненты, используемые или устанавливаемые данным ПО, могут иметь собственные лицензии. Пользователь несет полную ответственность за соблюдение этих лицензионных соглашений.</p>
<p>Ниже приведен список основных сторонних компонентов и ссылки на их исходный код:</p>
"""
# Читаем и парсим файл THIRD-PARTY
third_party_html = ""
third_party_file_path = os.path.join(Var.DATA_PATH, "THIRD-PARTY")
if os.path.exists(third_party_file_path):
with open(third_party_file_path, 'r', encoding='utf-8') as f_tp:
third_party_content = f_tp.read()
# Преобразуем контент в HTML
third_party_html += '<blockquote>'
for line in third_party_content.splitlines():
line = line.strip()
if not line:
third_party_html += '<br>'
continue
escaped_line = html.escape(line)
if line.startswith('http'):
third_party_html += f'&nbsp;&nbsp;&nbsp;&nbsp;<a href="{escaped_line}" style="font-size: 10pt;">{escaped_line}</a><br>'
else:
third_party_html += f'<b>{escaped_line}</b><br>'
third_party_html += '</blockquote>'
license_text.setHtml(license_html + third_party_html)
except (FileNotFoundError, TypeError):
license_text.setHtml(f'<h2>Лицензия</h2><p>Не удалось загрузить файл лицензии по пути:<br>{Var.LICENSE_FILE}</p>')
except Exception as e:
license_text.setHtml(f'<h2>Лицензия</h2><p>Произошла ошибка при чтении файла лицензии:<br>{str(e)}</p>')
license_layout.addWidget(license_text)
help_subtabs.addTab(license_tab, "Лицензия")
# Подвкладка "История изменений"
changelog_tab = QWidget()
changelog_layout = QVBoxLayout(changelog_tab)
changelog_text = QTextEdit()
changelog_text.setReadOnly(True)
changelog_text.setFont(QFont('DejaVu Sans Mono', 9))
try:
if not Var.CHANGELOG_FILE or not os.path.exists(Var.CHANGELOG_FILE):
raise FileNotFoundError
with open(Var.CHANGELOG_FILE, 'r', encoding='utf-8') as f:
changelog_content = f.read()
changelog_text.setText(changelog_content)
except (FileNotFoundError, TypeError):
changelog_text.setText(f"Файл истории изменений не найден по пути:\n{Var.CHANGELOG_FILE}")
except Exception as e:
changelog_text.setText(f"Не удалось прочитать файл истории изменений:\n{str(e)}")
changelog_layout.addWidget(changelog_text)
help_subtabs.addTab(changelog_tab, "История изменений")
self.tabs.addTab(help_tab, "Справка")
def update_installed_apps(self):
"""Обновляет список установленных приложений в виде кнопок"""
# Если активная кнопка находится в списке удаляемых, сбрасываем ее
if self.current_active_button in self.installed_buttons:
self.current_active_button = None
# Очистить существующие кнопки
for btn in self.installed_buttons:
btn.deleteLater()
self.installed_buttons.clear()
if not os.path.exists(Var.USER_WORK_PATH):
os.makedirs(Var.USER_WORK_PATH, exist_ok=True)
return
desktop_files = []
for entry in os.scandir(Var.USER_WORK_PATH):
if entry.is_file() and entry.name.endswith('.desktop'):
desktop_files.append(entry.path)
desktop_files.sort()
for i, desktop_path in enumerate(desktop_files):
display_name = os.path.splitext(os.path.basename(desktop_path))[0]
icon_path = None
try:
with open(desktop_path, 'r', encoding='utf-8') as f:
for line in f:
if line.startswith('Name='):
display_name = line.split('=', 1)[1].strip()
elif line.startswith('Icon='):
icon_file = line.split('=', 1)[1].strip()
if os.path.exists(icon_file):
icon_path = icon_file
except Exception as e:
print(f"Error reading {desktop_path}: {str(e)}")
btn = self._create_app_button(display_name, [icon_path], self.INSTALLED_BUTTON_LIST_STYLE)
# Обертка для рамки выделения
frame = QFrame()
frame.setStyleSheet(self.FRAME_STYLE_DEFAULT)
layout = QVBoxLayout(frame)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(btn)
# Клик по кнопке показывает информацию о приложении
btn.clicked.connect(lambda _, p=desktop_path, b=btn: self.show_installed_app_info(p, b))
row = i // 2
column = i % 2
self.installed_scroll_layout.addWidget(frame, row, column)
self.installed_buttons.append(btn)
def _set_active_button(self, button_widget):
"""Устанавливает активную кнопку и обновляет стили ее обертки (QFrame)."""
# Сброс стиля предыдущей активной кнопки
if self.current_active_button and self.current_active_button != button_widget:
parent_frame = self.current_active_button.parent()
if isinstance(parent_frame, QFrame):
parent_frame.setStyleSheet(self.FRAME_STYLE_DEFAULT)
# Применение стиля к новой кнопке
parent_frame = button_widget.parent()
if isinstance(parent_frame, QFrame):
parent_frame.setStyleSheet(self.FRAME_STYLE_SELECTED)
self.current_active_button = button_widget
def show_installed_app_info(self, desktop_path, button_widget):
"""Показывает информацию об установленном приложении в правой панели."""
self._set_active_button(button_widget)
# Очищаем поле поиска и принудительно обновляем список, чтобы показать все приложения
self.installed_search_edit.blockSignals(True)
self.installed_search_edit.clear()
self.installed_search_edit.blockSignals(False)
self.filter_installed_buttons()
# Прокручиваем к выбранному элементу
frame = button_widget.parent()
if isinstance(frame, QFrame):
# Даем циклу событий обработать перерисовку перед прокруткой
QTimer.singleShot(0, lambda: self.installed_scroll_area.ensureWidgetVisible(frame))
self.current_selected_app = {'desktop_path': desktop_path}
# Меняем растяжение: убираем у описания (индекс 1) и добавляем
# виджету с действиями для приложения (индекс 4), чтобы он оттолкнул кнопку "Восстановить" вниз.
self.info_panel_layout.setStretch(1, 0)
self.info_panel_layout.setStretch(4, 1)
try:
with open(desktop_path, 'r', encoding='utf-8') as f:
content = f.read()
# Парсим .desktop файл для получения информации
name = ""
comment = ""
exec_cmd = ""
icon = ""
for line in content.split('\n'):
if line.startswith('Name='):
name = line.split('=', 1)[1].strip()
elif line.startswith('Comment='):
comment = line.split('=', 1)[1].strip()
elif line.startswith('Exec='):
exec_cmd = line.split('=', 1)[1].strip()
elif line.startswith('Icon='):
icon = line.split('=', 1)[1].strip()
self.current_selected_app['name'] = name
self.current_selected_app['exec'] = exec_cmd
# Показываем панель информации
self.info_panel.setVisible(True)
# Показываем информацию в правой панели
self.script_title.setPixmap(QPixmap()) # Сначала очищаем иконку, чтобы отобразился текст
self.script_title.setText(name)
self.script_description.setVisible(False)
# Управляем видимостью кнопок
self.install_action_widget.setVisible(False)
self.installed_action_widget.setVisible(True)
self.installed_global_action_widget.setVisible(True)
self.backup_button.setVisible(True)
self.create_log_button.setVisible(True)
self.manual_install_path_widget.setVisible(False)
except Exception as e:
QMessageBox.warning(self, "Ошибка", f"Не удалось прочитать информацию о приложении: {str(e)}")
self.current_selected_app = None
self.info_panel.setVisible(False)
def _get_prefix_name_for_selected_app(self):
"""Извлекает имя префикса для выбранного приложения."""
if not self.current_selected_app or 'desktop_path' not in self.current_selected_app:
return None
desktop_file = self.current_selected_app['desktop_path']
if os.path.exists(desktop_file):
try:
with open(desktop_file, 'r', encoding='utf-8') as f:
for line in f:
if line.startswith('Exec='):
exec_line = line.strip()
# Ищем часть пути, например: .../prefixes/some_prefix_name/...
if "prefixes/" in exec_line:
prefix_part = exec_line.split("prefixes/")[1].split("/")[0]
if prefix_part:
return prefix_part
except Exception as e:
print(f"Error getting prefix name from {desktop_file}: {e}")
return None
def backup_prefix_for_app(self):
"""Создает резервную копию префикса для выбранного приложения."""
prefix_name = self._get_prefix_name_for_selected_app()
if not prefix_name:
QMessageBox.warning(self, "Ошибка", "Не удалось определить префикс для выбранного приложения.")
return
# Создаем кастомные кнопки
yes_button = QPushButton("Да")
no_button = QPushButton("Нет")
msg_box = QMessageBox()
msg_box.setWindowTitle("Создание резервной копии")
msg_box.setText(
f"Будет создана резервная копия префикса '{prefix_name}'.\n"
f"Файл будет сохранен на вашем Рабочем столе в формате .whpack.\n\nПродолжить?"
)
msg_box.addButton(yes_button, QMessageBox.YesRole)
msg_box.addButton(no_button, QMessageBox.NoRole)
msg_box.setDefaultButton(no_button)
msg_box.exec_()
if msg_box.clickedButton() != yes_button:
return # Отмена, если нажато "Нет" или крестик
# Используем модальный диалог для отображения процесса резервного копирования (бэкап)
self.command_dialog = QDialog(self)
self.command_dialog.setWindowTitle(f"Резервное копирование: {prefix_name}")
self.command_dialog.setMinimumSize(750, 400)
self.command_dialog.setModal(True)
self.command_dialog.setWindowFlags(self.command_dialog.windowFlags() & ~Qt.WindowCloseButtonHint)
layout = QVBoxLayout()
self.command_log_output = QTextEdit()
self.command_log_output.setReadOnly(True)
self.command_log_output.setFont(QFont('DejaVu Sans Mono', 10))
layout.addWidget(self.command_log_output)
self.command_close_button = QPushButton("Закрыть")
self.command_close_button.setEnabled(False)
self.command_close_button.clicked.connect(self.command_dialog.close)
layout.addWidget(self.command_close_button)
self.command_dialog.setLayout(layout)
self.command_process = QProcess()
self.command_process.setProcessChannelMode(QProcess.MergedChannels)
self.command_process.readyReadStandardOutput.connect(self._handle_command_output)
self.command_process.finished.connect(self._handle_command_finished)
winehelper_path = self.winehelper_path
args = ["backup-prefix", prefix_name]
self.command_log_output.append(f"Выполнение: {shlex.quote(winehelper_path)} {' '.join(shlex.quote(a) for a in args)}")
self.command_process.start(winehelper_path, args)
self.command_dialog.exec_()
def restore_prefix(self):
"""Восстанавливает префикс из резервной копии."""
backup_path, _ = QFileDialog.getOpenFileName(
self,
"Выберите файл резервной копии для восстановления",
os.path.expanduser("~"),
"WineHelper Backups (*.whpack);;Все файлы (*)"
)
if not backup_path:
return
# Используем модальный диалог для отображения процесса восстановления из бэкапа
self.command_dialog = QDialog(self)
self.command_dialog.setWindowTitle(f"Восстановление из: {os.path.basename(backup_path)}")
self.command_dialog.setMinimumSize(600, 400)
self.command_dialog.setModal(True)
self.command_dialog.setWindowFlags(self.command_dialog.windowFlags() & ~Qt.WindowCloseButtonHint)
layout = QVBoxLayout()
self.command_log_output = QTextEdit()
self.command_log_output.setReadOnly(True)
self.command_log_output.setFont(QFont('DejaVu Sans Mono', 10))
layout.addWidget(self.command_log_output)
self.command_close_button = QPushButton("Закрыть")
self.command_close_button.setEnabled(False)
self.command_close_button.clicked.connect(self.command_dialog.close)
layout.addWidget(self.command_close_button)
self.command_dialog.setLayout(layout)
self.command_process = QProcess()
self.command_process.setProcessChannelMode(QProcess.MergedChannels)
self.command_process.readyReadStandardOutput.connect(self._handle_command_output)
self.command_process.finished.connect(self._handle_restore_finished)
winehelper_path = self.winehelper_path
args = ["restore-prefix", backup_path]
self.command_log_output.append(f"Выполнение: {shlex.quote(winehelper_path)} {' '.join(shlex.quote(a) for a in args)}")
self.command_process.start(winehelper_path, args)
self.command_dialog.exec_()
def run_installed_app_with_debug(self):
"""Запускает выбранное установленное приложение с созданием лога"""
# Создаем кастомные кнопки
yes_button = QPushButton("Да")
no_button = QPushButton("Нет")
msg_box = QMessageBox()
msg_box.setWindowTitle("Создание лога")
msg_box.setText(
"Приложение будет запущено в режиме отладки.\n"
"После закрытия приложения лог будет сохранен в вашем домашнем каталоге "
"под именем 'winehelper.log'."
)
msg_box.addButton(yes_button, QMessageBox.YesRole)
msg_box.addButton(no_button, QMessageBox.NoRole)
msg_box.exec_()
if msg_box.clickedButton() == yes_button:
self._run_app_launcher(debug=True)
def run_installed_app(self):
"""Запускает выбранное установленное приложение"""
self._run_app_launcher(debug=False)
def _run_app_launcher(self, debug=False):
"""Внутренний метод для запуска приложения (с отладкой или без)"""
if not self.current_selected_app or 'exec' not in self.current_selected_app:
QMessageBox.warning(self, "Ошибка", "Сначала выберите приложение.")
return
command_str = self.current_selected_app['exec']
try:
# Используем shlex для безопасного разбора командной строки
command_parts = shlex.split(command_str)
# Удаляем параметры (%F и подобные)
clean_command = [part for part in command_parts if not part.startswith('%')]
if debug:
# Команда имеет вид: ['env', '/path/to/winehelper', '/path/to/app.exe']
# Нужно вставить '--debug' после скрипта winehelper
try:
script_index = -1
for i, part in enumerate(clean_command):
if 'winehelper' in os.path.basename(part):
script_index = i
break
if script_index != -1:
clean_command.insert(script_index + 1, '--debug')
else:
QMessageBox.critical(self, "Ошибка", "Не удалось найти скрипт winehelper в команде запуска.")
return
except Exception as e:
QMessageBox.critical(self, "Ошибка", f"Не удалось модифицировать команду для отладки: {e}")
return
# Удаление префикса
try:
subprocess.Popen(clean_command)
print(f"Запущено: {' '.join(clean_command)}")
except Exception as e:
QMessageBox.critical(self, "Ошибка запуска",
f"Не удалось запустить команду:\n{' '.join(clean_command)}\n\nОшибка: {str(e)}")
except Exception as e:
QMessageBox.critical(self, "Ошибка",
f"Не удалось обработать команду запуска:\n{command_str}\n\nОшибка: {str(e)}")
def uninstall_app(self):
"""Удаляет выбранное установленное приложение и его префикс"""
if not self.current_selected_app or 'desktop_path' not in self.current_selected_app:
QMessageBox.warning(self, "Ошибка", "Сначала выберите приложение.")
return
app_name = self.current_selected_app.get('name', 'это приложение')
prefix_name = self._get_prefix_name_for_selected_app()
if not prefix_name:
QMessageBox.warning(self, "Ошибка", "Не удалось определить префикс для выбранного приложения.")
return
# Создаем кастомные кнопки
yes_button = QPushButton("Да")
no_button = QPushButton("Нет")
msg_box = QMessageBox()
msg_box.setWindowTitle('Подтверждение')
msg_box.setText(
f'Вы действительно хотите удалить "{app_name}" и все связанные файлы (включая префикс "{prefix_name}")?'
)
msg_box.addButton(yes_button, QMessageBox.YesRole)
msg_box.addButton(no_button, QMessageBox.NoRole)
msg_box.setDefaultButton(no_button)
msg_box.exec_()
if msg_box.clickedButton() == yes_button:
try:
# Полный путь к префиксу
prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name)
# 1. Сначала собираем ВСЕ .desktop файлы, связанные с этим префиксом
all_desktop_files = set()
# Добавляем основной .desktop файл
if 'desktop_path' in self.current_selected_app:
all_desktop_files.add(self.current_selected_app['desktop_path'])
desktop_locations = [
Var.USER_WORK_PATH,
os.path.join(os.path.expanduser("~"), ".local/share/applications/WineHelper"),
os.path.join(os.path.expanduser("~"), "Desktop"),
os.path.join(os.path.expanduser("~"), "Рабочий стол"),
]
# Проверяем все .desktop файлы в стандартных местах
for location in desktop_locations:
if not os.path.exists(location):
continue
for file in os.listdir(location):
if file.endswith('.desktop'):
file_path = os.path.join(location, file)
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
if f"prefixes/{prefix_name}/" in content:
all_desktop_files.add(file_path)
except:
continue
# 2. Удаляем сам префикс
try:
if os.path.exists(prefix_path):
shutil.rmtree(prefix_path)
print(f"Удален префикс: {prefix_path}")
else:
print(f"Префикс не найден: {prefix_path}")
except Exception as e:
raise RuntimeError(f"Ошибка удаления префикса: {str(e)}")
# 3. Удаляем ВСЕ найденные .desktop файлы, связанные с этим префиксом
removed_files = []
for file_path in all_desktop_files:
try:
os.remove(file_path)
removed_files.append(file_path)
except Exception as e:
print(f"Ошибка удаления {file_path}: {str(e)}")
# 4. Удаляем категорию меню, если она пуста
menu_dir = os.path.join(os.path.expanduser("~"), ".local/share/applications/WineHelper")
if os.path.exists(menu_dir) and not os.listdir(menu_dir):
try:
os.rmdir(menu_dir)
# Удаляем связанные файлы меню
menu_files = [
os.path.join(os.path.expanduser("~"),
".local/share/desktop-directories/WineHelper.directory"),
os.path.join(os.path.expanduser("~"), ".config/menus/applications-merged/WineHelper.menu")
]
for f in menu_files:
if os.path.exists(f):
os.remove(f)
except Exception as e:
print(f"Ошибка удаления пустой категории меню: {str(e)}")
# Обновляем кэш desktop файлов
try:
subprocess.run(
["update-desktop-database", os.path.join(os.path.expanduser("~"), ".local/share/applications")])
except Exception as e:
print(f"Ошибка обновления кэша desktop файлов: {str(e)}")
# Обновляем список установленных приложений
self.update_installed_apps()
# Формируем отчет об удалении
report = f"Удаление завершено:\n"
report += f"- Префикс: {prefix_path}\n"
report += f"- Удаленные .desktop файлы ({len(removed_files)}):\n"
report += "\n".join(f" - {f}" for f in removed_files) if removed_files else " (не найдены)"
# Создаем кастомный диалог, чтобы кнопка была на русском
success_box = QMessageBox(self)
success_box.setWindowTitle("Успех")
success_box.setText(report)
success_box.setIcon(QMessageBox.Information)
success_box.addButton("Готово", QMessageBox.AcceptRole)
success_box.exec_()
self._reset_info_panel_to_default("Установленные")
except Exception as e:
QMessageBox.critical(self, "Ошибка",
f"Не удалось удалить приложение: {str(e)}\n\n"
f"Desktop файл: {self.current_selected_app.get('desktop_path', 'не определен')}\n"
f"Префикс: {prefix_name}\n"
f"Путь к префиксу: {prefix_path if 'prefix_path' in locals() else 'не определен'}")
def _filter_buttons_in_grid(self, search_text, button_list, grid_layout):
"""Общий метод для фильтрации кнопок и перестроения сетки."""
search_text_lower = search_text.lower()
visible_frames = []
for btn in button_list:
frame = btn.parent()
if isinstance(frame, QFrame):
# Сначала скрываем все, чтобы правильно перестроить сетку
frame.setVisible(False)
if search_text_lower in btn.text().lower():
visible_frames.append(frame)
# Перестраиваем сетку только с видимыми элементами
for i, frame in enumerate(visible_frames):
row, column = divmod(i, 2)
grid_layout.addWidget(frame, row, column)
frame.setVisible(True)
def filter_installed_buttons(self):
"""Фильтрует кнопки установленных программ."""
self._filter_buttons_in_grid(
self.installed_search_edit.text(), self.installed_buttons, self.installed_scroll_layout
)
def filter_manual_buttons(self):
"""Фильтрует кнопки ручной установки."""
self._filter_buttons_in_grid(
self.manual_search_edit.text(), self.manualinstall_buttons, self.manual_scroll_layout
)
def extract_info_ru(self, script_path):
"""Извлекает информацию из строки # info_ru: в скрипте"""
try:
with open(script_path, 'r', encoding='utf-8') as f:
for line in f:
if line.startswith('# info_ru:'):
return line.replace('# info_ru:', '').strip()
return "Описание отсутствует"
except Exception as e:
return f"Ошибка чтения файла: {str(e)}"
def show_script_info(self, script_name, button_widget):
"""Показывает информацию о выбранном скрипте"""
self._set_active_button(button_widget)
self.current_script = script_name
# Определяем виджеты и действия в зависимости от типа скрипта
if script_name in self.autoinstall_scripts:
script_path = os.path.join(Var.DATA_PATH, "autoinstall", script_name)
search_edit = self.search_edit
filter_func = self.filter_autoinstall_buttons
scroll_area = self.auto_scroll_area
self.manual_install_path_widget.setVisible(False)
else:
script_path = os.path.join(Var.DATA_PATH, "manualinstall", script_name)
search_edit = self.manual_search_edit
filter_func = self.filter_manual_buttons
scroll_area = self.manual_scroll_area
self.manual_install_path_widget.setVisible(True)
# Общая логика: очищаем поиск, обновляем список и прокручиваем к элементу
search_edit.blockSignals(True)
search_edit.clear()
search_edit.blockSignals(False)
filter_func()
frame = button_widget.parent()
if isinstance(frame, QFrame):
QTimer.singleShot(0, lambda: scroll_area.ensureWidgetVisible(frame))
# Обновляем информацию в правой панели
description = self.extract_info_ru(script_path)
icon_names = self.extract_icons_from_script(script_path)
prog_name = self.extract_prog_name_from_script(script_path)
prog_url = self.extract_prog_url_from_script(script_path)
display_name = prog_name if prog_name else script_name
if icon_names:
# Для заголовка используем первую иконку из списка
icon_path = os.path.join(Var.DATA_PATH, "image", f"{icon_names[0]}.png")
if os.path.exists(icon_path):
self.script_title.setPixmap(QPixmap(icon_path).scaled(64, 64, Qt.KeepAspectRatio))
else:
self.script_title.setPixmap(QPixmap())
else:
self.script_title.setPixmap(QPixmap())
self.script_title.setText(display_name)
html_description = f"<p>{description}</p>"
if prog_url:
html_description += f'<p><b>Официальный сайт:</b> <a href="{prog_url}">{prog_url}</a></p>'
self.script_description.setHtml(html_description)
self.script_description.setVisible(True)
self.install_action_widget.setVisible(True)
self.installed_action_widget.setVisible(False)
self.installed_global_action_widget.setVisible(False)
self.install_button.setText(f"Установить {display_name}")
def install_current_script(self):
"""Устанавливает текущий выбранный скрипт"""
if not self.current_script:
QMessageBox.warning(self, "Ошибка", "Не выбрана программа для установки")
return
if self.current_script in self.manualinstall_scripts and not self.install_path_edit.text().strip():
QMessageBox.warning(self, "Ошибка", "Укажите путь к установочному файлу")
return
# Создаем диалоговое окно установки
self.install_dialog = QDialog(self)
self.install_dialog.setWindowTitle(f"Установка {self.current_script}")
self.install_dialog.setMinimumSize(750, 400)
self.install_dialog.setWindowModality(Qt.WindowModal)
self.stacked_widget = QStackedWidget()
layout = QVBoxLayout()
layout.addWidget(self.stacked_widget)
self.install_dialog.setLayout(layout)
# Первая страница - лицензионное соглашение
license_page = QWidget()
license_layout = QVBoxLayout(license_page)
license_text = QTextEdit()
license_text.setReadOnly(True)
# Получаем текст лицензии из скрипта winehelper
script_path = os.path.join(Var.DATA_PATH, "winehelper")
license_content = ""
try:
with open(script_path, 'r', encoding='utf-8') as f:
capturing = False
for line in f:
if 'print_warning "Лицензионные соглашения использования сторонних компонентов:' in line:
capturing = True
continue
if capturing:
if 'Подтверждая продолжение установки' in line:
break
# Очищаем строку от лишних символов
clean_line = line.strip()
clean_line = clean_line.replace('print_warning "', '').replace('\\n', '\n')
clean_line = clean_line.rstrip('"')
license_content += clean_line + '\n'
license_text.setHtml(f"""
<h3>Лицензионные соглашения использования сторонних компонентов:</h3>
<p>{license_content}</p>
""")
except Exception as e:
print(f"Ошибка чтения файла для извлечения лицензии: {str(e)}")
license_text.setHtml("""
<h3>Лицензионные соглашения</h3>
<p>Не удалось загрузить текст лицензионного соглашения.</p>
""")
license_layout.addWidget(license_text)
self.license_checkbox = QCheckBox("Я принимаю условия лицензионного соглашения")
license_layout.addWidget(self.license_checkbox)
self.btn_continue = QPushButton("Продолжить установку")
self.btn_continue.setEnabled(False)
self.btn_continue.clicked.connect(self._prepare_installation)
license_layout.addWidget(self.btn_continue)
# Вторая страница - лог установки
log_page = QWidget()
log_layout = QVBoxLayout(log_page)
self.log_output = QTextEdit()
self.log_output.setReadOnly(True)
self.log_output.setFont(QFont('DejaVu Sans Mono', 10))
log_layout.addWidget(self.log_output)
self.control_buttons = QWidget()
btn_layout = QHBoxLayout(self.control_buttons)
self.btn_abort = QPushButton("Прервать")
self.btn_abort.clicked.connect(self.abort_installation)
btn_layout.addWidget(self.btn_abort)
self.btn_close = QPushButton("Закрыть")
self.btn_close.setEnabled(False)
self.btn_close.clicked.connect(self.install_dialog.close)
btn_layout.addWidget(self.btn_close)
log_layout.addWidget(self.control_buttons)
self.stacked_widget.addWidget(license_page)
self.stacked_widget.addWidget(log_page)
# Назначение кастомного обработчика закрытия окна
def dialog_close_handler(event):
self.handle_install_dialog_close(event)
self.install_dialog.closeEvent = dialog_close_handler
self.license_checkbox.stateChanged.connect(
lambda state: self.btn_continue.setEnabled(state == Qt.Checked)
)
self.install_dialog.show()
def _prepare_installation(self):
"""Подготавливает и запускает процесс установки"""
self.stacked_widget.setCurrentIndex(1)
winehelper_path = self.winehelper_path
script_path = os.path.join(Var.DATA_PATH,
"autoinstall" if self.current_script in self.autoinstall_scripts else "manualinstall",
self.current_script)
if not os.path.exists(winehelper_path):
QMessageBox.critical(self.install_dialog, "Ошибка", f"winehelper не найден по пути:\n{winehelper_path}")
return
if not os.path.exists(script_path):
QMessageBox.critical(self.install_dialog, "Ошибка", f"Скрипт установки не найден:\n{script_path}")
return
if self.current_script in self.manualinstall_scripts:
install_file = self.install_path_edit.text().strip()
if not install_file:
QMessageBox.critical(self.install_dialog, "Ошибка", "Не указан путь к установочному файлу")
return
QTimer.singleShot(100, lambda: self._start_installation(winehelper_path, script_path, install_file))
else:
QTimer.singleShot(100, lambda: self._start_installation(winehelper_path, script_path))
def _start_installation(self, winehelper_path, script_path, install_file=None):
"""Запускает процесс установки"""
self.install_process = QProcess()
self.install_process.setProcessChannelMode(QProcess.MergedChannels)
self.install_process.setWorkingDirectory(os.path.dirname(winehelper_path))
env = QProcessEnvironment.systemEnvironment()
env.insert("GUI_MODE", "1")
self.install_process.setProcessEnvironment(env)
self.install_process.readyReadStandardOutput.connect(self.handle_process_output)
self.install_process.finished.connect(self.handle_process_finished)
args = ["install", os.path.basename(script_path)]
if install_file:
args.append(install_file)
self.append_log(f"=== Начало установки {self.current_script} ===")
self.append_log(f"Исполняемый файл: {winehelper_path}")
self.append_log(f"Аргументы: {' '.join(shlex.quote(a) for a in args)}")
try:
self.install_process.start(winehelper_path, args)
if not self.install_process.waitForStarted(3000):
raise RuntimeError("Не удалось запустить процесс установки")
self.append_log("Процесс установки успешно запущен...")
except Exception as e:
self.append_log(f"\n=== ОШИБКА: {str(e)} ===", is_error=True)
QMessageBox.critical(self.install_dialog, "Ошибка", f"Не удалось запустить установку:\n{str(e)}")
self.cleanup_process()
def append_log(self, text, is_error=False):
"""Добавляет сообщение в лог"""
if not hasattr(self, 'log_output'): return
cursor = self.log_output.textCursor()
cursor.movePosition(QTextCursor.End)
if is_error:
cursor.insertHtml(f'<font color="red">{text}</font><br>')
else:
cursor.insertText(f"{text}\n")
self.log_output.ensureCursorVisible()
QApplication.processEvents()
def handle_process_output(self):
"""Обрабатывает вывод процесса"""
output = self.install_process.readAllStandardOutput().data().decode('utf-8', errors='ignore').strip()
if output:
self.append_log(output)
def handle_process_finished(self, exit_code, exit_status):
"""Обрабатывает завершение процесса"""
if exit_code == 0 and exit_status == QProcess.NormalExit:
self.append_log("\n=== Установка успешно завершена ===")
# Создаем кастомный диалог, чтобы кнопка была на русском
success_box = QMessageBox(self.install_dialog)
success_box.setWindowTitle("Успех")
success_box.setText(f"Программа {self.current_script} установлена успешно!")
success_box.setIcon(QMessageBox.Information)
success_box.addButton("Готово", QMessageBox.AcceptRole)
success_box.exec_()
self.update_installed_apps()
# Кнопка закрыть
self.btn_close.setEnabled(True)
# Кнопка прервать
self.btn_abort.setEnabled(False)
def handle_install_dialog_close(self, event):
"""Обрабатывает событие закрытия диалога установки."""
# Проверяем, запущен ли еще процесс установки
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(self.install_dialog)
msg_box.setWindowTitle("Прервать установку?")
msg_box.setText("Установка еще не завершена.\nВы действительно хотите прервать процесс?")
msg_box.setIcon(QMessageBox.Question)
msg_box.addButton(yes_button, QMessageBox.YesRole)
msg_box.addButton(no_button, QMessageBox.NoRole)
msg_box.setDefaultButton(no_button)
msg_box.exec_()
if msg_box.clickedButton() == yes_button:
self.append_log("\n=== Пользователь прервал установку через закрытие окна ===", is_error=True)
# Завершаем процесс. Сигнал finished вызовет handle_process_finished,
# который обновит состояние кнопок.
self.install_process.terminate()
event.accept() # Разрешаем закрытие окна
else:
# Пользователь нажал "Нет", поэтому игнорируем событие закрытия
event.ignore()
else:
# Процесс не запущен (установка завершена или еще не началась),
# поэтому просто закрываем окно
event.accept()
def abort_installation(self):
"""Прерывает текущую установку"""
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.setWindowTitle("Подтверждение")
msg_box.setText("Вы действительно хотите прервать установку?")
msg_box.addButton(yes_button, QMessageBox.YesRole)
msg_box.addButton(no_button, QMessageBox.NoRole)
msg_box.setDefaultButton(no_button)
msg_box.exec_()
if msg_box.clickedButton() == yes_button:
self.append_log("\n=== Пользователь прервал установку ===", is_error=True)
self.cleanup_process()
def _handle_command_output(self):
"""Обрабатывает вывод для диалога команды"""
if hasattr(self, 'command_process') and self.command_process:
output = self.command_process.readAllStandardOutput().data().decode('utf-8', errors='ignore').strip()
if output and hasattr(self, 'command_log_output'):
self.command_log_output.append(output)
QApplication.processEvents()
def _handle_command_finished(self, exit_code, exit_status):
"""Обрабатывает завершение для диалога команды"""
if exit_code == 0:
self.command_log_output.append(f"\n=== Команда успешно завершена ===")
else:
self.command_log_output.append(f"\n=== Ошибка выполнения (код: {exit_code}) ===")
self.command_close_button.setEnabled(True)
def _handle_restore_finished(self, exit_code, exit_status):
"""Обрабатывает завершение для диалога команды восстановления."""
if exit_code == 0:
self.command_log_output.append(f"\n=== Восстановление успешно завершено ===")
self.update_installed_apps()
self.filter_installed_buttons()
else:
self.command_log_output.append(f"\n=== Ошибка выполнения (код: {exit_code}) ===")
self.command_close_button.setEnabled(True)
def cleanup_process(self):
"""Очищает ресурсы процесса, принудительно завершая его, если он активен."""
if hasattr(self, 'install_process') and self.install_process:
if self.install_process.state() == QProcess.Running:
self.install_process.terminate()
# Даем процессу 3 секунды на завершение
if not self.install_process.waitForFinished(3000):
self.append_log("Процесс не ответил на terminate, отправляется kill...", is_error=True)
self.install_process.kill()
self.install_process.waitForFinished() # Ждем завершения после kill
self.install_process.deleteLater()
self.install_process = None
def filter_autoinstall_buttons(self):
"""Фильтрует кнопки автоматической установки."""
self._filter_buttons_in_grid(
self.search_edit.text(), self.autoinstall_buttons, self.scroll_layout
)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = WineHelperGUI()
window.show()
sys.exit(app.exec_())