diff --git a/winehelper b/winehelper index 78dd9b8..a575300 100755 --- a/winehelper +++ b/winehelper @@ -1179,6 +1179,16 @@ init_wineprefix () { # добавление ассоциаций файлов для запуска нативного приложения из wine # пример переменной: WH_XDG_OPEN="txt doc pdf" check_variables WH_XDG_OPEN "0" + # Сохраняем старые ассоциации, чтобы потом удалить те, что больше не нужны + local old_xdg_open + if [[ -f "$WINEPREFIX/last.conf" ]]; then + old_xdg_open=$(grep "WH_XDG_OPEN=" "$WINEPREFIX/last.conf" | sed -e 's/.*WH_XDG_OPEN="//' -e 's/"$//') + fi + + # Если переменная WH_XDG_OPEN была установлена извне (например, из GUI), + # то мы должны принудительно установить ее значение в "0", если она пуста, + # чтобы корректно удалить старые ассоциации. + [[ -z "$WH_XDG_OPEN" ]] && WH_XDG_OPEN="0" local WRAPPER="${WH_TMP_DIR}/wh-xdg-open.sh" local XDG_OPEN_REG="Software\Classes\xdg-open\shell\open\command" if [[ $WH_XDG_OPEN != "0" ]] ; then @@ -1201,13 +1211,25 @@ init_wineprefix () { # добавляем новую команду xdg-open в реестр get_and_set_reg_file --add "$XDG_OPEN_REG" '@=' 'REG_SZ' "$WRAPPER %1" "system" + # Удаляем старые ассоциации, которых нет в новом списке + if [[ -n "$old_xdg_open" ]]; then + for old_ext in $old_xdg_open; do + if ! echo " $WH_XDG_OPEN " | grep -q " $old_ext "; then + get_and_set_reg_file --delete "Software\Classes\.$old_ext" '@=' + fi + done + fi + # добавляем ассоциации файлов для запуска с помощью xdg-open for ext in $WH_XDG_OPEN ; do get_and_set_reg_file --add "Software\Classes\.$ext" '@=' 'REG_SZ' "xdg-open" "system" done print_info "Используются ассоциации с нативными приложениями для файлов: \"$WH_XDG_OPEN\"" else - # удаление команды xdg-open из реестра + # удаление всех ассоциаций + for old_ext in $old_xdg_open; do + get_and_set_reg_file --delete "Software\Classes\.$old_ext" '@=' + done get_and_set_reg_file --delete "$XDG_OPEN_REG" '@=' # удаяем скрипт-обёртку try_remove_file "$WRAPPER" @@ -1348,6 +1370,13 @@ init_database () { . "$WHDB_FILE" elif check_prefix_var && [[ -f "$WINEPREFIX/last.conf" ]] ; then print_info "Найдены настройки из предыдущего использования префикса: $WINEPREFIX" + # Сохраняем значение WH_XDG_OPEN, если оно было установлено извне (например, из GUI). + # Это предотвращает его перезапись старым значением из last.conf. + # Используем `declare -p` для надежного определения, была ли переменная установлена, + # даже если она пустая (что означает "удалить все ассоциации"). + if declare -p WH_XDG_OPEN &>/dev/null; then + wh_xdg_open_from_env="$WH_XDG_OPEN" + fi cat "$WINEPREFIX/last.conf" . "$WINEPREFIX/last.conf" else @@ -1356,10 +1385,19 @@ init_database () { } prepair_wine () { + # Объявляем переменную здесь, чтобы она была доступна после вызова init_database + local wh_xdg_open_from_env + if [[ -n "$INSTALL_SCRIPT_NAME" ]] then print_info "Используются настройки из скрипта установки: $INSTALL_SCRIPT_NAME" else init_database fi + # Восстанавливаем значение WH_XDG_OPEN, если оно было установлено извне. + # Проверяем, была ли переменная сохранена, а не ее значение. + # Это позволяет корректно передать пустую строку. + if declare -p wh_xdg_open_from_env &>/dev/null; then + export WH_XDG_OPEN="$wh_xdg_open_from_env" + fi init_wine_ver init_wineprefix use_winetricks diff --git a/winehelper_gui.py b/winehelper_gui.py index e25768d..eec3f65 100644 --- a/winehelper_gui.py +++ b/winehelper_gui.py @@ -12,7 +12,7 @@ import hashlib from functools import partial 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, QInputDialog) + QListWidget, QListWidgetItem, QGridLayout, QFrame, QDialog, QTextBrowser, QInputDialog, QDialogButtonBox) from PyQt5.QtCore import Qt, QProcess, QSize, QTimer, QProcessEnvironment, QPropertyAnimation, QEasingCurve from PyQt5.QtGui import QIcon, QFont, QTextCursor, QPixmap, QPainter, QDesktopServices from PyQt5.QtNetwork import QLocalServer, QLocalSocket @@ -1332,6 +1332,75 @@ class CreatePrefixDialog(QDialog): self.accept() +class FileAssociationsDialog(QDialog): + """Диалог для управления ассоциациями файлов (WH_XDG_OPEN).""" + + def __init__(self, current_associations, parent=None): + super().__init__(parent) + self.setWindowTitle("Настройка ассоциаций файлов") + self.setMinimumWidth(450) + self.setModal(True) + + self.new_associations = current_associations + + layout = QVBoxLayout(self) + layout.setSpacing(10) # Добавляем вертикальный отступ между виджетами + + info_label = QLabel( + "Укажите расширения файлов, которые должны открываться нативными
" + "приложениями Linux. Чтобы удалить все ассоциации, очистите поле.

" + "Пример: pdf docx txt" + ) + info_label.setWordWrap(True) + info_label.setTextFormat(Qt.RichText) + layout.addWidget(info_label) + + self.associations_edit = QLineEdit() + # Если ассоциации не заданы (значение "0"), поле будет пустым, чтобы показать подсказку + if current_associations != "0": + self.associations_edit.setText(current_associations) + + self.associations_edit.setPlaceholderText("Введите расширения через пробел...") + layout.addWidget(self.associations_edit) + + # Запрещенные расширения + forbidden_label = QLabel( + "Запрещено использовать: cpl, dll, exe, lnk, msi" + ) + forbidden_label.setTextFormat(Qt.RichText) # Включаем обработку HTML + layout.addWidget(forbidden_label) + + button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + button_box.accepted.connect(self.validate_and_accept) + button_box.rejected.connect(self.reject) + layout.addWidget(button_box) + + def validate_and_accept(self): + """Проверяет введенные данные перед закрытием.""" + forbidden_extensions = {"cpl", "dll", "exe", "lnk", "msi"} + + # Получаем введенные расширения, очищаем от лишних пробелов + input_text = self.associations_edit.text().lower().strip() + entered_extensions = {ext.strip() for ext in input_text.split() if ext.strip()} + + found_forbidden = entered_extensions.intersection(forbidden_extensions) + + if found_forbidden: + msg_box = QMessageBox(self) + msg_box.setIcon(QMessageBox.Warning) + msg_box.setWindowTitle("Недопустимые расширения") + msg_box.setTextFormat(Qt.RichText) + msg_box.setText( + "Следующие расширения запрещены и не могут быть использованы:

" + f"{', '.join(sorted(list(found_forbidden)))}" + ) + msg_box.exec_() + return + + # Сохраняем результат в виде отсортированной строки + self.new_associations = " ".join(sorted(list(entered_extensions))) + self.accept() + class ComponentVersionSelectionDialog(QDialog): """Диалог для выбора версии компонента (DXVK, VKD3D).""" @@ -2119,6 +2188,13 @@ class WineHelperGUI(QMainWindow): self.vkd3d_manage_button.setToolTip("Установка или удаление определенной версии vkd3d-proton в префиксе.") management_layout.addWidget(self.vkd3d_manage_button, 5, 1) + self.file_associations_button = QPushButton("Ассоциации файлов") + self.file_associations_button.setMinimumHeight(32) + self.file_associations_button.clicked.connect(self.open_file_associations_manager) + self.file_associations_button.setToolTip( + "Настройка открытия определенных типов файлов с помощью нативных приложений Linux.") + management_layout.addWidget(self.file_associations_button, 6, 0, 1, 2) + # --- Правая сторона: Информационный блок и кнопки установки --- right_column_widget = QWidget() right_column_layout = QVBoxLayout(right_column_widget) @@ -2151,7 +2227,7 @@ class WineHelperGUI(QMainWindow): right_column_layout.setStretch(0, 1) # Информационное окно растягивается right_column_layout.setStretch(1, 0) # Группа кнопок не растягивается - management_layout.addWidget(right_column_widget, 0, 2, 6, 1) + management_layout.addWidget(right_column_widget, 0, 2, 7, 1) management_layout.setColumnStretch(0, 1) management_layout.setColumnStretch(1, 1) @@ -2358,8 +2434,9 @@ class WineHelperGUI(QMainWindow): "VKD3D_VER": ("Версия VKD3D", lambda v: v if v else "Не установлено"), "WINEESYNC": ("ESync", lambda v: "Включен" if v == "1" else "Выключен"), "WINEFSYNC": ("FSync", lambda v: "Включен" if v == "1" else "Выключен"), + "WH_XDG_OPEN": ("Ассоциации файлов", lambda v: v if v and v != "0" else "Не заданы"), } - display_order = ["WINEPREFIX", "WINEARCH", "WH_WINE_USE", "BASE_PFX", "DXVK_VER", "VKD3D_VER", "WINEESYNC", "WINEFSYNC"] + display_order = ["WINEPREFIX", "WINEARCH", "WH_WINE_USE", "BASE_PFX", "DXVK_VER", "VKD3D_VER", "WINEESYNC", "WINEFSYNC", "WH_XDG_OPEN"] html_content = f'

' html_content += f"Имя: {html.escape(prefix_name)}
" @@ -2659,6 +2736,64 @@ class WineHelperGUI(QMainWindow): if exit_code == 0: self.update_prefix_info_display(prefix_name) + def open_file_associations_manager(self): + """Открывает диалог для управления ассоциациями файлов.""" + prefix_name = self.current_managed_prefix_name + if not prefix_name: + QMessageBox.warning(self, "Ошибка", "Сначала выберите префикс.") + return + + current_associations = self._get_prefix_component_version(prefix_name, "WH_XDG_OPEN") or "" + + dialog = FileAssociationsDialog(current_associations, self) + if dialog.exec_() == QDialog.Accepted: + new_associations = dialog.new_associations + # Запускаем обновление, только если значение изменилось + if new_associations != current_associations: + self.run_update_associations_command(prefix_name, new_associations) + + def run_update_associations_command(self, prefix_name, new_associations): + """Выполняет команду обновления ассоциаций файлов.""" + prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name) + + self.command_dialog = QDialog(self) + self.command_dialog.setWindowTitle("Обновление ассоциаций файлов") + 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( + lambda exit_code, exit_status: self._handle_component_install_finished( + prefix_name, exit_code, exit_status + ) + ) + + env = QProcessEnvironment.systemEnvironment() + env.insert("WINEPREFIX", prefix_path) + # Устанавливаем новую переменную окружения для скрипта + env.insert("WH_XDG_OPEN", new_associations) + self.command_process.setProcessEnvironment(env) + + args = ["init-prefix"] + 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 create_launcher_for_prefix(self): """ Открывает диалог для создания ярлыка для приложения внутри выбранного префикса.