diff --git a/auto_completion/bash_completion/winehelper b/auto_completion/bash_completion/winehelper index 0c78907..83c8109 100644 --- a/auto_completion/bash_completion/winehelper +++ b/auto_completion/bash_completion/winehelper @@ -4,7 +4,7 @@ _winehelper_completions() { COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" - opts="--help --version --debug install installed -r -i remove-all --clear-pfx killall remove-prefix backup-prefix restore-prefix --changelog changelog" + opts="--help --version --debug install installed -r -i remove-all --clear-pfx killall remove-prefix backup-prefix restore-prefix create-prefix --changelog changelog" wine_cmd="winecfg winereg winefile wineconsole winetricks desktop regedit explorer cmd run" case "${prev}" in diff --git a/auto_completion/zsh_completion/_winehelper b/auto_completion/zsh_completion/_winehelper index f094e47..fc12c01 100644 --- a/auto_completion/zsh_completion/_winehelper +++ b/auto_completion/zsh_completion/_winehelper @@ -14,6 +14,7 @@ _winehelper() { 'remove-all[Удалить WineHelper и все связанные данные]' '--clear-pfx[Очистить префикс \[имя_префикса\]]' 'killall[Убить все процессы]' + 'create-prefix[Создать новый префикс]' 'remove-prefix[Удалить префикс и все связанные данные]' 'backup-prefix[Создать резерную копию префикса]' 'restore-prefix[восстановить префикс из резервной копии "путь/до/whpack"]' @@ -55,6 +56,8 @@ _winehelper() { remove-prefix|backup-prefix) _get_prefixes ;; + create-prefix) + ;; restore-prefix) _files ;; diff --git a/sha256sum.list b/sha256sum.list old mode 100755 new mode 100644 index 3455209..2d44485 --- a/sha256sum.list +++ b/sha256sum.list @@ -1,5 +1,4 @@ ##### WINE ##### - 009c95bfe2df3f9264c9c5092f3e30ea7a168dd7869046058a718a70739602d4 wine_wh_tflex_10-9_amd64.tar.xz 3571c40a787f0386e0160b5d471b1bf11fa2a235b497156cd38198b90bcb4a9f wine_x_tkg_10-0_i586.tar.xz 71d6fdfd23d7988471d345c68e81699b8af931352b03dd216424b398f63c7b12 wine_x_tkg_10-0_amd64.tar.xz @@ -9,6 +8,7 @@ e0a84bb4908c3927954d7eef6b8ac7212e442b8c107d000c6890fec340f96183 wine-9.0.14-al 61bec1230b37b8fcc69fd45f848b44fd88cc41fcdd5dc3080336d7da63660f40 wine-7.16.1-alt1-amd64.tar.xz 6fea17fd131f57c2ebf7ca4c60d3c5a9e819afe16e5d0b77ecb750da99ae0e38 wine-7.16.1-alt1-i586.tar.xz +##### WINE_LG ##### 765e52484f06169909b179f4884d04ae4b440ff868e4b5722e64fc7d7a036ad9 WINE_LG_10-10-1.tar.xz 7a5128fcebbeed652ba36c5135821fd2c82493e87d715e8bc63e3210ba7ee849 WINE_LG_10-11.tar.xz 373beeb85c1c8163e752a32a453dcf575dd7ecf971726cecb3195e330d094925 WINE_LG_10-12.tar.xz @@ -25,6 +25,7 @@ da79f89b1fc3175f113bf5ec35fc2755550159034736cff086f2165480dd6e0f WINE_LG_9-12-2 51ff56e435a90eeff13ffbbac543a820d1df14d65f2e3b760ad100bc946e4bfc WINE_LG_9-7.tar.xz 09901b17a7aaa13e5c1a1f99ab82c9f0d223aa2f5c9ac94938e5a9a0088bb244 WINE_LG_9-9.tar.xz +##### PROTON_LG ##### d0f5b42096bc5ed379a26e415d99963f849129d5cec1ba9134b1ee50100b049b PROTON_LG_10-10.tar.xz d87eb914ce7e5cab47e6b480aa400f8c7840f39a22d585bf35092a033ac09201 PROTON_LG_10-13.tar.xz 05ca56607af7cee30b4797ad6c6a111d38a52c9d5c2184d30eec2650f1585b58 PROTON_LG_10-15.tar.xz @@ -44,13 +45,13 @@ e09c5da9ddffc5d390de7689df1778f279cc84a946665cc76ce77861010b2604 PROTON_LG_9-4. 56eae794a48aa7322ce5a636c490974d7f2516528d3153991fb033b0fa5fe3cf PROTON_LG_9-5.tar.xz 82263e1d0ab16130f15d419f2661fc5a7baf29193b9eeeb7eec3f01a3e54a1b3 PROTON_LG_9-7.tar.xz +##### PROTON_STEAM ##### 09e6e386a87710996d634e08650bbc733365df65d3491a4372f06cf4e815a8b2 PROTON_STEAM_6.3-8.tar.xz 2f8bdfeb75e5427886aac7d2a599a1ee8900e50d5873a0e620d5703707bf4271 PROTON_STEAM_7.0-5.tar.xz b33aca95664067c9eff06a2b4380992d43a5d31880ff970fb340e851671be625 PROTON_STEAM_8.0-4.tar.xz 9a20af28f4213e4f1c532b79a20e6a95ef872130784fc306510637342c6f58a1 PROTON_STEAM_8.0-5.tar.xz ##### DXVK ##### - d1a86ca53ab7e156e1252d3cf7eed3c66fe0651699cfc9c4ba152024261d8258 dxvk-1.10.1-967.tar.xz 98411effbc1b3611b11c4ca3af2ad29400b08e1c8c380791d6351600a06fb3c7 dxvk-1.10.3-28.tar.xz 061568b51ce99d285ef2d0f1f31aab65b3e6fd34747073859c5a719bec2a7afd dxvk-1.6.1.tar.xz @@ -100,7 +101,6 @@ c02b565d2fca1dc4066fb58acfcdd3919386fdbb01b30cfe181d4dd02ac5f1bb dxvk-2.7-5.tar 5f97deb1eeb97ed41d0539264c0ca98e8841f79ec59684f32e7f1ca5c29a109b dxvk-sarek-1.11.0.tar.xz ##### VKD3D ##### - df1a940d2e072a884524f66a16e0f0ef74048bc6a6642b96eb257528a522109f vkd3d-proton-1.1-2602.tar.xz bc86b06af83054e25bad21fe4bec4c10538837221fe847470571df7d556d355b vkd3d-proton-1.1-2967.tar.xz 41bd2465015f069ef2d378e42a0f906fd40f1ce70e1602b36e445209b16e0d50 vkd3d-proton-1.1-3088.tar.xz @@ -154,7 +154,6 @@ edf16d2b37bc77d121d5d81b06b60d3f694e0060c6606e729ceab30de3d27466 vkd3d-proton-s 405bfe3b7c7f80034837c05656535053305727ee4bf1d993521b67b71d08ebc6 extra_fonts_v01.tar.xz ##### PREFIX ##### - 0e86cd9958d1bd1bfac99e23165a091a4ea2fa3e693c551f69aabeed5f681fea defpfx_x86_v01.tar.xz # create with wine_x_tkg_10-0_amd64 (universal user: xuser and isolate_home by default) # winetricks msxml3 msxml4 msxml6 andale arial comicsans courier georgia impact times trebuchet verdana webdings corefonts wsh57 vcrun6 mdac27 jet40 gdiplus lucida tahoma ucrtbase2019 vcrun2019 diff --git a/winehelper b/winehelper index 512039c..81e8f80 100755 --- a/winehelper +++ b/winehelper @@ -811,10 +811,20 @@ init_wine_ver () { export WINEDIR="$WH_DIST_DIR/$WH_WINE_USE" if [[ ! -d "$WINEDIR" ]] ; then - local wine_package="$WH_TMP_DIR/$WH_WINE_USE.tar.xz" - try_download "$CLOUD_URL/$WH_WINE_USE.tar.xz" "$wine_package" check256sum + local download_url wine_package + download_url="$CLOUD_URL/$WH_WINE_USE.tar.xz" + wine_package="$WH_TMP_DIR/$WH_WINE_USE.tar.xz" + + try_download "$download_url" "$wine_package" "check256sum" unpack "$wine_package" "$WH_DIST_DIR/" try_remove_file "$wine_package" + + # Управление структурой подкаталога Proton "files", перемещая содержимое вверх + if [[ -d "$WINEDIR/files" ]]; then + print_info "Обнаружена структура каталогов Proton, исправляем пути..." + mv "$WINEDIR"/files/* "$WINEDIR/" + rmdir "$WINEDIR/files" + fi fi [[ ! -f "$WINEDIR/version" ]] && echo "$WH_WINE_USE" > "$WINEDIR/version" @@ -1465,6 +1475,238 @@ remove_prefix() { fi } +select_wine_version() { + local sha256_file="$DATA_PATH/sha256sum.list" + [[ ! -f "$sha256_file" ]] && fatal "Файл с версиями WINE/Proton не найден: $sha256_file" + + if [[ "$WINEARCH" == "win64" ]]; then + print_info "Фильтруем версии для 64-битного префикса..." + else # win32 + print_info "Фильтруем версии для 32-битного префикса..." + fi + + local options=() + local total_versions_found=0 + + # --- System --- + local system_wine_display_name="system" + if command -v wine &>/dev/null; then + local system_wine_version + system_wine_version=$(wine --version 2>/dev/null) + [[ -n "$system_wine_version" ]] && system_wine_display_name="$system_wine_version" + fi + options+=("--- System ---") + options+=("$system_wine_display_name") + + # --- Other versions from sha256sum.list --- + local current_group="" + local group_versions=() + + flush_group() { + if [[ ${#group_versions[@]} -gt 0 ]]; then + IFS=$'\n' sorted_versions=($(sort -Vr <<<"${group_versions[*]}")) + unset IFS + options+=("${sorted_versions[@]}") + ((total_versions_found+=${#group_versions[@]})) + group_versions=() + fi + } + + while IFS= read -r line; do + if [[ "$line" =~ ^#+[[:space:]]([^#[:space:]]+)[[:space:]]#* ]]; then + flush_group + current_group="${BASH_REMATCH[1]}" + # Отображаем только группы, которые являются сборками WINE или PROTON + case "$current_group" in + WINE|WINE_LG|PROTON_LG|PROTON_STEAM) + local pretty_key=$(echo "$current_group" | tr '_' ' ' | sed -e "s/\b\(.\)/\u\1/g") + options+=("--- $pretty_key ---") + ;; + *) + current_group="" + ;; + esac + elif [[ -n "$current_group" ]] && [[ "$line" =~ [a-f0-9]{64} ]]; then + local filename=$(echo "$line" | awk '{print $2}') + local version_name=${filename%.tar.xz} + + if [[ "$WINEARCH" == "win64" ]]; then + if [[ "$version_name" =~ (amd64|x86_64|wow64) ]] || ! [[ "$version_name" =~ i[3-6]86 ]]; then + group_versions+=("$version_name") + fi + else # win32 + if [[ "$version_name" =~ i[3-6]86 ]]; then + group_versions+=("$version_name") + fi + fi + fi + done < "$sha256_file" + flush_group + + if [[ $total_versions_found -eq 0 ]]; then + print_warning "Не найдено подходящих версий WINE/Proton для архитектуры $WINEARCH." + print_warning "Будет использована версия по умолчанию: $WH_WINE_USE" + return + fi + + local selectable_options=("Отмена") + local display_groups=() + local current_group_items=() + local choice_idx=0 + + flush_current_group() { + if ((${#current_group_items[@]} > 0)); then + # Объединяйте элементы с помощью уникального разделителя для последующего разделения + display_groups+=("$(IFS='@@@'; echo "${current_group_items[*]}")") + current_group_items=() + fi + } + + current_group_items+=(" 0) Отмена создания префикса") + + for opt in "${options[@]}"; do + if [[ "$opt" == "---"* ]]; then + flush_current_group + display_groups+=("$opt") + else + ((choice_idx++)) + current_group_items+=(" ${choice_idx}) $opt") + selectable_options+=("$opt") + fi + done + flush_current_group # Очистка последней группы + + print_info "Выберите версию WINE/Proton для $WINEARCH префикса:" + + local first_block=true + for group_data in "${display_groups[@]}"; do + if [[ "$group_data" == "---"* ]]; then + if [[ "$first_block" = false ]]; then + echo + fi + echo "$group_data" + else + local items_to_print=() + IFS='@@@' read -r -a items_to_print <<< "$group_data" + + local num_items=${#items_to_print[@]} + local term_width=${COLUMNS:-80} + local max_len=0 + for item in "${items_to_print[@]}"; do + (( ${#item} > max_len )) && max_len=${#item} + done + + ((max_len+=2)) + local num_cols=$(( term_width / max_len )) + (( num_cols = num_cols > 0 ? num_cols : 1 )) + local num_rows=$(( (num_items + num_cols - 1) / num_cols )) + + for ((i=0; i= 0 && user_choice <= max_choice )); then + if [[ "$user_choice" == "0" ]]; then + print_info "Создание префикса отменено." + exit 0 + fi + local selected_opt + selected_opt="${selectable_options[$user_choice]}" + if [[ "$selected_opt" == "$system_wine_display_name" ]]; then + export WH_WINE_USE="system" + else + export WH_WINE_USE="$selected_opt" + fi + break + else + print_error "Неверный выбор. Введите число от 0 до $max_choice." + fi + done +} + +create_prefix() { + print_info "Существующие префиксы:" + local prefixes=() + for prefix in "$WH_PREFIXES_DIR"/*; do + if [[ -d "$prefix" ]] ; then + prefixes+=("$(basename "$prefix")") + echo " - $(basename "$prefix")" + fi + done + + if [[ ${#prefixes[@]} -eq 0 ]]; then + print_info "Нет существующих префиксов." + fi + echo + + read -p "Введите имя для нового префикса или 0 для отмены (по умолчанию: default): " prefix_name + if [[ "$prefix_name" == "0" ]] ; then + print_info "Создание префикса отменено." + exit 0 + fi + + prefix_name=${prefix_name:-default} + + if [[ ! "$prefix_name" =~ ^[a-zA-Z0-9_.-]+$ ]] ; then + fatal "Имя префикса может содержать только латинские буквы, цифры, точки, дефисы и подчеркивания" + fi + + if [[ -d "$WH_PREFIXES_DIR/$prefix_name" ]] ; then + fatal "Префикс с именем '$prefix_name' уже существует. Создание отменено." + fi + + print_info "Создается префикс с именем: \"$prefix_name\"" + + print_info "Выберите разрядность префикса:" + echo " 0) Отмена создания префикса" + echo " 1) 32-bit" + echo " 2) 64-bit" + echo + local arch_choice + read -p "Ваш выбор [0-2] (по умолчанию 1): " arch_choice + case "${arch_choice:-1}" in + 0) print_info "Создание префикса отменено." ; exit 0 ;; + 1) export WINEARCH="win32" ;; + 2) export WINEARCH="win64" ;; + *) fatal "Неверный выбор. Операция отменена." ;; + esac + + select_wine_version + + print_info "Выберите тип создаваемого префикса:" + echo " 0) Отмена создания префикса" + echo " 1) Чистый префикс (без библиотек)" + echo " 2) С рекомендуемыми библиотеками" + echo + local pfx_type_choice + read -p "Ваш выбор [0-2] (по умолчанию 1): " pfx_type_choice + case "${pfx_type_choice:-1}" in + 0) print_info "Создание префикса отменено." ; exit 0 ;; + 1) export BASE_PFX="none" ;; + 2) ;; # Оставляем BASE_PFX пустым, чтобы init_wineprefix использовал значение по умолчанию + *) fatal "Неверный выбор. Операция отменена." ;; + esac + + export WINEPREFIX="$WH_PREFIXES_DIR/$prefix_name" + + if ! init_wine_ver || ! init_wineprefix; then + fatal "Ошибка инициализации префикса." + fi + + print_ok "Префикс '$prefix_name' (${WINEARCH}) успешно создан." +} + remove_winehelper () { local answer if [[ $1 =~ --force|-y ]] ; then @@ -1748,6 +1990,7 @@ wh_info () { installed список установленных программ run [программа] запуск программы (отладка) remove-all удалить WineHelper и все связанные данные + create-prefix создать префикс remove-prefix [имя_префикса] удалить префикс и все связанные данные backup-prefix [имя_префикса] создать резервную копию префикса restore-prefix \"путь/до/whpack\" восстановить префикс из резервной копии @@ -1796,8 +2039,10 @@ case "$arg1" in backup-prefix) backup_prefix "$@" ;; restore-prefix) restore_prefix "$@" ;; remove-all) remove_winehelper "$@" ;; + create-prefix) create_prefix "$@" ;; remove-prefix) remove_prefix "$@" ;; create-base-pfx) create_base_pfx "$@" ;; + init-prefix) prepair_wine ; wait_wineserver ;; *) if [[ -f "$arg1" ]] ; then WIN_FILE_EXEC="$(readlink -f "$arg1")" diff --git a/winehelper_gui.py b/winehelper_gui.py index 2895c8a..81a3526 100644 --- a/winehelper_gui.py +++ b/winehelper_gui.py @@ -10,11 +10,11 @@ import time import json import hashlib from functools import partial -from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,QPushButton, QLabel, QTabWidget, - QTextEdit, QFileDialog, QMessageBox, QLineEdit, QCheckBox, QStackedWidget, QScrollArea, +from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,QPushButton, QLabel, QTabWidget, QTabBar, + 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 @@ -424,10 +424,11 @@ class WinetricksManagerDialog(QDialog): "Для переустановки компонента: Выделите его в списке и нажмите кнопку «Переустановить»." ) - def __init__(self, prefix_path, winetricks_path, parent=None): + def __init__(self, prefix_path, winetricks_path, parent=None, wine_executable=None): super().__init__(parent) self.prefix_path = prefix_path self.winetricks_path = winetricks_path + self.wine_executable = wine_executable or 'wine' self.initial_states = {} self.apply_process = None self.installation_finished = False @@ -587,6 +588,7 @@ class WinetricksManagerDialog(QDialog): env = QProcessEnvironment.systemEnvironment() env.insert("WINEPREFIX", self.prefix_path) + env.insert("WINE", self.wine_executable) # Отключаем winemenubuilder, чтобы избежать зависаний, связанных с 'wineserver -w'. # Это известная проблема при запуске winetricks из ГУИ. process.setProcessEnvironment(env) @@ -827,6 +829,7 @@ class WinetricksManagerDialog(QDialog): self.apply_process.setProcessChannelMode(QProcess.MergedChannels) env = QProcessEnvironment.systemEnvironment() env.insert("WINEPREFIX", self.prefix_path) + env.insert("WINE", self.wine_executable) self.apply_process.setProcessEnvironment(env) self.apply_process.readyReadStandardOutput.connect(lambda: self.log_output.append(self.apply_process.readAllStandardOutput().data().decode('utf-8', 'ignore').strip())) self.apply_process.finished.connect(self.on_apply_finished) @@ -837,13 +840,8 @@ class WinetricksManagerDialog(QDialog): # 1. Проверяем, была ли отмена пользователем if self.user_cancelled: self._log("\n=== Установка прервана пользователем. ===") - self._show_message_box("Отмена", "Установки компонентов прервана пользователем.", - QMessageBox.Warning, {"buttons": {"Да": QMessageBox.AcceptRole}}) - - # Сбрасываем флаг и восстанавливаем UI - self.user_cancelled = False - self.apply_button.setEnabled(True) - self.close_button.setEnabled(True) + # Процесс завершен, теперь можно безопасно закрыть диалог. + self.close() return # 2. Обрабатываем реальную ошибку @@ -889,8 +887,10 @@ class WinetricksManagerDialog(QDialog): {"buttons": {"Да": QMessageBox.YesRole, "Нет": QMessageBox.NoRole}, "default": "Нет"}) if reply == "Да": self.user_cancelled = True + self.log_output.append("\n=== Прерывание установки... Ожидание завершения процесса... ===") self.apply_process.terminate() # Попытка мягкого завершения - event.accept() # Разрешаем закрытие + # Запрещаем закрытие. on_apply_finished обработает его после завершения процесса. + event.ignore() else: event.ignore() # Запрещаем закрытие else: @@ -1011,11 +1011,228 @@ class ScriptParser: except Exception as e: return f"Ошибка чтения файла: {str(e)}" +class WineVersionSelectionDialog(QDialog): + """Диалог для выбора версии Wine/Proton с группировкой.""" + + def __init__(self, architecture, parent=None): + super().__init__(parent) + self.architecture = architecture + self.selected_version = None + self.wine_versions_data = {} + self.system_wine_display_name = "Системная версия" + self.selected_display_text = None + + 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.""" + 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) + + QApplication.processEvents() + + self._parse_sha256_list() + self.populate_ui() + + self.refresh_button.setEnabled(True) + self.version_tabs.setEnabled(True) + + def _parse_sha256_list(self): + """Парсит sha256sum.list для получения списка версий.""" + sha256_path = os.path.join(Var.DATA_PATH, "sha256sum.list") + if not os.path.exists(sha256_path): + QMessageBox.warning(self, "Ошибка", f"Файл с версиями не найден:\n{sha256_path}") + self.wine_versions_data = {} + return + + self.wine_versions_data = {} + current_group = None + + try: + with open(sha256_path, 'r', encoding='utf-8') as f: + for line in f: + line = line.strip() + if not line: + continue + + match = re.match(r'^#+\s+([A-Z_]+)\s+#*$', line) + if match: + group_name = match.group(1) + allowed_groups = {"WINE", "WINE_LG", "PROTON_LG", "PROTON_STEAM"} + # Отображаем только группы, которые являются сборками WINE или PROTON + if group_name in allowed_groups: + current_group = group_name + if current_group not in self.wine_versions_data: + self.wine_versions_data[current_group] = [] + else: + current_group = None + continue + + if current_group and re.match(r'^[a-f0-9]{64}', line): + parts = line.split(maxsplit=1) + if len(parts) == 2: + filename = parts[1] + if filename.endswith('.tar.xz'): + version_name = filename[:-7] + self.wine_versions_data[current_group].append(version_name) + except IOError as e: + QMessageBox.warning(self, "Ошибка", f"Не удалось прочитать файл версий:\n{e}") + self.wine_versions_data = {} + + def populate_ui(self): + """Заполняет UI отфильтрованными версиями.""" + self.version_tabs.clear() + + if not self.wine_versions_data: + error_widget = QWidget() + error_layout = QVBoxLayout(error_widget) + error_label = QLabel("Список версий пуст или не удалось его загрузить.") + error_label.setAlignment(Qt.AlignCenter) + error_layout.addWidget(error_label) + self.version_tabs.addTab(error_widget, "Ошибка") + return + + 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 --- + if shutil.which('wine'): + self.system_wine_display_name = "Системная версия" + 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" + self.system_wine_display_name = version_line + except (FileNotFoundError, subprocess.CalledProcessError) as e: + print(f"Не удалось получить версию системного wine: {e}") + # Если wine возвращает ошибку, используем имя по умолчанию "Системная версия" + + self._create_version_tab("Системный", [(self.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, []) + + filtered_versions = [] + for name in sorted(versions, 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 + if version_name == 'system': + self.selected_display_text = self.system_wine_display_name + else: + self.selected_display_text = version_name + self.accept() + class WineHelperGUI(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("WineHelper") - self.setMinimumSize(950, 500) + self.setMinimumSize(950, 550) if Var.WH_ICON_PATH and os.path.exists(Var.WH_ICON_PATH): self.setWindowIcon(QIcon(Var.WH_ICON_PATH)) @@ -1062,31 +1279,54 @@ class WineHelperGUI(QMainWindow): self.current_selected_app = None self.icon_animators = {} self.previous_tab_index = 0 + self.selected_wine_version_value = None + self.created_prefixes_info = {} # Хранит информацию о префиксах, созданных на вкладке + self.current_managed_prefix_name = None # Имя префикса, выбранного в выпадающем списке + self.pending_prefix_info = None # Временное хранилище информации о создаваемом префиксе + # Добавим путь к файлу состояния + self.config_dir = os.path.join(os.path.expanduser("~"), ".config", "winehelper") + os.makedirs(self.config_dir, exist_ok=True) + self.state_file = os.path.join(self.config_dir, "gui_state.json") + + # 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) - self.main_layout = QHBoxLayout() + self.main_layout = QVBoxLayout() self.main_widget.setLayout(self.main_layout) - # Создаем табы - self.tabs = QTabWidget() - self.main_layout.addWidget(self.tabs, stretch=1) + # Создаем кастомную панель вкладок и виджет со страницами + self.tab_bar = QTabBar() + self.stacked_widget = QStackedWidget() + self.main_layout.addWidget(self.tab_bar) + + # Горизонтальный layout для страниц и инфо-панели + content_layout = QHBoxLayout() + content_layout.addWidget(self.stacked_widget, stretch=1) # Создаем панель информации о скрипте self.create_info_panel() - self.main_layout.addWidget(self.info_panel, stretch=1) + content_layout.addWidget(self.info_panel, stretch=1) + + self.main_layout.addLayout(content_layout) # Фиксируем минимальные размеры - self.tabs.setMinimumWidth(520) + self.stacked_widget.setMinimumWidth(520) self.info_panel.setMinimumWidth(415) # Вкладки self.create_auto_install_tab() self.create_manual_install_tab() self.create_installed_tab() + self.create_prefix_tab() self.create_help_tab() + # Загружаем состояние после создания всех виджетов + self._load_state() + # Инициализируем состояние, которое будет использоваться для логов self._reset_log_state() @@ -1094,9 +1334,10 @@ class WineHelperGUI(QMainWindow): self.update_installed_apps() # Соединяем сигнал смены вкладок с функцией - self.tabs.currentChanged.connect(self.on_tab_changed) + self.tab_bar.currentChanged.connect(self.stacked_widget.setCurrentIndex) + self.tab_bar.currentChanged.connect(self.on_tab_changed) # Устанавливаем начальное состояние видимости панели - self.on_tab_changed(self.tabs.currentIndex()) + self.on_tab_changed(self.tab_bar.currentIndex()) def activate(self): """ @@ -1109,6 +1350,11 @@ class WineHelperGUI(QMainWindow): self.raise_() self.activateWindow() + def add_tab(self, widget, title): + """Добавляет вкладку в кастомный TabBar и страницу в StackedWidget.""" + self.tab_bar.addTab(title) + self.stacked_widget.addWidget(widget) + def _reset_info_panel_to_default(self, tab_name): """Сбрасывает правую панель в состояние по умолчанию для указанной вкладки.""" if tab_name == "Автоматическая установка": @@ -1150,7 +1396,7 @@ class WineHelperGUI(QMainWindow): def on_tab_changed(self, index): """Скрывает или показывает панель информации в зависимости от активной вкладки.""" # Очищаем поле поиска на вкладке, которую покинули - previous_widget = self.tabs.widget(self.previous_tab_index) + previous_widget = self.stacked_widget.widget(self.previous_tab_index) if previous_widget: # Ищем QLineEdit в дочерних элементах search_edit = previous_widget.findChild(QLineEdit) @@ -1160,20 +1406,20 @@ class WineHelperGUI(QMainWindow): # Обновляем индекс предыдущей вкладки для следующего переключения self.previous_tab_index = index - current_tab_text = self.tabs.tabText(index) + current_tab_text = self.tab_bar.tabText(index) # Сбрасываем растяжение к состоянию по умолчанию: # растягивается виджет с описанием (индекс 1), а не виджет с действиями (индекс 4) 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): """Создает правую панель с информацией о скрипте""" @@ -1528,7 +1774,7 @@ class WineHelperGUI(QMainWindow): buttons_list = [] self._populate_install_grid(grid_layout, scripts, script_folder, buttons_list) - self.tabs.addTab(tab_widget, tab_title) + self.add_tab(tab_widget, tab_title) return scripts, buttons_list, grid_layout, search_edit, scroll_area @@ -1563,7 +1809,397 @@ class WineHelperGUI(QMainWindow): 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, "Установленные") + self.add_tab(installed_tab, "Установленные") + + def open_wine_version_dialog(self): + """Открывает диалог выбора версии Wine.""" + architecture = "win32" if self.arch_win32_radio.isChecked() else "win64" + dialog = WineVersionSelectionDialog(architecture, self) + if dialog.exec_() == QDialog.Accepted and dialog.selected_version: + self.wine_version_edit.setText(dialog.selected_display_text) + self.selected_wine_version_value = dialog.selected_version + + def clear_wine_version_selection(self): + """ + Сбрасывает выбор версии Wine при смене архитектуры, + чтобы заставить пользователя выбрать заново. + """ + self.wine_version_edit.clear() + self.selected_wine_version_value = None + + 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("Например: my_prefix") + form_layout.addRow("Имя нового префикса:", self.prefix_name_edit) + + arch_widget = QWidget() + arch_layout = QHBoxLayout(arch_widget) + arch_layout.setContentsMargins(0, 0, 0, 0) + 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) + form_layout.addRow("Разрядность:", arch_widget) + + type_widget = QWidget() + type_layout = QHBoxLayout(type_widget) + type_layout.setContentsMargins(0, 0, 0, 0) + self.type_clean_radio = QRadioButton("Чистый") + self.type_clean_radio.setToolTip("Создает пустой префикс Wine без каких-либо дополнительных компонентов.") + self.type_recommended_radio = QRadioButton("С рекомендуемыми библиотеками") + tooltip_text = "Устанавливает базовый набор компонентов, необходимый для большинства приложений" + self.type_recommended_radio.setToolTip(tooltip_text) + self.type_clean_radio.setChecked(True) + type_layout.addWidget(self.type_clean_radio) + type_layout.addWidget(self.type_recommended_radio) + form_layout.addRow("Наполнение:", type_widget) + + 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("Версия Wine/Proton:", version_layout) + + 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.addLayout(form_layout) + layout.addWidget(self.create_prefix_button) + + # --- Контейнер для выбора и управления созданными префиксами --- + self.management_container_groupbox = QGroupBox("Управление созданными префиксами") + self.management_container_groupbox.setVisible(False) # Скрыт, пока нет префиксов + container_layout = QVBoxLayout(self.management_container_groupbox) + + selector_layout = QHBoxLayout() + self.created_prefix_selector = QComboBox() + self.created_prefix_selector.setPlaceholderText("Выберите префикс для управления") + self.created_prefix_selector.currentIndexChanged.connect(self.on_created_prefix_selected) + selector_layout.addWidget(self.created_prefix_selector, 1) + + self.delete_prefix_button = QPushButton() + self.delete_prefix_button.setIcon(QIcon.fromTheme("user-trash")) + self.delete_prefix_button.setToolTip("Удалить выбранный префикс") + self.delete_prefix_button.setEnabled(False) + self.delete_prefix_button.clicked.connect(self.delete_selected_prefix) + selector_layout.addWidget(self.delete_prefix_button) + + container_layout.addLayout(selector_layout) + + # --- Виджет для управления выбранным префиксом --- + self.prefix_management_groupbox = QWidget() + self.prefix_management_groupbox.setEnabled(False) + management_layout = QGridLayout(self.prefix_management_groupbox) + management_layout.setSpacing(5) + + # --- Левая сторона: Кнопки --- + self.prefix_winetricks_button = QPushButton("Менеджер компонентов") + self.prefix_winetricks_button.setMinimumHeight(32) + self.prefix_winetricks_button.clicked.connect( + lambda: self.open_winetricks_manager(prefix_name=self.current_managed_prefix_name)) + self.prefix_winetricks_button.setToolTip( + "Установка компонентов, библиотек и шрифтов в префикс с помощью Winetricks.") + management_layout.addWidget(self.prefix_winetricks_button, 0, 0) + + self.prefix_winecfg_button = QPushButton("Редактор настроек") + self.prefix_winecfg_button.setMinimumHeight(32) + self.prefix_winecfg_button.clicked.connect( + lambda: self._run_wine_util('winecfg', prefix_name=self.current_managed_prefix_name)) + self.prefix_winecfg_button.setToolTip( + "Запуск утилиты winecfg для настройки параметров Wine (версия Windows, диски, аудио и т.д.).") + management_layout.addWidget(self.prefix_winecfg_button, 0, 1) + + self.prefix_regedit_button = QPushButton("Редактор реестра") + self.prefix_regedit_button.setMinimumHeight(32) + self.prefix_regedit_button.clicked.connect( + lambda: self._run_wine_util('regedit', prefix_name=self.current_managed_prefix_name)) + self.prefix_regedit_button.setToolTip( + "Запуск редактора реестра Wine (regedit) для просмотра и изменения ключей реестра в префиксе.") + management_layout.addWidget(self.prefix_regedit_button, 1, 0) + + self.prefix_uninstaller_button = QPushButton("Удаление программ") + self.prefix_uninstaller_button.setMinimumHeight(32) + self.prefix_uninstaller_button.clicked.connect( + lambda: self._run_wine_util('uninstaller', prefix_name=self.current_managed_prefix_name)) + self.prefix_uninstaller_button.setToolTip( + "Запуск стандартного деинсталлятора Wine для удаления установленных в префикс Windows-программ.") + management_layout.addWidget(self.prefix_uninstaller_button, 1, 1) + + self.prefix_cmd_button = QPushButton("Командная строка") + self.prefix_cmd_button.setMinimumHeight(32) + self.prefix_cmd_button.clicked.connect(lambda: self._run_wine_util('cmd', prefix_name=self.current_managed_prefix_name)) + self.prefix_cmd_button.setToolTip("Запуск командной строки (cmd) в окружении выбранного префикса.") + management_layout.addWidget(self.prefix_cmd_button, 2, 0) + + self.prefix_winefile_button = QPushButton("Файловый менеджер") + self.prefix_winefile_button.setMinimumHeight(32) + self.prefix_winefile_button.clicked.connect( + lambda: self._run_wine_util('winefile', prefix_name=self.current_managed_prefix_name)) + self.prefix_winefile_button.setToolTip("Запуск файлового менеджера Wine (winefile) для просмотра файлов внутри префикса.") + management_layout.addWidget(self.prefix_winefile_button, 2, 1) + + # --- Правая сторона: Информационный блок --- + self.prefix_info_display = QTextBrowser() + self.prefix_info_display.setReadOnly(True) + self.prefix_info_display.setFrameStyle(QFrame.StyledPanel) + management_layout.addWidget(self.prefix_info_display, 0, 2, 3, 1) + + management_layout.setColumnStretch(0, 1) + management_layout.setColumnStretch(1, 1) + management_layout.setColumnStretch(2, 2) + + # --- Separator and Installer --- + separator = QFrame() + separator.setFrameShape(QFrame.HLine) + separator.setFrameShadow(QFrame.Sunken) + management_layout.addWidget(separator, 3, 0, 1, 3) + + install_group = QWidget() + install_layout = QVBoxLayout(install_group) + install_layout.setContentsMargins(0, 5, 0, 0) + install_layout.setSpacing(5) + + install_path_layout = QHBoxLayout() + self.prefix_install_path_edit = QLineEdit() + self.prefix_install_path_edit.setPlaceholderText("Путь к .exe или .msi файлу...") + install_path_layout.addWidget(self.prefix_install_path_edit) + + self.prefix_browse_button = QPushButton("Обзор...") + self.prefix_browse_button.clicked.connect(self.browse_for_prefix_installer) + install_path_layout.addWidget(self.prefix_browse_button) + install_layout.addLayout(install_path_layout) + + self.prefix_install_button = QPushButton("Установить приложение в префикс") + self.prefix_install_button.setEnabled(False) + self.prefix_install_button.clicked.connect(self.run_prefix_installer) + install_layout.addWidget(self.prefix_install_button) + + management_layout.addWidget(install_group, 4, 0, 1, 3) + + container_layout.addWidget(self.prefix_management_groupbox) + layout.addWidget(self.management_container_groupbox) + layout.addStretch() + self.add_tab(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.prefix_name_edit.textChanged.connect(self.on_prefix_name_edited) + self.wine_version_edit.textChanged.connect(self.update_create_prefix_button_state) + self.prefix_install_path_edit.textChanged.connect(self.update_prefix_install_button_state) + + def _load_state(self): + """Загружает последнее состояние GUI из файла.""" + if not os.path.exists(self.state_file): + return + try: + with open(self.state_file, 'r', encoding='utf-8') as f: + state = json.load(f) + + loaded_prefixes = state.get('created_prefixes_info', {}) + if not loaded_prefixes: + return + + # Блокируем сигналы, чтобы избежать преждевременного срабатывания + self.created_prefix_selector.blockSignals(True) + self.created_prefix_selector.clear() + self.created_prefixes_info.clear() + + for prefix_name, prefix_info in loaded_prefixes.items(): + prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name) + if os.path.isdir(prefix_path): + self.created_prefixes_info[prefix_name] = prefix_info + self.created_prefix_selector.addItem(prefix_name) + + self.created_prefix_selector.blockSignals(False) + + if self.created_prefix_selector.count() > 0: + self.management_container_groupbox.setVisible(True) + last_managed = state.get('current_managed_prefix_name') + index = self.created_prefix_selector.findText(last_managed) + if index != -1: + self.created_prefix_selector.setCurrentIndex(index) + else: + self.created_prefix_selector.setCurrentIndex(0) # Выбираем первый, если предыдущий не найден + else: + self.management_container_groupbox.setVisible(False) + + except (IOError, json.JSONDecodeError, TypeError) as e: + print(f"Предупреждение: не удалось загрузить состояние GUI: {e}") + + def _save_state(self): + """Сохраняет текущее состояние GUI в файл.""" + state = { + 'created_prefixes_info': self.created_prefixes_info, + 'current_managed_prefix_name': self.current_managed_prefix_name + } + try: + with open(self.state_file, 'w', encoding='utf-8') as f: + json.dump(state, f, indent=2, ensure_ascii=False) + except IOError as e: + print(f"Предупреждение: не удалось сохранить состояние GUI: {e}") + + def on_created_prefix_selected(self, index): + """Обрабатывает выбор префикса из выпадающего списка.""" + if index == -1: + self.current_managed_prefix_name = None + self._setup_prefix_management_panel(None) + self.delete_prefix_button.setEnabled(False) + else: + prefix_name = self.created_prefix_selector.itemText(index) + self.current_managed_prefix_name = prefix_name + self._setup_prefix_management_panel(prefix_name) + self.delete_prefix_button.setEnabled(True) + self._save_state() + + def delete_selected_prefix(self): + """Удаляет префикс, выбранный в выпадающем списке на вкладке 'Создать префикс'.""" + prefix_name = self.current_managed_prefix_name + if not prefix_name: + return + + msg_box = QMessageBox(self) + msg_box.setIcon(QMessageBox.Question) + msg_box.setWindowTitle('Подтверждение удаления') + msg_box.setText(f'Вы уверены, что хотите удалить префикс "{prefix_name}"?\n\n' + 'Это действие необратимо и удалит все данные внутри префикса.') + + yes_button = msg_box.addButton("Да", QMessageBox.YesRole) + no_button = msg_box.addButton("Нет", QMessageBox.NoRole) + msg_box.setDefaultButton(no_button) + + msg_box.exec_() + + # Если пользователь нажал не "Да", выходим + if msg_box.clickedButton() != yes_button: + return + + prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name) + + try: + if os.path.isdir(prefix_path): + shutil.rmtree(prefix_path) + if prefix_name in self.created_prefixes_info: + del self.created_prefixes_info[prefix_name] + index_to_remove = self.created_prefix_selector.findText(prefix_name) + if index_to_remove != -1: + self.created_prefix_selector.removeItem(index_to_remove) + QMessageBox.information(self, "Успех", f"Префикс '{prefix_name}' был успешно удален.") + except Exception as e: + QMessageBox.critical(self, "Ошибка удаления", f"Не удалось удалить префикс '{prefix_name}':\n{e}") + + def on_prefix_name_edited(self, text): + """Сбрасывает состояние управления префиксом, когда пользователь вводит новое имя.""" + if text: + if self.created_prefix_selector.currentIndex() != -1: + self.created_prefix_selector.setCurrentIndex(-1) + + def _setup_prefix_management_panel(self, prefix_name): + """Настраивает панель управления префиксом на основе текущего состояния.""" + if prefix_name and prefix_name in self.created_prefixes_info: + self.prefix_management_groupbox.setEnabled(True) + self.update_prefix_info_display(prefix_name) + else: + self.prefix_management_groupbox.setEnabled(False) + self.prefix_info_display.clear() + self.prefix_install_path_edit.clear() + self.update_prefix_install_button_state() + + def update_prefix_info_display(self, prefix_name): + """Обновляет информационный блок для созданного префикса.""" + info = self.created_prefixes_info.get(prefix_name) + if not info: + self.prefix_info_display.clear() + return + + html_content = f""" +

+ Имя: {html.escape(info['name'])}
+ Архитектура: {html.escape(info['arch'])}
+ Версия Wine: {html.escape(info['wine_version'])}
+ Путь: {html.escape(info['path'])}

""" + self.prefix_info_display.setHtml(html_content) + + def browse_for_prefix_installer(self): + """Открывает диалог выбора файла для установки в созданный префикс.""" + file_path, _ = QFileDialog.getOpenFileName( + self, + "Выберите установочный файл", + os.path.expanduser("~"), + "Исполняемые файлы (*.exe *.msi);;Все файлы (*)" + ) + if file_path: + self.prefix_install_path_edit.setText(file_path) + + def update_prefix_install_button_state(self): + """Обновляет состояние кнопки установки в префикс.""" + path_ok = bool(self.prefix_install_path_edit.text().strip()) + prefix_selected = self.current_managed_prefix_name is not None + self.prefix_install_button.setEnabled(path_ok and prefix_selected) + + def run_prefix_installer(self): + """Запускает установку файла в выбранный префикс.""" + prefix_name = self.current_managed_prefix_name + installer_path = self.prefix_install_path_edit.text().strip() + + if not prefix_name: + QMessageBox.warning(self, "Ошибка", "Не выбран префикс для установки.") + return + if not installer_path or not os.path.isfile(installer_path): + QMessageBox.warning(self, "Ошибка", "Указан неверный путь к установочному файлу.") + return + + prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name) + wine_executable = self._get_wine_executable_for_prefix(prefix_name) + + 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_dialog) + self.command_process.setProcessChannelMode(QProcess.MergedChannels) + self.command_process.readyReadStandardOutput.connect(self._handle_command_output) + self.command_process.finished.connect(self._handle_prefix_install_finished) + + env = QProcessEnvironment.systemEnvironment() + env.insert("WINEPREFIX", prefix_path) + self.command_process.setProcessEnvironment(env) + + args = [installer_path] + self.command_log_output.append(f"Запуск установки: {shlex.quote(wine_executable)} {shlex.quote(installer_path)}") + self.command_process.start(wine_executable, args) + self.command_dialog.exec_() def create_help_tab(self): """Создает вкладку 'Справка' с подвкладками""" @@ -1683,7 +2319,133 @@ class WineHelperGUI(QMainWindow): changelog_layout.addWidget(changelog_text) help_subtabs.addTab(changelog_tab, "История изменений") - self.tabs.addTab(help_tab, "Справка") + self.add_tab(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 + + # Сбрасываем выбор в выпадающем списке, чтобы панель управления скрылась на время создания + if self.created_prefix_selector.count() > 0: + self.created_prefix_selector.setCurrentIndex(-1) + + 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.selected_wine_version_value + wine_use_display = self.wine_version_edit.text() + + # Сохраняем информацию для отображения после создания + self.pending_prefix_info = { + 'name': prefix_name, + 'path': prefix_path, + 'arch': "32-bit" if wine_arch == "win32" else "64-bit", + 'type': 'Чистый' if base_pfx else 'С рекомендуемыми библиотеками', + 'wine_version': wine_use_display + } + + 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_display}\nТип: {'Чистый' if base_pfx else 'С рекомендуемыми библиотеками'}\n\n" + "="*40 + "\n") + self.command_log_output.textCursor().insertText(f"Выполнение: {shlex.quote(self.winehelper_path)} {' '.join(shlex.quote(a) for a in args)}\n") + QApplication.processEvents() + 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: + # Добавляем новый префикс в список и выбираем его + if self.pending_prefix_info: + self.created_prefixes_info[prefix_name] = self.pending_prefix_info + self.pending_prefix_info = None + + if self.created_prefix_selector.findText(prefix_name) == -1: + self.created_prefix_selector.addItem(prefix_name) + + self.created_prefix_selector.setCurrentText(prefix_name) + + if not self.management_container_groupbox.isVisible(): + self.management_container_groupbox.setVisible(True) + + self._save_state() + self.prefix_name_edit.clear() + self.wine_version_edit.clear() + QMessageBox.information(self, "Успех", + f"Префикс '{prefix_name}' успешно создан.\n" + "Теперь вы можете управлять им, выбрав его из выпадающего списка.") def update_installed_apps(self): """Обновляет список установленных приложений в виде кнопок""" @@ -1741,6 +2503,20 @@ class WineHelperGUI(QMainWindow): self.installed_scroll_layout.addWidget(frame, row, column) self.installed_buttons.append(btn) + def _handle_prefix_install_finished(self, exit_code, exit_status): + """Обрабатывает завершение установки в префикс.""" + if exit_code == 0: + self.command_log_output.append("\n=== Установка успешно завершена ===") + else: + self.command_log_output.append(f"\n=== Ошибка выполнения (код: {exit_code}) ===") + + if self.command_process: + self.command_process.deleteLater() + self.command_process = None + self.command_close_button.setEnabled(True) + self.prefix_install_path_edit.clear() + self.update_installed_apps() + def _set_active_button(self, button_widget): """Устанавливает активную кнопку и обновляет стили ее обертки (QFrame).""" # Сброс стиля предыдущей активной кнопки @@ -1891,7 +2667,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 +2710,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) @@ -1969,11 +2743,14 @@ class WineHelperGUI(QMainWindow): if msg_box.clickedButton() == yes_button: self._run_app_launcher(debug=True) - def open_winetricks_manager(self): + def open_winetricks_manager(self, prefix_name=None): """Открывает новый диалог для управления компонентами Winetricks.""" - prefix_name = self._get_prefix_name_for_selected_app() if not prefix_name: - QMessageBox.warning(self, "Менеджер Winetricks", "Сначала выберите установленное приложение, чтобы открыть менеджер для его префикса.") + prefix_name = self._get_prefix_name_for_selected_app() + + if not prefix_name: + QMessageBox.warning(self, "Менеджер Winetricks", + "Не удалось определить префикс. Выберите установленное приложение или создайте новый префикс.") return prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name) @@ -1997,23 +2774,13 @@ class WineHelperGUI(QMainWindow): QMessageBox.critical(self, "Ошибка", f"Скрипт winetricks не найден в директории:\n{winehelper_dir}") return - dialog = WinetricksManagerDialog(prefix_path, winetricks_path, self) + wine_executable = self._get_wine_executable_for_prefix(prefix_name) + dialog = WinetricksManagerDialog(prefix_path, winetricks_path, self, wine_executable=wine_executable) dialog.exec_() - def _run_wine_util(self, util_name): - """Запускает стандартную утилиту Wine для выбранного префикса.""" - prefix_name = self._get_prefix_name_for_selected_app() - if not prefix_name: - QMessageBox.warning(self, "Ошибка", "Сначала выберите установленное приложение.") - return - + def _get_wine_executable_for_prefix(self, prefix_name): + """Определяет и возвращает путь к исполняемому файлу wine для указанного префикса.""" prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name) - if not os.path.isdir(prefix_path): - QMessageBox.critical(self, "Ошибка", f"Каталог префикса не найден:\n{prefix_path}") - return - - # --- Определение используемой версии Wine --- - wine_executable = 'wine' # По умолчанию системный wine last_conf_path = os.path.join(prefix_path, "last.conf") wh_wine_use = None @@ -2022,7 +2789,6 @@ class WineHelperGUI(QMainWindow): with open(last_conf_path, 'r', encoding='utf-8') as f: for line in f: if 'WH_WINE_USE=' in line: - # Извлекаем значение из строки вида: export WH_WINE_USE="wine_ver" value = line.split('=', 1)[1].strip().strip('"\'') if value: wh_wine_use = value @@ -2033,12 +2799,28 @@ class WineHelperGUI(QMainWindow): if wh_wine_use and not wh_wine_use.startswith('system'): local_wine_path = os.path.join(Var.USER_WORK_PATH, "dist", wh_wine_use, "bin", "wine") if os.path.exists(local_wine_path): - wine_executable = local_wine_path - else: - QMessageBox.warning(self, "Предупреждение", - f"Локальная версия Wine '{wh_wine_use}' не найдена по пути:\n{local_wine_path}\n\n" - "Будет использована системная версия Wine.") - # --- Конец определения версии Wine --- + return local_wine_path + QMessageBox.warning(self, "Предупреждение", + f"Локальная версия Wine '{wh_wine_use}' не найдена по пути:\n{local_wine_path}\n\n" + "Будет использована системная версия Wine.") + return 'wine' # По умолчанию системный wine + + def _run_wine_util(self, util_name, prefix_name=None): + """Запускает стандартную утилиту Wine для выбранного префикса.""" + if not prefix_name: + prefix_name = self._get_prefix_name_for_selected_app() + + if not prefix_name: + QMessageBox.warning(self, "Ошибка", + "Не удалось определить префикс. Выберите установленное приложение или создайте новый префикс.") + return + + prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name) + if not os.path.isdir(prefix_path): + QMessageBox.critical(self, "Ошибка", f"Каталог префикса не найден:\n{prefix_path}") + return + + wine_executable = self._get_wine_executable_for_prefix(prefix_name) env = os.environ.copy() env["WINEPREFIX"] = prefix_path @@ -2332,6 +3114,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""" +
{escaped_license_content}
+ """) + except (FileNotFoundError, TypeError): + license_text.setHtml(f'

Лицензионные соглашения

Не удалось загрузить файл лицензионного соглашения по пути:
{Var.LICENSE_AGREEMENT_FILE}

') + except Exception as e: + license_text.setHtml(f'

Лицензионные соглашения

Произошла ошибка при чтении файла лицензии:
{str(e)}

') + + 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: @@ -2342,71 +3173,27 @@ class WineHelperGUI(QMainWindow): QMessageBox.warning(self, "Ошибка", "Укажите путь к установочному файлу") return + if not self._show_license_agreement_dialog(): + return # Пользователь отклонил лицензию + + self.installation_cancelled = False + # Создаем диалоговое окно установки self.install_dialog = QDialog(self) title_name = self._get_current_app_title() self.install_dialog.setWindowTitle(f"Установка «{title_name}»") 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_found = False - - 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""" -
{escaped_license_content}
- """) - license_found = True - except (FileNotFoundError, TypeError): - license_text.setHtml(f'

Лицензионные соглашения

Не удалось загрузить файл лицензионного соглашения по пути:
{Var.LICENSE_AGREEMENT_FILE}

') - except Exception as e: - print(f"Ошибка чтения файла лицензии: {str(e)}") - license_text.setHtml(f""" -

Лицензионные соглашения

-

Произошла ошибка при чтении файла лицензии:
{str(e)}

- """) - - 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.install_dialog.setAttribute(Qt.WA_DeleteOnClose) # Удалять диалог при закрытии + log_layout = QVBoxLayout(self.install_dialog) 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) + control_buttons = QWidget() + btn_layout = QHBoxLayout(control_buttons) self.btn_abort = QPushButton("Прервать") self.btn_abort.clicked.connect(self.abort_installation) btn_layout.addWidget(self.btn_abort) @@ -2415,25 +3202,16 @@ class WineHelperGUI(QMainWindow): 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) + log_layout.addWidget(control_buttons) # Назначение кастомного обработчика закрытия окна 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) - ) - - if not license_found: - self.license_checkbox.setEnabled(False) - self.install_dialog.show() + # Сразу же готовим и запускаем установку + self._prepare_installation() def _reset_log_state(self): """Сбрасывает состояние буфера и флага прогресса для лога установки.""" @@ -2442,7 +3220,6 @@ class WineHelperGUI(QMainWindow): def _prepare_installation(self): """Подготавливает и запускает процесс установки""" - self.stacked_widget.setCurrentIndex(1) self._reset_log_state() # Сбрасываем состояние для обработки лога winehelper_path = self.winehelper_path @@ -2499,10 +3276,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 +3291,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 +3334,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') @@ -2586,6 +3404,14 @@ class WineHelperGUI(QMainWindow): cursor.insertText("\n") self._reset_log_state() + + if hasattr(self, 'installation_cancelled') and self.installation_cancelled: + self.append_log("\n=== Установка была прервана. ===") + self.cleanup_process() + if self.install_dialog: + self.install_dialog.close() + return + if exit_code == 0 and exit_status == QProcess.NormalExit: self.append_log("\n=== Установка успешно завершена ===") # Создаем кастомный диалог, чтобы кнопка была на русском @@ -2603,12 +3429,28 @@ class WineHelperGUI(QMainWindow): self.btn_close.setEnabled(True) # Кнопка прервать self.btn_abort.setEnabled(False) + self.install_process = None - # Процесс завершен, можно запланировать его удаление и очистить ссылку, - # чтобы избежать утечек и висячих ссылок. - if self.install_process: - self.install_process.deleteLater() - self.install_process = None + 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) def handle_install_dialog_close(self, event): """Обрабатывает событие закрытия диалога установки.""" @@ -2628,11 +3470,10 @@ class WineHelperGUI(QMainWindow): msg_box.exec_() if msg_box.clickedButton() == yes_button: - self.append_log("\n=== Пользователь прервал установку через закрытие окна ===", is_error=True) - # Завершаем процесс. Сигнал finished вызовет handle_process_finished, - # который обновит состояние кнопок. + self.append_log("\n=== Пользователь прервал установку через закрытие окна. Ожидание... ===", is_error=True) + self.installation_cancelled = True self.install_process.terminate() - event.accept() # Разрешаем закрытие окна + event.ignore() # Запрещаем закрытие, handle_process_finished обработает его else: # Пользователь нажал "Нет", поэтому игнорируем событие закрытия event.ignore() @@ -2655,7 +3496,8 @@ class WineHelperGUI(QMainWindow): msg_box.exec_() if msg_box.clickedButton() == yes_button: self.append_log("\n=== Пользователь прервал установку ===", is_error=True) - self.cleanup_process() + self.installation_cancelled = True + self.install_process.terminate() def _handle_command_output(self): """Обрабатывает вывод для диалога команды"""