forked from CastroFidel/winehelper
added a prefix creation tab
This commit is contained in:
@@ -2176,6 +2176,7 @@ case "$arg1" in
|
||||
remove-prefix) remove_prefix "$@" ;;
|
||||
create-base-pfx) create_base_pfx "$@" ;;
|
||||
generate-db) generate_wine_metadata "$@" ;;
|
||||
init-prefix) prepair_wine ; wait_wineserver ;;
|
||||
*)
|
||||
if [[ -f "$arg1" ]] ; then
|
||||
WIN_FILE_EXEC="$(readlink -f "$arg1")"
|
||||
|
@@ -11,10 +11,10 @@ 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,
|
||||
QTextEdit, QFileDialog, QMessageBox, QLineEdit, QCheckBox, QStackedWidget, QScrollArea, QFormLayout, QGroupBox, QRadioButton, QComboBox,
|
||||
QListWidget, QListWidgetItem, QGridLayout, QFrame, QDialog, QTextBrowser)
|
||||
from PyQt5.QtCore import Qt, QProcess, QSize, QTimer, QProcessEnvironment, QPropertyAnimation, QEasingCurve
|
||||
from PyQt5.QtGui import QIcon, QFont, QTextCursor, QPixmap, QPainter
|
||||
from PyQt5.QtGui import QIcon, QFont, QTextCursor, QPixmap, QPainter, QDesktopServices
|
||||
from PyQt5.QtNetwork import QLocalServer, QLocalSocket
|
||||
|
||||
|
||||
@@ -1011,6 +1011,205 @@ class ScriptParser:
|
||||
except Exception as e:
|
||||
return f"Ошибка чтения файла: {str(e)}"
|
||||
|
||||
class WineVersionSelectionDialog(QDialog):
|
||||
"""Диалог для выбора версии Wine/Proton с группировкой."""
|
||||
|
||||
def __init__(self, architecture, winehelper_path, user_work_path, parent=None):
|
||||
super().__init__(parent)
|
||||
self.architecture = architecture
|
||||
self.winehelper_path = winehelper_path
|
||||
self.user_work_path = user_work_path
|
||||
self.selected_version = None
|
||||
self.wine_versions_data = {}
|
||||
|
||||
self.setWindowTitle(f"Выбор версии Wine/Proton для {architecture} префикса")
|
||||
self.setMinimumSize(900, 500)
|
||||
self.setModal(True)
|
||||
|
||||
main_layout = QVBoxLayout(self)
|
||||
|
||||
self.search_edit = QLineEdit()
|
||||
self.search_edit.setPlaceholderText("Поиск версии...")
|
||||
self.search_edit.textChanged.connect(self.filter_versions)
|
||||
main_layout.addWidget(self.search_edit)
|
||||
|
||||
self.version_tabs = QTabWidget()
|
||||
main_layout.addWidget(self.version_tabs)
|
||||
button_layout = QHBoxLayout()
|
||||
self.refresh_button = QPushButton("Обновить список")
|
||||
self.refresh_button.setIcon(QIcon.fromTheme("view-refresh"))
|
||||
self.refresh_button.clicked.connect(self.load_versions)
|
||||
button_layout.addStretch()
|
||||
button_layout.addWidget(self.refresh_button)
|
||||
main_layout.addLayout(button_layout)
|
||||
|
||||
self.load_versions()
|
||||
|
||||
def load_versions(self):
|
||||
"""Запускает процесс получения списка версий Wine."""
|
||||
if not shutil.which('jq'):
|
||||
QMessageBox.critical(self, "Ошибка", "Утилита 'jq' не найдена. Невозможно получить список версий Wine.\n\nУстановите пакет 'jq'.")
|
||||
return
|
||||
|
||||
self.version_tabs.clear()
|
||||
loading_widget = QWidget()
|
||||
loading_layout = QVBoxLayout(loading_widget)
|
||||
status_label = QLabel("Загрузка, пожалуйста, подождите...")
|
||||
status_label.setAlignment(Qt.AlignCenter)
|
||||
loading_layout.addWidget(status_label)
|
||||
self.version_tabs.addTab(loading_widget, "Загрузка...")
|
||||
self.version_tabs.setEnabled(False)
|
||||
|
||||
self.refresh_button.setEnabled(False)
|
||||
self.db_process = QProcess(self)
|
||||
self.db_process.setProcessChannelMode(QProcess.MergedChannels)
|
||||
self.db_process.finished.connect(self._on_db_generation_finished)
|
||||
self.db_process.start(self.winehelper_path, ["generate-db"])
|
||||
|
||||
def _on_db_generation_finished(self, exit_code, exit_status):
|
||||
"""Обрабатывает завершение генерации метаданных Wine."""
|
||||
self.refresh_button.setEnabled(True)
|
||||
self.version_tabs.setEnabled(True)
|
||||
|
||||
error_message = None
|
||||
if exit_code != 0:
|
||||
error_output = self.db_process.readAll().data().decode('utf-8', 'ignore')
|
||||
QMessageBox.warning(self, "Ошибка", f"Не удалось получить список версий Wine.\n\n{error_output}")
|
||||
error_message = "Ошибка загрузки списка версий."
|
||||
else:
|
||||
metadata_file = os.path.join(self.user_work_path, "tmp", "wine_metadata.json")
|
||||
if not os.path.exists(metadata_file):
|
||||
QMessageBox.warning(self, "Ошибка", f"Файл метаданных не найден:\n{metadata_file}")
|
||||
error_message = "Ошибка: файл метаданных не найден."
|
||||
else:
|
||||
try:
|
||||
with open(metadata_file, 'r', encoding='utf-8') as f:
|
||||
self.wine_versions_data = json.load(f)
|
||||
except (json.JSONDecodeError, IOError) as e:
|
||||
QMessageBox.warning(self, "Ошибка", f"Не удалось прочитать или обработать файл метаданных:\n{e}")
|
||||
error_message = "Ошибка парсинга JSON."
|
||||
|
||||
if error_message:
|
||||
self.version_tabs.clear()
|
||||
error_widget = QWidget()
|
||||
error_layout = QVBoxLayout(error_widget)
|
||||
error_label = QLabel(error_message)
|
||||
error_label.setAlignment(Qt.AlignCenter)
|
||||
error_layout.addWidget(error_label)
|
||||
self.version_tabs.addTab(error_widget, "Ошибка")
|
||||
return
|
||||
|
||||
self.populate_ui()
|
||||
|
||||
def populate_ui(self):
|
||||
"""Заполняет UI отфильтрованными версиями."""
|
||||
self.version_tabs.clear()
|
||||
|
||||
is_win64 = self.architecture == "win64"
|
||||
re_32bit = re.compile(r'i[3-6]86|x86(?!_64)')
|
||||
re_64bit = re.compile(r'amd64|x86_64|wow64')
|
||||
|
||||
# --- System Tab ---
|
||||
system_wine_display_name = "system"
|
||||
if shutil.which('wine'):
|
||||
try:
|
||||
# Пытаемся получить версию системного wine
|
||||
result = subprocess.run(['wine', '--version'], capture_output=True, text=True, check=True, encoding='utf-8')
|
||||
version_line = result.stdout.strip()
|
||||
# Вывод обычно "wine-X.Y.Z"
|
||||
system_wine_display_name = f"system ({version_line})"
|
||||
except (FileNotFoundError, subprocess.CalledProcessError) as e:
|
||||
print(f"Не удалось получить версию системного wine: {e}")
|
||||
# Если wine возвращает ошибку, просто используем "system"
|
||||
|
||||
self._create_version_tab("Системный", [(system_wine_display_name, "system")])
|
||||
|
||||
# --- Other versions from JSON ---
|
||||
group_keys = sorted(self.wine_versions_data.keys())
|
||||
|
||||
for key in group_keys:
|
||||
versions = self.wine_versions_data.get(key, [])
|
||||
all_version_names = {v.get("name", "") for v in versions if v.get("name")}
|
||||
|
||||
filtered_versions = []
|
||||
for name in sorted(list(all_version_names), reverse=True):
|
||||
if is_win64:
|
||||
if re_64bit.search(name) or not re_32bit.search(name):
|
||||
filtered_versions.append(name)
|
||||
else: # win32
|
||||
if re_32bit.search(name):
|
||||
filtered_versions.append(name)
|
||||
|
||||
if not filtered_versions:
|
||||
continue
|
||||
|
||||
pretty_key = key.replace('_', ' ').title()
|
||||
self._create_version_tab(pretty_key, filtered_versions)
|
||||
|
||||
self.filter_versions()
|
||||
|
||||
def _create_version_tab(self, title, versions_list):
|
||||
"""Создает вкладку с сеткой кнопок для переданного списка версий."""
|
||||
tab_page = QWidget()
|
||||
tab_layout = QVBoxLayout(tab_page)
|
||||
tab_layout.setContentsMargins(5, 5, 5, 5)
|
||||
|
||||
scroll_area = QScrollArea()
|
||||
scroll_area.setWidgetResizable(True)
|
||||
tab_layout.addWidget(scroll_area)
|
||||
|
||||
scroll_content = QWidget()
|
||||
scroll_area.setWidget(scroll_content)
|
||||
|
||||
grid_layout = QGridLayout(scroll_content)
|
||||
grid_layout.setAlignment(Qt.AlignTop)
|
||||
|
||||
num_columns = 3
|
||||
row, col = 0, 0
|
||||
for version_data in versions_list:
|
||||
if isinstance(version_data, tuple):
|
||||
display_name, value_name = version_data
|
||||
else:
|
||||
display_name = value_name = version_data
|
||||
|
||||
btn = QPushButton(display_name)
|
||||
btn.clicked.connect(partial(self.on_version_selected, value_name))
|
||||
grid_layout.addWidget(btn, row, col)
|
||||
col += 1
|
||||
if col >= num_columns:
|
||||
col = 0
|
||||
row += 1
|
||||
|
||||
self.version_tabs.addTab(tab_page, title)
|
||||
|
||||
def filter_versions(self):
|
||||
"""Фильтрует видимость кнопок версий на основе текста поиска."""
|
||||
search_text = self.search_edit.text().lower()
|
||||
|
||||
for i in range(self.version_tabs.count()):
|
||||
tab_widget = self.version_tabs.widget(i)
|
||||
# The grid layout is inside a scroll area content widget
|
||||
grid_layout = tab_widget.findChild(QGridLayout)
|
||||
if not grid_layout:
|
||||
continue
|
||||
|
||||
any_visible_in_tab = False
|
||||
for j in range(grid_layout.count()):
|
||||
btn_widget = grid_layout.itemAt(j).widget()
|
||||
if isinstance(btn_widget, QPushButton):
|
||||
is_match = search_text in btn_widget.text().lower()
|
||||
btn_widget.setVisible(is_match)
|
||||
if is_match:
|
||||
any_visible_in_tab = True
|
||||
|
||||
# Enable/disable tab based on content
|
||||
self.version_tabs.setTabEnabled(i, any_visible_in_tab)
|
||||
|
||||
def on_version_selected(self, version_name):
|
||||
"""Обрабатывает выбор версии."""
|
||||
self.selected_version = version_name
|
||||
self.accept()
|
||||
|
||||
class WineHelperGUI(QMainWindow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
@@ -1063,6 +1262,9 @@ class WineHelperGUI(QMainWindow):
|
||||
self.icon_animators = {}
|
||||
self.previous_tab_index = 0
|
||||
|
||||
# State for command dialog log processing (specifically for prefix creation)
|
||||
self.command_output_buffer = ""
|
||||
self.command_last_line_was_progress = False
|
||||
# Создаем главный виджет и layout
|
||||
self.main_widget = QWidget()
|
||||
self.setCentralWidget(self.main_widget)
|
||||
@@ -1085,6 +1287,7 @@ class WineHelperGUI(QMainWindow):
|
||||
self.create_auto_install_tab()
|
||||
self.create_manual_install_tab()
|
||||
self.create_installed_tab()
|
||||
self.create_prefix_tab()
|
||||
self.create_help_tab()
|
||||
|
||||
# Инициализируем состояние, которое будет использоваться для логов
|
||||
@@ -1167,13 +1370,13 @@ class WineHelperGUI(QMainWindow):
|
||||
self.info_panel_layout.setStretch(1, 1)
|
||||
self.info_panel_layout.setStretch(4, 0)
|
||||
|
||||
if current_tab_text in ["Автоматическая установка", "Ручная установка", "Установленные"]:
|
||||
if current_tab_text in ["Справка", "Создать префикс"]:
|
||||
self.info_panel.setVisible(False)
|
||||
else:
|
||||
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):
|
||||
"""Создает правую панель с информацией о скрипте"""
|
||||
@@ -1565,6 +1768,81 @@ class WineHelperGUI(QMainWindow):
|
||||
)
|
||||
self.tabs.addTab(installed_tab, "Установленные")
|
||||
|
||||
def open_wine_version_dialog(self):
|
||||
"""Открывает диалог выбора версии Wine."""
|
||||
architecture = "win32" if self.arch_win32_radio.isChecked() else "win64"
|
||||
dialog = WineVersionSelectionDialog(architecture, self.winehelper_path, Var.USER_WORK_PATH, self)
|
||||
if dialog.exec_() == QDialog.Accepted and dialog.selected_version:
|
||||
self.wine_version_edit.setText(dialog.selected_version)
|
||||
|
||||
def clear_wine_version_selection(self):
|
||||
"""
|
||||
Сбрасывает выбор версии Wine при смене архитектуры,
|
||||
чтобы заставить пользователя выбрать заново.
|
||||
"""
|
||||
self.wine_version_edit.clear()
|
||||
|
||||
def create_prefix_tab(self):
|
||||
"""Создает вкладку для создания нового префикса"""
|
||||
self.prefix_tab = QWidget()
|
||||
layout = QVBoxLayout(self.prefix_tab)
|
||||
layout.setContentsMargins(10, 10, 10, 10)
|
||||
|
||||
form_layout = QFormLayout()
|
||||
form_layout.setSpacing(10)
|
||||
|
||||
self.prefix_name_edit = QLineEdit()
|
||||
self.prefix_name_edit.setPlaceholderText("Например: 'm_prefix'")
|
||||
form_layout.addRow("<b>Имя нового префикса:</b>", self.prefix_name_edit)
|
||||
|
||||
self.arch_groupbox = QGroupBox("Архитектура")
|
||||
arch_layout = QHBoxLayout()
|
||||
self.arch_win32_radio = QRadioButton("32-bit")
|
||||
self.arch_win64_radio = QRadioButton("64-bit")
|
||||
self.arch_win64_radio.setChecked(True)
|
||||
arch_layout.addWidget(self.arch_win32_radio)
|
||||
arch_layout.addWidget(self.arch_win64_radio)
|
||||
self.arch_groupbox.setLayout(arch_layout)
|
||||
form_layout.addRow("<b>Разрядность:</b>", self.arch_groupbox)
|
||||
|
||||
self.type_groupbox = QGroupBox("Тип префикса")
|
||||
type_layout = QHBoxLayout()
|
||||
self.type_clean_radio = QRadioButton("Чистый")
|
||||
self.type_recommended_radio = QRadioButton("С рекомендуемыми библиотеками")
|
||||
self.type_recommended_radio.setChecked(True)
|
||||
type_layout.addWidget(self.type_clean_radio)
|
||||
type_layout.addWidget(self.type_recommended_radio)
|
||||
self.type_groupbox.setLayout(type_layout)
|
||||
form_layout.addRow("<b>Наполнение:</b>", self.type_groupbox)
|
||||
|
||||
self.wine_version_edit = QLineEdit()
|
||||
self.wine_version_edit.setReadOnly(True)
|
||||
self.wine_version_edit.setPlaceholderText("Версия не выбрана")
|
||||
|
||||
select_version_button = QPushButton("Выбрать версию...")
|
||||
select_version_button.clicked.connect(self.open_wine_version_dialog)
|
||||
|
||||
version_layout = QHBoxLayout()
|
||||
version_layout.addWidget(self.wine_version_edit)
|
||||
version_layout.addWidget(select_version_button)
|
||||
form_layout.addRow("<b>Версия Wine/Proton:</b>", version_layout)
|
||||
|
||||
layout.addLayout(form_layout)
|
||||
layout.addStretch()
|
||||
|
||||
self.create_prefix_button = QPushButton("Создать префикс")
|
||||
self.create_prefix_button.setFont(QFont('Arial', 12, QFont.Bold))
|
||||
self.create_prefix_button.setStyleSheet("background-color: #0078d7; color: white;")
|
||||
self.create_prefix_button.setEnabled(False)
|
||||
self.create_prefix_button.clicked.connect(self.start_prefix_creation)
|
||||
layout.addWidget(self.create_prefix_button)
|
||||
|
||||
self.tabs.addTab(self.prefix_tab, "Создать префикс")
|
||||
|
||||
self.arch_win32_radio.toggled.connect(self.clear_wine_version_selection)
|
||||
self.prefix_name_edit.textChanged.connect(self.update_create_prefix_button_state)
|
||||
self.wine_version_edit.textChanged.connect(self.update_create_prefix_button_state)
|
||||
|
||||
def create_help_tab(self):
|
||||
"""Создает вкладку 'Справка' с подвкладками"""
|
||||
help_tab = QWidget()
|
||||
@@ -1685,6 +1963,101 @@ class WineHelperGUI(QMainWindow):
|
||||
|
||||
self.tabs.addTab(help_tab, "Справка")
|
||||
|
||||
def update_create_prefix_button_state(self):
|
||||
"""Включает или выключает кнопку 'Создать префикс' в зависимости от заполнения полей."""
|
||||
name_ok = bool(self.prefix_name_edit.text().strip())
|
||||
version_ok = bool(self.wine_version_edit.text().strip())
|
||||
self.create_prefix_button.setEnabled(name_ok and version_ok)
|
||||
|
||||
def start_prefix_creation(self):
|
||||
"""Запускает создание префикса после валидации."""
|
||||
if not self._show_license_agreement_dialog():
|
||||
return
|
||||
|
||||
prefix_name = self.prefix_name_edit.text().strip()
|
||||
|
||||
if not prefix_name:
|
||||
QMessageBox.warning(self, "Ошибка", "Имя префикса не может быть пустым.")
|
||||
return
|
||||
|
||||
if not re.match(r'^[a-zA-Z0-9_.-]+$', prefix_name):
|
||||
QMessageBox.warning(self, "Ошибка", "Имя префикса может содержать только латинские буквы, цифры, точки, дефисы и подчеркивания.")
|
||||
return
|
||||
|
||||
prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name)
|
||||
if os.path.exists(prefix_path):
|
||||
QMessageBox.warning(self, "Ошибка", f"Префикс с именем '{prefix_name}' уже существует.")
|
||||
return
|
||||
|
||||
wine_arch = "win32" if self.arch_win32_radio.isChecked() else "win64"
|
||||
base_pfx = "none" if self.type_clean_radio.isChecked() else ""
|
||||
wine_use = self.wine_version_edit.text()
|
||||
|
||||
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_output_buffer = ""
|
||||
self.command_last_line_was_progress = False
|
||||
|
||||
self.command_process = QProcess(self.command_dialog)
|
||||
self.command_process.setProcessChannelMode(QProcess.MergedChannels)
|
||||
|
||||
# Для создания префикса используем специальный обработчик вывода с поддержкой прогресс-бара
|
||||
self.command_process.readyReadStandardOutput.connect(self._handle_prefix_creation_output)
|
||||
self.command_process.finished.connect(self._handle_prefix_creation_finished)
|
||||
|
||||
env = QProcessEnvironment.systemEnvironment()
|
||||
env.insert("WINEPREFIX", prefix_path)
|
||||
env.insert("WINEARCH", wine_arch)
|
||||
env.insert("WH_WINE_USE", wine_use)
|
||||
if base_pfx:
|
||||
env.insert("BASE_PFX", base_pfx)
|
||||
self.command_process.setProcessEnvironment(env)
|
||||
|
||||
args = ["init-prefix"]
|
||||
self.command_log_output.append(f"=== Параметры создания префикса ===\nИмя: {prefix_name}\nПуть: {prefix_path}\nАрхитектура: {wine_arch}\nВерсия Wine: {wine_use}\nТип: {'Чистый' if base_pfx else 'С рекомендуемыми библиотеками'}\n\n" + "="*40 + "\n")
|
||||
self.command_log_output.append(f"Выполнение: {shlex.quote(self.winehelper_path)} {' '.join(shlex.quote(a) for a in args)}")
|
||||
self.command_process.start(self.winehelper_path, args)
|
||||
self.command_dialog.exec_()
|
||||
|
||||
def _handle_prefix_creation_finished(self, exit_code, exit_status):
|
||||
"""Обрабатывает завершение создания префикса."""
|
||||
# Обрабатываем остаток в буфере, если он есть
|
||||
if self.command_output_buffer:
|
||||
self._process_command_log_line(self.command_output_buffer)
|
||||
self.command_output_buffer = ""
|
||||
|
||||
# Если последней строкой был прогресс, "завершаем" его переносом строки.
|
||||
if self.command_last_line_was_progress:
|
||||
cursor = self.command_log_output.textCursor()
|
||||
cursor.movePosition(QTextCursor.End)
|
||||
cursor.insertText("\n")
|
||||
self.command_last_line_was_progress = False
|
||||
|
||||
prefix_name = self.command_process.processEnvironment().value('WINEPREFIX').split('/')[-1]
|
||||
|
||||
self._handle_command_finished(exit_code, exit_status)
|
||||
if exit_code == 0:
|
||||
self.prefix_name_edit.clear()
|
||||
self.wine_version_edit.clear()
|
||||
QMessageBox.information(self, "Успех", f"Префикс '{prefix_name}' успешно создан.")
|
||||
|
||||
def update_installed_apps(self):
|
||||
"""Обновляет список установленных приложений в виде кнопок"""
|
||||
# Если активная кнопка находится в списке удаляемых, сбрасываем ее
|
||||
@@ -1891,7 +2264,6 @@ class WineHelperGUI(QMainWindow):
|
||||
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_dialog)
|
||||
self.command_process.setProcessChannelMode(QProcess.MergedChannels)
|
||||
@@ -1935,7 +2307,6 @@ class WineHelperGUI(QMainWindow):
|
||||
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_dialog)
|
||||
self.command_process.setProcessChannelMode(QProcess.MergedChannels)
|
||||
@@ -2332,6 +2703,55 @@ class WineHelperGUI(QMainWindow):
|
||||
self.installed_global_action_widget.setVisible(False)
|
||||
self.install_button.setText(f"Установить «{display_name}»")
|
||||
|
||||
def _show_license_agreement_dialog(self):
|
||||
"""Показывает модальный диалог с лицензионным соглашением."""
|
||||
dialog = QDialog(self)
|
||||
dialog.setWindowTitle("Лицензионное соглашение")
|
||||
dialog.setMinimumSize(750, 400)
|
||||
dialog.setModal(True)
|
||||
|
||||
layout = QVBoxLayout(dialog)
|
||||
|
||||
license_text = QTextBrowser()
|
||||
try:
|
||||
license_file_path = Var.LICENSE_AGREEMENT_FILE
|
||||
if not license_file_path or not os.path.exists(license_file_path):
|
||||
raise FileNotFoundError
|
||||
|
||||
with open(license_file_path, 'r', encoding='utf-8') as f:
|
||||
license_content = f.read()
|
||||
|
||||
escaped_license_content = html.escape(license_content)
|
||||
license_text.setHtml(f"""
|
||||
<pre style="font-family: sans-serif; font-size: 10pt; white-space: pre-wrap; word-wrap: break-word;">{escaped_license_content}</pre>
|
||||
""")
|
||||
except (FileNotFoundError, TypeError):
|
||||
license_text.setHtml(f'<h3>Лицензионные соглашения</h3><p>Не удалось загрузить файл лицензионного соглашения по пути:<br>{Var.LICENSE_AGREEMENT_FILE}</p>')
|
||||
except Exception as e:
|
||||
license_text.setHtml(f'<h3>Лицензионные соглашения</h3><p>Произошла ошибка при чтении файла лицензии:<br>{str(e)}</p>')
|
||||
|
||||
layout.addWidget(license_text)
|
||||
|
||||
checkbox = QCheckBox("Я принимаю условия лицензионного соглашения")
|
||||
layout.addWidget(checkbox)
|
||||
|
||||
button_layout = QHBoxLayout()
|
||||
accept_button = QPushButton("Принять")
|
||||
accept_button.setEnabled(False)
|
||||
accept_button.clicked.connect(dialog.accept)
|
||||
|
||||
cancel_button = QPushButton("Отклонить")
|
||||
cancel_button.clicked.connect(dialog.reject)
|
||||
|
||||
button_layout.addStretch()
|
||||
button_layout.addWidget(accept_button)
|
||||
button_layout.addWidget(cancel_button)
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
checkbox.stateChanged.connect(lambda state: accept_button.setEnabled(state == Qt.Checked))
|
||||
|
||||
return dialog.exec_() == QDialog.Accepted
|
||||
|
||||
def install_current_script(self):
|
||||
"""Устанавливает текущий выбранный скрипт"""
|
||||
if not self.current_script:
|
||||
@@ -2499,10 +2919,11 @@ class WineHelperGUI(QMainWindow):
|
||||
QMessageBox.critical(self.install_dialog, "Ошибка", f"Не удалось запустить установку:\n{str(e)}")
|
||||
self.cleanup_process()
|
||||
|
||||
def append_log(self, text, is_error=False, add_newline=True):
|
||||
"""Добавляет сообщение в лог"""
|
||||
if not hasattr(self, 'log_output'): return
|
||||
cursor = self.log_output.textCursor()
|
||||
def _append_to_log(self, log_widget, text, is_error=False, add_newline=True):
|
||||
"""Helper to append text to a QTextEdit log widget."""
|
||||
if not log_widget:
|
||||
return
|
||||
cursor = log_widget.textCursor()
|
||||
cursor.movePosition(QTextCursor.End)
|
||||
|
||||
if is_error:
|
||||
@@ -2513,9 +2934,13 @@ class WineHelperGUI(QMainWindow):
|
||||
formatted_text = f"{text}\n" if add_newline else text
|
||||
cursor.insertText(formatted_text)
|
||||
|
||||
self.log_output.ensureCursorVisible()
|
||||
log_widget.ensureCursorVisible()
|
||||
QApplication.processEvents()
|
||||
|
||||
def append_log(self, text, is_error=False, add_newline=True):
|
||||
"""Добавляет сообщение в лог установки."""
|
||||
self._append_to_log(self.log_output, text, is_error, add_newline)
|
||||
|
||||
def _process_log_line(self, line_with_delimiter):
|
||||
"""Обрабатывает одну строку лога, управляя заменой строк прогресса."""
|
||||
is_progress_line = '\r' in line_with_delimiter
|
||||
@@ -2552,6 +2977,42 @@ class WineHelperGUI(QMainWindow):
|
||||
|
||||
self.last_line_was_progress = is_progress_line
|
||||
|
||||
def _process_command_log_line(self, line_with_delimiter):
|
||||
"""Обрабатывает одну строку лога для диалога создания префикса, управляя заменой строк прогресса."""
|
||||
is_progress_line = '\r' in line_with_delimiter
|
||||
|
||||
# Фильтруем "мусорные" строки прогресса (например, '-=O=-' от wget),
|
||||
# обрабатывая только те, что содержат знак процента.
|
||||
if is_progress_line:
|
||||
if not re.search(r'\d\s*%', line_with_delimiter):
|
||||
return # Игнорируем строку прогресса без процентов
|
||||
|
||||
clean_line = line_with_delimiter.replace('\r', '').replace('\n', '').strip()
|
||||
|
||||
if not clean_line:
|
||||
return
|
||||
|
||||
cursor = self.command_log_output.textCursor()
|
||||
|
||||
# Если новая строка - это прогресс, и предыдущая тоже была прогрессом,
|
||||
# то мы удаляем старую, чтобы заменить ее новой.
|
||||
if is_progress_line and self.command_last_line_was_progress:
|
||||
cursor.movePosition(QTextCursor.End)
|
||||
cursor.select(QTextCursor.LineUnderCursor)
|
||||
cursor.removeSelectedText()
|
||||
elif not is_progress_line and self.command_last_line_was_progress:
|
||||
# Это переход от строки прогресса к финальной строке.
|
||||
# Вместо добавления переноса, мы заменяем предыдущую строку новой.
|
||||
cursor.movePosition(QTextCursor.End)
|
||||
cursor.select(QTextCursor.LineUnderCursor)
|
||||
cursor.removeSelectedText()
|
||||
|
||||
# Добавляем новую очищенную строку.
|
||||
# Для прогресса - без переноса строки, для обычных строк - с переносом.
|
||||
self._append_to_log(self.command_log_output, clean_line, add_newline=not is_progress_line)
|
||||
|
||||
self.command_last_line_was_progress = is_progress_line
|
||||
|
||||
def handle_process_output(self):
|
||||
"""Обрабатывает вывод процесса, корректно отображая однострочный прогресс."""
|
||||
new_data = self.install_process.readAllStandardOutput().data().decode('utf-8', errors='ignore')
|
||||
@@ -2604,6 +3065,27 @@ class WineHelperGUI(QMainWindow):
|
||||
# Кнопка прервать
|
||||
self.btn_abort.setEnabled(False)
|
||||
|
||||
def _handle_prefix_creation_output(self):
|
||||
"""Обрабатывает вывод процесса создания префикса, корректно отображая прогресс."""
|
||||
if not hasattr(self, 'command_process') or not self.command_process:
|
||||
return
|
||||
new_data = self.command_process.readAllStandardOutput().data().decode('utf-8', errors='ignore')
|
||||
self.command_output_buffer += new_data
|
||||
|
||||
while True:
|
||||
# Ищем ближайший разделитель (\n или \r)
|
||||
idx_n = self.command_output_buffer.find('\n')
|
||||
idx_r = self.command_output_buffer.find('\r')
|
||||
|
||||
if idx_n == -1 and idx_r == -1:
|
||||
break # Нет полных строк для обработки
|
||||
|
||||
split_idx = min(idx for idx in [idx_n, idx_r] if idx != -1)
|
||||
|
||||
line = self.command_output_buffer[:split_idx + 1]
|
||||
self.command_output_buffer = self.command_output_buffer[split_idx + 1:]
|
||||
self._process_command_log_line(line)
|
||||
|
||||
# Процесс завершен, можно запланировать его удаление и очистить ссылку,
|
||||
# чтобы избежать утечек и висячих ссылок.
|
||||
if self.install_process:
|
||||
|
Reference in New Issue
Block a user