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 "$@" ;;
|
remove-prefix) remove_prefix "$@" ;;
|
||||||
create-base-pfx) create_base_pfx "$@" ;;
|
create-base-pfx) create_base_pfx "$@" ;;
|
||||||
generate-db) generate_wine_metadata "$@" ;;
|
generate-db) generate_wine_metadata "$@" ;;
|
||||||
|
init-prefix) prepair_wine ; wait_wineserver ;;
|
||||||
*)
|
*)
|
||||||
if [[ -f "$arg1" ]] ; then
|
if [[ -f "$arg1" ]] ; then
|
||||||
WIN_FILE_EXEC="$(readlink -f "$arg1")"
|
WIN_FILE_EXEC="$(readlink -f "$arg1")"
|
||||||
|
@@ -11,10 +11,10 @@ import json
|
|||||||
import hashlib
|
import hashlib
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,QPushButton, QLabel, QTabWidget,
|
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,QPushButton, QLabel, QTabWidget,
|
||||||
QTextEdit, QFileDialog, QMessageBox, QLineEdit, QCheckBox, QStackedWidget, QScrollArea,
|
QTextEdit, QFileDialog, QMessageBox, QLineEdit, QCheckBox, QStackedWidget, QScrollArea, QFormLayout, QGroupBox, QRadioButton, QComboBox,
|
||||||
QListWidget, QListWidgetItem, QGridLayout, QFrame, QDialog, QTextBrowser)
|
QListWidget, QListWidgetItem, QGridLayout, QFrame, QDialog, QTextBrowser)
|
||||||
from PyQt5.QtCore import Qt, QProcess, QSize, QTimer, QProcessEnvironment, QPropertyAnimation, QEasingCurve
|
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
|
from PyQt5.QtNetwork import QLocalServer, QLocalSocket
|
||||||
|
|
||||||
|
|
||||||
@@ -1011,6 +1011,205 @@ class ScriptParser:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Ошибка чтения файла: {str(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):
|
class WineHelperGUI(QMainWindow):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@@ -1063,6 +1262,9 @@ class WineHelperGUI(QMainWindow):
|
|||||||
self.icon_animators = {}
|
self.icon_animators = {}
|
||||||
self.previous_tab_index = 0
|
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
|
# Создаем главный виджет и layout
|
||||||
self.main_widget = QWidget()
|
self.main_widget = QWidget()
|
||||||
self.setCentralWidget(self.main_widget)
|
self.setCentralWidget(self.main_widget)
|
||||||
@@ -1085,6 +1287,7 @@ class WineHelperGUI(QMainWindow):
|
|||||||
self.create_auto_install_tab()
|
self.create_auto_install_tab()
|
||||||
self.create_manual_install_tab()
|
self.create_manual_install_tab()
|
||||||
self.create_installed_tab()
|
self.create_installed_tab()
|
||||||
|
self.create_prefix_tab()
|
||||||
self.create_help_tab()
|
self.create_help_tab()
|
||||||
|
|
||||||
# Инициализируем состояние, которое будет использоваться для логов
|
# Инициализируем состояние, которое будет использоваться для логов
|
||||||
@@ -1167,13 +1370,13 @@ class WineHelperGUI(QMainWindow):
|
|||||||
self.info_panel_layout.setStretch(1, 1)
|
self.info_panel_layout.setStretch(1, 1)
|
||||||
self.info_panel_layout.setStretch(4, 0)
|
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.info_panel.setVisible(True)
|
||||||
self._reset_info_panel_to_default(current_tab_text)
|
self._reset_info_panel_to_default(current_tab_text)
|
||||||
if current_tab_text == "Установленные":
|
if current_tab_text == "Установленные":
|
||||||
self.filter_installed_buttons()
|
self.filter_installed_buttons()
|
||||||
else:
|
|
||||||
self.info_panel.setVisible(False)
|
|
||||||
|
|
||||||
def create_info_panel(self):
|
def create_info_panel(self):
|
||||||
"""Создает правую панель с информацией о скрипте"""
|
"""Создает правую панель с информацией о скрипте"""
|
||||||
@@ -1565,6 +1768,81 @@ class WineHelperGUI(QMainWindow):
|
|||||||
)
|
)
|
||||||
self.tabs.addTab(installed_tab, "Установленные")
|
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):
|
def create_help_tab(self):
|
||||||
"""Создает вкладку 'Справка' с подвкладками"""
|
"""Создает вкладку 'Справка' с подвкладками"""
|
||||||
help_tab = QWidget()
|
help_tab = QWidget()
|
||||||
@@ -1685,6 +1963,101 @@ class WineHelperGUI(QMainWindow):
|
|||||||
|
|
||||||
self.tabs.addTab(help_tab, "Справка")
|
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):
|
def update_installed_apps(self):
|
||||||
"""Обновляет список установленных приложений в виде кнопок"""
|
"""Обновляет список установленных приложений в виде кнопок"""
|
||||||
# Если активная кнопка находится в списке удаляемых, сбрасываем ее
|
# Если активная кнопка находится в списке удаляемых, сбрасываем ее
|
||||||
@@ -1891,7 +2264,6 @@ class WineHelperGUI(QMainWindow):
|
|||||||
self.command_close_button.clicked.connect(self.command_dialog.close)
|
self.command_close_button.clicked.connect(self.command_dialog.close)
|
||||||
layout.addWidget(self.command_close_button)
|
layout.addWidget(self.command_close_button)
|
||||||
self.command_dialog.setLayout(layout)
|
self.command_dialog.setLayout(layout)
|
||||||
|
|
||||||
# Устанавливаем родителя, чтобы избежать утечек памяти
|
# Устанавливаем родителя, чтобы избежать утечек памяти
|
||||||
self.command_process = QProcess(self.command_dialog)
|
self.command_process = QProcess(self.command_dialog)
|
||||||
self.command_process.setProcessChannelMode(QProcess.MergedChannels)
|
self.command_process.setProcessChannelMode(QProcess.MergedChannels)
|
||||||
@@ -1935,7 +2307,6 @@ class WineHelperGUI(QMainWindow):
|
|||||||
self.command_close_button.clicked.connect(self.command_dialog.close)
|
self.command_close_button.clicked.connect(self.command_dialog.close)
|
||||||
layout.addWidget(self.command_close_button)
|
layout.addWidget(self.command_close_button)
|
||||||
self.command_dialog.setLayout(layout)
|
self.command_dialog.setLayout(layout)
|
||||||
|
|
||||||
# Устанавливаем родителя, чтобы избежать утечек памяти
|
# Устанавливаем родителя, чтобы избежать утечек памяти
|
||||||
self.command_process = QProcess(self.command_dialog)
|
self.command_process = QProcess(self.command_dialog)
|
||||||
self.command_process.setProcessChannelMode(QProcess.MergedChannels)
|
self.command_process.setProcessChannelMode(QProcess.MergedChannels)
|
||||||
@@ -2332,6 +2703,55 @@ class WineHelperGUI(QMainWindow):
|
|||||||
self.installed_global_action_widget.setVisible(False)
|
self.installed_global_action_widget.setVisible(False)
|
||||||
self.install_button.setText(f"Установить «{display_name}»")
|
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):
|
def install_current_script(self):
|
||||||
"""Устанавливает текущий выбранный скрипт"""
|
"""Устанавливает текущий выбранный скрипт"""
|
||||||
if not self.current_script:
|
if not self.current_script:
|
||||||
@@ -2499,10 +2919,11 @@ class WineHelperGUI(QMainWindow):
|
|||||||
QMessageBox.critical(self.install_dialog, "Ошибка", f"Не удалось запустить установку:\n{str(e)}")
|
QMessageBox.critical(self.install_dialog, "Ошибка", f"Не удалось запустить установку:\n{str(e)}")
|
||||||
self.cleanup_process()
|
self.cleanup_process()
|
||||||
|
|
||||||
def append_log(self, text, is_error=False, add_newline=True):
|
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 hasattr(self, 'log_output'): return
|
if not log_widget:
|
||||||
cursor = self.log_output.textCursor()
|
return
|
||||||
|
cursor = log_widget.textCursor()
|
||||||
cursor.movePosition(QTextCursor.End)
|
cursor.movePosition(QTextCursor.End)
|
||||||
|
|
||||||
if is_error:
|
if is_error:
|
||||||
@@ -2513,9 +2934,13 @@ class WineHelperGUI(QMainWindow):
|
|||||||
formatted_text = f"{text}\n" if add_newline else text
|
formatted_text = f"{text}\n" if add_newline else text
|
||||||
cursor.insertText(formatted_text)
|
cursor.insertText(formatted_text)
|
||||||
|
|
||||||
self.log_output.ensureCursorVisible()
|
log_widget.ensureCursorVisible()
|
||||||
QApplication.processEvents()
|
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):
|
def _process_log_line(self, line_with_delimiter):
|
||||||
"""Обрабатывает одну строку лога, управляя заменой строк прогресса."""
|
"""Обрабатывает одну строку лога, управляя заменой строк прогресса."""
|
||||||
is_progress_line = '\r' in 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
|
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):
|
def handle_process_output(self):
|
||||||
"""Обрабатывает вывод процесса, корректно отображая однострочный прогресс."""
|
"""Обрабатывает вывод процесса, корректно отображая однострочный прогресс."""
|
||||||
new_data = self.install_process.readAllStandardOutput().data().decode('utf-8', errors='ignore')
|
new_data = self.install_process.readAllStandardOutput().data().decode('utf-8', errors='ignore')
|
||||||
@@ -2604,6 +3065,27 @@ class WineHelperGUI(QMainWindow):
|
|||||||
# Кнопка прервать
|
# Кнопка прервать
|
||||||
self.btn_abort.setEnabled(False)
|
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:
|
if self.install_process:
|
||||||
|
Reference in New Issue
Block a user