Compare commits

..

25 Commits

Author SHA1 Message Date
Mikhail Tergoev
d5f337e6b4 Merge branch 'minergenon-devel' 2025-09-29 23:54:59 +03:00
Sergey Palcheh
904c9c9895 revision of the sub-tab Authors 2025-09-29 21:24:37 +06:00
Sergey Palcheh
1d4ee1fd70 the prefix control display is enabled by default 2025-09-29 20:40:07 +06:00
Sergey Palcheh
02a2256c8c fixed the character input in the name of the prefix being created 2025-09-29 20:27:33 +06:00
Mikhail Tergoev
cbcdba204e TODO: system menu directory 2025-09-29 16:05:26 +03:00
Mikhail Tergoev
66c56f6ecf removed broken README.MD 2025-09-29 15:52:21 +03:00
Mikhail Tergoev
221b59eda7 added README.MD 2025-09-29 15:50:46 +03:00
Mikhail Tergoev
adf5f78360 kill_wine worked only with WH 2025-09-29 14:40:57 +03:00
Mikhail Tergoev
01f19cd94d first print_license_agreement before run_autoinstall 2025-09-29 14:23:31 +03:00
Mikhail Tergoev
117e497f94 Merge branch 'minergenon-devel' 2025-09-29 14:06:36 +03:00
Sergey Palcheh
3527846c6c added to the tray show/hide 2025-09-29 11:33:23 +06:00
Sergey Palcheh
553d427d66 added a gui tray 2025-09-28 21:26:39 +06:00
Sergey Palcheh
0f8f192634 added a prefix template creation button 2025-09-27 14:13:56 +06:00
Sergey Palcheh
7f64378670 improved the button for adding associations 2025-09-27 12:19:11 +06:00
Sergey Palcheh
165c4ee110 the agreement acceptance window has been removed when selecting the dxvk/vkd3d versions 2025-09-27 11:32:46 +06:00
Sergey Palcheh
843b90c1c2 the agreement acceptance window has been removed when selecting wine versions 2025-09-27 11:19:14 +06:00
Sergey Palcheh
e3ac6dd967 fixed closing applications when closing the gui 2025-09-27 11:08:26 +06:00
Mikhail Tergoev
5763749aa0 updated init dxvk/vkd3d and fixed download from tty 2025-09-26 14:44:14 +03:00
Mikhail Tergoev
b1f192b2ff fixed file associacion and always read last.conf 2025-09-25 15:04:04 +03:00
Mikhail Tergoev
42aa29d208 Merge branch 'minergenon-devel' 2025-09-25 13:10:01 +03:00
Mikhail Tergoev
3ad737e27d fixed nettest icon for GUI and added unpack tests 2025-09-25 12:42:19 +03:00
Sergey Palcheh
97996fb67b added a file association button 2025-09-25 13:29:56 +06:00
Sergey Palcheh
9f994a8cc3 added notification when closing the WH when the application is running 2025-09-24 12:16:58 +06:00
Sergey Palcheh
463306d0cf the window about the successful installation of components has been removed 2025-09-24 10:42:08 +06:00
Sergey Palcheh
940cface08 the status bar in the prefix component manager window has been removed 2025-09-24 10:38:02 +06:00
3 changed files with 470 additions and 139 deletions

View File

@@ -5,7 +5,6 @@ export PROG_URL="https://www.kpolyakov.spb.ru/prog/nettest/nettget.htm"
export WH_WINE_USE="wine_x_tkg_10-0_amd64"
export WINEPREFIX="nettest"
export PROG_NAME="NetTest"
export PROG_ICON="nettest"
export BASE_PFX="none"
export WINEARCH="win64"
export INSTALL_DLL=""
@@ -18,7 +17,13 @@ if [[ -f "$ZIP_FILE" ]] \
then
prepair_wine
PROG_PATH="$DRIVE_C/nettest"
unpack "$2" "$PROG_PATH"
if [[ $ZIP_FILE =~ "tests" ]] ; then
unpack "$2" "$PROG_PATH/tests"
print_info "Тесты $(basename "$ZIP_FILE") установлены."
exit 0
else
unpack "$2" "$PROG_PATH"
fi
cp -fr "$PROG_PATH/fonts/"* "$DRIVE_C/windows/Fonts/"

View File

@@ -126,6 +126,12 @@ WH_TESTINSTALL_DIR="$DATA_PATH/testinstall"
WH_WINETRICKS="$DATA_PATH/winetricks_$WINETRICKS_VERSION"
WH_MENU_DIR="$HOME/.local/share/applications/WineHelper"
# TODO: system menu directory
# /usr/share/desktop-directories/WineHelper.directory
# /etc/xdg/menus/applications-merged/WineHelper.menu
# user menu directory
WH_MENU_CATEGORY="$HOME/.local/share/desktop-directories/WineHelper.directory"
WH_MENU_CONFIG="$HOME/.config/menus/applications-merged/WineHelper.menu"
@@ -162,12 +168,10 @@ check_variables WINE_WIN_START "start /wait /high /unix"
check_variables WINE_CPU_TOPOLOGY "8"
check_variables USE_RENDERER "opengl" # opengl, damavand, proton
check_variables DXVK_VER "1.10.3-28"
check_variables DXVK_VER "none"
# check_variables DXVK_CONFIG_FILE "path/to/dxvk.conf"
check_variables VKD3D_VER "1.1-2602"
check_variables VKD3D_VER "none"
# check_variables VKD3D_LIMIT_TESS_FACTORS 64
# check_variables VKD3D_FEATURE_LEVEL "12_0"
@@ -395,10 +399,14 @@ print_license_agreement () {
}
try_download () {
if [[ $WH_USE_GUI == "1" ]] \
&& [[ $(ps -o command= -p "$PPID" | awk '{print $2}') =~ "$DATA_PATH/winehelper_gui.py" ]]
then print_ok "Соглашения приняты из графического интерфейса."
else print_license_agreement
if [[ $1 != "cloud" ]] ; then
if [[ $WH_USE_GUI == "1" ]] \
&& [[ $(ps -o command= -p "$PPID" | awk '{print $2}') =~ "$DATA_PATH/winehelper_gui.py" ]]
then print_ok "Соглашения приняты из графического интерфейса."
else print_license_agreement
fi
else
shift
fi
local download_file_url output_file output_file_name
download_file_url="${1// /%20}"
@@ -694,9 +702,11 @@ EOF
echo '#!/usr/bin/env bash'
echo "# cmd_name: $INSTALL_SCRIPT_NAME"
} > "$exe_file".whdb
grep -e "info_" -e "#####" -e "export" -e "var_" "$INSTALL_SCRIPT" \
| grep -vE "LAUNCH_PARAMETERS|AUTOINSTALL|WIN_FILE_EXEC|echo" \
grep -e "info_" -e "#####" -e "PROG_URL=" -e "WINEPREFIX=" -e "INSTALL_DLL=" \
-e "PROG_NAME=" -e "PROG_ICON=" -e "var_" "$INSTALL_SCRIPT" \
| awk '{$1=$1;print}' >> "$exe_file".whdb
print_info "Создан файл настроек для $exe_file"
fi
}
@@ -760,31 +770,25 @@ run_installed_programs () {
fi
}
init_wined3d () {
if [[ "$USE_RENDERER" != "proton" ]] ; then
WINED3D_FILES="d3d8 d3d9 d3d10_1 d3d10 d3d10core d3d11 dxgi d3d12 d3d12core"
for wined3dfiles in $WINED3D_FILES ; do
try_copy_wine_dll_to_pfx_64 "$wined3dfiles.dll"
try_copy_wine_dll_to_pfx_32 "$wined3dfiles.dll"
done
# if [[ "$USE_RENDERER" == "damavand" ]]
# then export WINE_D3D_CONFIG="renderer=vulkan"
# else export WINE_D3D_CONFIG="renderer=gl"
# fi
return 0
else
return 1
fi
copy_wined3d () {
for wined3dfiles in $1 ; do
try_copy_wine_dll_to_pfx_64 "$wined3dfiles.dll"
try_copy_wine_dll_to_pfx_32 "$wined3dfiles.dll"
done
}
init_dxvk () {
check_variables USE_DXVK_VER "$1"
DXVK_VER="$1"
if [[ $DXVK_VER == "none" ]] ; then
copy_wined3d "d3d8 d3d9 d3d10_1 d3d10 d3d10core d3d11 dxgi"
return 0
fi
get_dxvk() {
local DXVK_URL="$1"
local DXVK_VAR_VER="$2"
local DXVK_PACKAGE="${WH_VULKAN_LIBDIR}/${DXVK_VAR_VER}.tar.$(echo "${DXVK_URL#*.tar.}")"
if try_download "$DXVK_URL" "$DXVK_PACKAGE" check256sum \
if try_download cloud "$DXVK_URL" "$DXVK_PACKAGE" check256sum \
&& unpack "$DXVK_PACKAGE" "$WH_VULKAN_LIBDIR"
then
try_remove_file "$DXVK_PACKAGE"
@@ -793,36 +797,37 @@ init_dxvk () {
return 1
}
for DXVK_VAR_VER in "$USE_DXVK_VER" $@ ; do
if [[ ! -d "${WH_VULKAN_LIBDIR}/${DXVK_VAR_VER}" ]] ; then
get_dxvk "$CLOUD_URL/${DXVK_VAR_VER}.tar.xz" "$DXVK_VAR_VER"
fi
done
if [[ ! -d "${WH_VULKAN_LIBDIR}/${DXVK_VER}" ]] ; then
get_dxvk "$CLOUD_URL/${DXVK_VER}.tar.xz" "$DXVK_VER"
fi
if [[ "${WH_USE_WINE_DXGI}" == 1 ]] ; then
if [[ $WH_USE_WINE_DXGI == "1" ]] ; then
DXVK_FILES="d3d9 d3d10_1 d3d10 d3d11" # dxvk_config openvr_api_dxvk"
try_copy_wine_dll_to_pfx_64 "dxgi.dll"
try_copy_wine_dll_to_pfx_32 "dxgi.dll"
copy_wined3d "dxgi"
else
DXVK_FILES="d3d9 d3d10_1 d3d10 d3d11 dxgi" # dxvk_config openvr_api_dxvk"
fi
for dxvkfiles in $DXVK_FILES ; do
try_copy_other_dll_to_pfx_64 "${WH_VULKAN_LIBDIR}/${USE_DXVK_VER}/x64/$dxvkfiles.dll"
if try_copy_other_dll_to_pfx_32 "${WH_VULKAN_LIBDIR}/${USE_DXVK_VER}/x32/$dxvkfiles.dll"
try_copy_other_dll_to_pfx_64 "${WH_VULKAN_LIBDIR}/${DXVK_VER}/x64/$dxvkfiles.dll"
if try_copy_other_dll_to_pfx_32 "${WH_VULKAN_LIBDIR}/${DXVK_VER}/x32/$dxvkfiles.dll"
then var_winedlloverride_update "$dxvkfiles=n"
fi
done
}
init_vkd3d () {
check_variables USE_VKD3D_VER "$1"
VKD3D_VER="$1"
if [[ $VKD3D_VER == "none" ]] ; then
copy_wined3d "d3d12 d3d12core"
return 0
fi
get_vkd3d() {
local VKD3D_URL="$1"
local VKD3D_VAR_VER="$2"
local VKD3D_PACKAGE="${WH_VULKAN_LIBDIR}/${VKD3D_VAR_VER}.tar.$(echo "${VKD3D_URL#*.tar.}")"
if try_download "$VKD3D_URL" "$VKD3D_PACKAGE" check256sum \
if try_download cloud "$VKD3D_URL" "$VKD3D_PACKAGE" check256sum \
&& unpack "$VKD3D_PACKAGE" "$WH_VULKAN_LIBDIR"
then
try_remove_file "$VKD3D_PACKAGE"
@@ -831,16 +836,14 @@ init_vkd3d () {
return 1
}
for VKD3D_VAR_VER in "$USE_VKD3D_VER" $@ ; do
if [[ ! -d "${WH_VULKAN_LIBDIR}/${VKD3D_VAR_VER}" ]] ; then
get_vkd3d "$CLOUD_URL/${VKD3D_VAR_VER}.tar.xz" "$VKD3D_VAR_VER"
fi
done
if [[ ! -d "${WH_VULKAN_LIBDIR}/${VKD3D_VER}" ]] ; then
get_vkd3d "$CLOUD_URL/${VKD3D_VER}.tar.xz" "$VKD3D_VER"
fi
VKD3D_FILES="d3d12 d3d12core libvkd3d-shader-1 libvkd3d-1" # libvkd3d-proton-utils-3
for vkd3dfiles in $VKD3D_FILES ; do
try_copy_other_dll_to_pfx_64 "${WH_VULKAN_LIBDIR}/${USE_VKD3D_VER}/x64/$vkd3dfiles.dll"
if try_copy_other_dll_to_pfx_32 "${WH_VULKAN_LIBDIR}/${USE_VKD3D_VER}/x86/$vkd3dfiles.dll"
try_copy_other_dll_to_pfx_64 "${WH_VULKAN_LIBDIR}/${VKD3D_VER}/x64/$vkd3dfiles.dll"
if try_copy_other_dll_to_pfx_32 "${WH_VULKAN_LIBDIR}/${VKD3D_VER}/x86/$vkd3dfiles.dll"
then var_winedlloverride_update "$vkd3dfiles=n"
fi
done
@@ -855,7 +858,7 @@ init_wine_ver () {
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"
try_download cloud "$download_url" "$wine_package" "check256sum"
unpack "$wine_package" "$WH_DIST_DIR/"
try_remove_file "$wine_package"
@@ -908,7 +911,7 @@ init_wine_ver () {
CPCSP_PROXY_NAME="wine-cpcsp_proxy-$CPCSP_PROXY_VER"
CPCSP_PROXY_URL="$CLOUD_URL/$CPCSP_PROXY_NAME.tar.xz"
try_download "$CPCSP_PROXY_URL" "$WH_TMP_DIR/$CPCSP_PROXY_NAME.tar.xz" check256sum
try_download cloud "$CPCSP_PROXY_URL" "$WH_TMP_DIR/$CPCSP_PROXY_NAME.tar.xz" check256sum
unpack "$WH_TMP_DIR/$CPCSP_PROXY_NAME.tar.xz" "$WH_TMP_DIR"
cp -fr "$WH_TMP_DIR/$CPCSP_PROXY_NAME/"i386-* "$WINEDIR/lib/wine/"
@@ -1184,6 +1187,7 @@ init_wineprefix () {
# добавление ассоциаций файлов для запуска нативного приложения из wine
# пример переменной: WH_XDG_OPEN="txt doc pdf"
check_variables 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
@@ -1206,13 +1210,19 @@ init_wineprefix () {
# добавляем новую команду xdg-open в реестр
get_and_set_reg_file --add "$XDG_OPEN_REG" '@=' 'REG_SZ' "$WRAPPER %1" "system"
# удаляем старые ассоциации, которых нет в новом списке
sed -i '/@="xdg-open"/d' "$WINEPREFIX/system.reg"
# добавляем ассоциации файлов для запуска с помощью 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"
@@ -1274,7 +1284,7 @@ init_wineprefix () {
echo "# переменные последнего использования префикса:" > "$WINEPREFIX/last.conf"
for var in WH_WINE_USE BASE_PFX WINEARCH WH_WINDOWS_VER WINEESYNC WINEFSYNC \
STAGING_SHARED_MEMORY WINE_LARGE_ADDRESS_AWARE WH_USE_SHADER_CACHE WH_USE_WINE_DXGI \
WINE_CPU_TOPOLOGY USE_RENDERER DXVK_VER VKD3D_VER WH_XDG_OPEN WH_USE_MESA_GL_OVERRIDE
WINE_CPU_TOPOLOGY DXVK_VER VKD3D_VER WH_XDG_OPEN WH_USE_MESA_GL_OVERRIDE
do
echo "export $var=\"${!var}\"" >> "$WINEPREFIX/last.conf"
done
@@ -1324,7 +1334,8 @@ use_winetricks () {
}
kill_wine () {
wine_pids=$(ls -l /proc/*/exe 2>/dev/null | grep -E 'wine(64)?-preloader|wineserver' | awk -F/ '{print $3}')
wine_pids=$(ls -l /proc/*/exe 2>/dev/null | grep -E 'wine(64)?-preloader|wineserver' \
| grep "$USER_WORK_PATH" | awk -F/ '{print $3}')
for pw_kill_pids in ${wine_pids}; do
if ps cax | grep "${pw_kill_pids}" ; then
@@ -1351,12 +1362,12 @@ init_database () {
if [[ "$WHDB_FILE" != "0" ]] ; then
print_info "Используется файл настроек: $WHDB_FILE"
. "$WHDB_FILE"
elif check_prefix_var && [[ -f "$WINEPREFIX/last.conf" ]] ; then
fi
if check_prefix_var && [[ -f "$WINEPREFIX/last.conf" ]] ; then
print_info "Найдены настройки из предыдущего использования префикса: $WINEPREFIX"
cat "$WINEPREFIX/last.conf"
. "$WINEPREFIX/last.conf"
else
print_warning "Файл настроек не найден. Пропускаем."
fi
}
@@ -1365,16 +1376,13 @@ prepair_wine () {
then print_info "Используются настройки из скрипта установки: $INSTALL_SCRIPT_NAME"
else init_database
fi
init_wine_ver
init_wineprefix
use_winetricks
init_dxvk "$DXVK_VER"
init_vkd3d "$VKD3D_VER"
if init_wined3d ; then
:
else
init_dxvk "$DXVK_VER"
init_vkd3d "$VKD3D_VER"
fi
[[ "$MANGOHUD" == 1 ]] && MANGOHUD_RUN="mangohud"
}
@@ -1415,6 +1423,12 @@ wine_run_install () {
}
run_autoinstall () {
if [[ $WH_USE_GUI == "1" ]] \
&& [[ $(ps -o command= -p "$PPID" | awk '{print $2}') =~ "$DATA_PATH/winehelper_gui.py" ]]
then print_ok "Соглашения приняты из графического интерфейса."
else print_license_agreement
fi
if [[ $1 == "--clear-pfx" ]] ; then
export CLEAR_PREFIX="1"
shift
@@ -2155,16 +2169,14 @@ run_install_dxvk() {
fi
check_prefix_var
init_database
export DXVK_VER="$version"
init_wine_ver
init_wineprefix
if [[ "$version" == "none" ]] ; then
print_info "Удаление DXVK..."
init_wined3d
update_last_conf_var "DXVK_VER" ""
else
init_dxvk "$version"
update_last_conf_var "DXVK_VER" "$USE_DXVK_VER"
if [[ "$DXVK_VER" == "none" ]]
then print_info "Удаление DXVK..."
else print_info "Установка DXVK: $DXVK_VER"
fi
init_dxvk "$DXVK_VER"
wait_wineserver
}
@@ -2179,16 +2191,14 @@ run_install_vkd3d() {
fi
check_prefix_var
init_database
export VKD3D_VER="$version"
init_wine_ver
init_wineprefix
if [[ "$version" == "none" ]] ; then
print_info "Удаление VKD3D..."
init_wined3d
update_last_conf_var "VKD3D_VER" ""
else
init_vkd3d "$version"
update_last_conf_var "VKD3D_VER" "$USE_VKD3D_VER"
if [[ "$VKD3D_VER" == "none" ]]
then print_info "Удаление VKD3D..."
else print_info "Установка VKD3D: $VKD3D_VER"
fi
init_vkd3d "$VKD3D_VER"
wait_wineserver
}

View File

@@ -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, QTabBar,
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, QSystemTrayIcon, QMenu)
from PyQt5.QtCore import Qt, QProcess, QSize, QTimer, QProcessEnvironment, QPropertyAnimation, QEasingCurve
from PyQt5.QtGui import QIcon, QFont, QTextCursor, QPixmap, QPainter, QDesktopServices
from PyQt5.QtGui import QIcon, QFont, QTextCursor, QPixmap, QPainter, QCursor
from PyQt5.QtNetwork import QLocalServer, QLocalSocket
@@ -474,10 +474,9 @@ class WinetricksManagerDialog(QDialog):
self.log_output.setText(self.INFO_TEXT)
main_layout.addWidget(self.log_output)
# Кнопки управления
# Кнопки управления, выровненные по правому краю
button_layout = QHBoxLayout()
self.status_label = QLabel("Загрузка компонентов...")
button_layout.addWidget(self.status_label, 1)
button_layout.addStretch(1)
self.apply_button = QPushButton("Применить")
self.apply_button.setEnabled(False)
@@ -548,7 +547,6 @@ class WinetricksManagerDialog(QDialog):
def load_all_categories(self):
"""Запускает загрузку всех категорий."""
self.loading_count = len(self.categories)
self.category_statuses = {name: "загрузка..." for name in self.categories.keys()}
for internal_name in self.categories.values():
self._start_load_process(internal_name)
@@ -602,13 +600,6 @@ class WinetricksManagerDialog(QDialog):
process.finished.connect(partial(self._on_load_finished, category))
process.start(self.winetricks_path, [category, "list"])
def _update_status_label(self):
"""Обновляет текстовую метку состояния загрузки."""
status_parts = []
for name, status in self.category_statuses.items():
status_parts.append(f"{name}: {status}")
self.status_label.setText(" | ".join(status_parts))
def _parse_winetricks_log(self):
"""Читает winetricks.log и возвращает множество установленных компонентов."""
installed_verbs = set()
@@ -681,22 +672,15 @@ class WinetricksManagerDialog(QDialog):
if exit_code != 0 or exit_status != QProcess.NormalExit:
error_string = process.errorString() if process else "N/A"
self._log(f"--- Ошибка загрузки категории '{category}' (код: {exit_code}) ---", "red")
self.category_statuses[category_display_name] = "ошибка"
self._update_status_label() # Показываем ошибку в статусе
self._log(f"--- Ошибка загрузки категории '{category_display_name}' (код: {exit_code}) ---", "red")
if exit_status == QProcess.CrashExit:
self._log("--- Процесс winetricks завершился аварийно. ---", "red")
# По умолчанию используется "Неизвестная ошибка", которая не очень полезна.
if error_string != "Неизвестная ошибка":
self._log(f"--- Системная ошибка: {error_string} ---", "red")
self._log(output if output.strip() else "Winetricks не вернул вывод. Проверьте, что он работает корректно.")
self._log("--------------------------------------------------", "red")
else:
self.category_statuses[category_display_name] = "готово"
installed_verbs = self._parse_winetricks_log()
# Обновляем статус только если это была сетевая загрузка
if from_cache is None:
self._update_status_label()
found_items = self._parse_winetricks_list_output(output, installed_verbs, list_widget)
if from_cache is None: # Только если мы не читали из кэша
@@ -721,7 +705,6 @@ class WinetricksManagerDialog(QDialog):
self.loading_count -= 1
if self.loading_count == 0:
self.status_label.setText("Готово.")
self._update_ui_state()
def _on_item_changed(self, item):
@@ -862,11 +845,6 @@ class WinetricksManagerDialog(QDialog):
# 3. Обрабатываем успех
self._log("\n=== Все операции успешно завершены ===")
self._show_message_box("Успех",
"Операции с компонентами были успешно выполнены.",
QMessageBox.Information,
{"buttons": {"Да": QMessageBox.AcceptRole}})
self.apply_button.setEnabled(True)
self.reinstall_button.setEnabled(False) # Сбрасываем в неактивное состояние
self.close_button.setEnabled(True)
@@ -876,7 +854,6 @@ class WinetricksManagerDialog(QDialog):
search_edit.clear()
# Перезагружаем данные, чтобы обновить состояние
self.status_label.setText("Обновление данных...")
self.initial_states.clear()
self.load_all_categories()
self.installation_finished = True
@@ -1233,9 +1210,9 @@ class CreatePrefixDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.parent_gui = parent # Store reference to main window
self.parent_gui = parent # Сохранить ссылку на главное окно
self.setWindowTitle("Создание нового префикса")
self.setMinimumSize(500, 250)
self.setMinimumSize(680, 250)
self.setModal(True)
# Attributes to store results
@@ -1249,9 +1226,22 @@ class CreatePrefixDialog(QDialog):
form_layout = QFormLayout()
form_layout.setSpacing(10)
# Создаем виджет для поля ввода и предупреждения
name_input_widget = QWidget()
name_input_layout = QVBoxLayout(name_input_widget)
name_input_layout.setContentsMargins(0, 0, 0, 0)
name_input_layout.setSpacing(2)
self.prefix_name_edit = QLineEdit()
self.prefix_name_edit.setPlaceholderText("Например: my_prefix")
form_layout.addRow("<b>Имя нового префикса:</b>", self.prefix_name_edit)
name_input_layout.addWidget(self.prefix_name_edit)
self.name_warning_label = QLabel("Имя может содержать только латинские буквы, цифры, тире и знаки подчеркивания.")
self.name_warning_label.setStyleSheet("color: red;")
self.name_warning_label.setVisible(False)
name_input_layout.addWidget(self.name_warning_label)
form_layout.addRow("<b>Имя нового префикса:</b>", name_input_widget)
arch_widget = QWidget()
arch_layout = QHBoxLayout(arch_widget)
@@ -1308,7 +1298,7 @@ class CreatePrefixDialog(QDialog):
# Connect signals
self.arch_win32_radio.toggled.connect(self.clear_wine_version_selection)
self.prefix_name_edit.textChanged.connect(self.update_create_button_state)
self.prefix_name_edit.textChanged.connect(self.validate_prefix_name)
self.wine_version_edit.textChanged.connect(self.update_create_button_state)
def open_wine_version_dialog(self):
@@ -1324,11 +1314,28 @@ class CreatePrefixDialog(QDialog):
self.wine_version_edit.clear()
self.selected_wine_version_value = None
def validate_prefix_name(self, text):
"""Проверяет имя префикса в реальном времени и показывает/скрывает предупреждение."""
valid_pattern = r'^[a-zA-Z0-9_-]*$'
if re.match(valid_pattern, text):
self.name_warning_label.setVisible(False)
else:
# Удаляем недопустимые символы
cleaned_text = re.sub(r'[^a-zA-Z0-9_-]', '', text)
# Блокируем сигналы, чтобы избежать рекурсии при изменении текста
self.prefix_name_edit.blockSignals(True)
self.prefix_name_edit.setText(cleaned_text)
self.prefix_name_edit.blockSignals(False)
self.name_warning_label.setVisible(True)
self.update_create_button_state()
def update_create_button_state(self):
"""Включает или выключает кнопку 'Создать'."""
name_ok = bool(self.prefix_name_edit.text().strip())
version_ok = bool(self.wine_version_edit.text().strip())
self.create_button.setEnabled(name_ok and version_ok)
# Кнопка активна, только если имя валидно и версия выбрана
self.create_button.setEnabled(name_ok and version_ok and not self.name_warning_label.isVisible())
def accept_creation(self):
"""Валидирует данные, сохраняет их и закрывает диалог с успехом."""
@@ -1338,8 +1345,8 @@ class CreatePrefixDialog(QDialog):
QMessageBox.warning(self, "Ошибка", "Имя префикса не может быть пустым.")
return
if not re.match(r'^[a-zA-Z0-9_.-]+$', prefix_name):
QMessageBox.warning(self, "Ошибка", "Имя префикса может содержать только латинские буквы, цифры, точки, дефисы и подчеркивания.")
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)
@@ -1355,6 +1362,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(
"Укажите расширения файлов, которые должны открываться нативными<br>"
"приложениями Linux. Чтобы удалить все ассоциации, очистите поле.<br><br>"
"<b>Пример:</b> <code>pdf docx txt</code>"
)
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(
"<small><b>Запрещено использовать:</b> cpl, dll, exe, lnk, msi</small>"
)
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(
"Следующие расширения запрещены и не могут быть использованы:<br><br>"
f"<b>{', '.join(sorted(list(found_forbidden)))}</b>"
)
msg_box.exec_()
return
# Сохраняем результат в виде отсортированной строки
self.new_associations = " ".join(sorted(list(entered_extensions)))
self.accept()
class ComponentVersionSelectionDialog(QDialog):
"""Диалог для выбора версии компонента (DXVK, VKD3D)."""
@@ -1535,6 +1611,7 @@ class WineHelperGUI(QMainWindow):
self.current_managed_prefix_name = None # Имя префикса, выбранного в выпадающем списке
self.prefixes_before_install = set()
self.is_quitting = False # Флаг для корректного выхода из приложения
self.command_output_buffer = ""
self.command_last_line_was_progress = False
# Создаем главный виджет и layout
@@ -1601,6 +1678,50 @@ class WineHelperGUI(QMainWindow):
self.raise_()
self.activateWindow()
def create_tray_icon(self):
"""Создает и настраивает иконку в системном трее."""
if not QSystemTrayIcon.isSystemTrayAvailable():
print("Системный трей не доступен.")
return
self.tray_icon = QSystemTrayIcon(self)
icon_path = Var.WH_ICON_PATH
if icon_path and os.path.exists(icon_path):
pixmap = QPixmap(icon_path)
if not pixmap.isNull():
self.tray_icon.setIcon(QIcon(pixmap))
# Создаем и сохраняем меню как атрибут класса, чтобы оно не удалялось
self.tray_menu = QMenu(self)
toggle_visibility_action = self.tray_menu.addAction("Показать/Скрыть")
toggle_visibility_action.triggered.connect(self.toggle_visibility)
self.tray_menu.addSeparator()
quit_action = self.tray_menu.addAction("Выход")
quit_action.triggered.connect(self.quit_application)
self.tray_icon.activated.connect(self.on_tray_icon_activated)
self.tray_icon.show()
def on_tray_icon_activated(self, reason):
"""Обрабатывает клики по иконке в трее."""
# Показываем меню при левом клике
if reason == QSystemTrayIcon.Trigger:
# Получаем позицию курсора и показываем меню
self.tray_menu.popup(QCursor.pos())
def toggle_visibility(self):
"""Переключает видимость главного окна."""
if self.isVisible() and self.isActiveWindow():
self.hide()
else:
# Сначала скрываем, чтобы "сбросить" состояние, затем активируем.
# Это помогает обойти проблемы с фокусом и переключением рабочих столов.
self.hide()
self.activate()
def add_tab(self, widget, title):
"""Добавляет вкладку в кастомный TabBar и страницу в StackedWidget."""
self.tab_bar.addTab(title)
@@ -2042,7 +2163,7 @@ class WineHelperGUI(QMainWindow):
# --- Контейнер для выбора и управления созданными префиксами ---
self.management_container_groupbox = QGroupBox()
self.management_container_groupbox.setVisible(False) # Скрыт, пока нет префиксов
self.management_container_groupbox.setVisible(True) # Всегда виден
container_layout = QVBoxLayout(self.management_container_groupbox)
selector_layout = QHBoxLayout()
@@ -2051,6 +2172,13 @@ class WineHelperGUI(QMainWindow):
self.created_prefix_selector.currentIndexChanged.connect(self.on_created_prefix_selected)
selector_layout.addWidget(self.created_prefix_selector, 1)
self.create_base_pfx_button = QPushButton()
self.create_base_pfx_button.setIcon(QIcon.fromTheme("document-export"))
self.create_base_pfx_button.setToolTip("Создать шаблон из выбранного префикса (для опытных пользователей)")
self.create_base_pfx_button.setEnabled(False)
self.create_base_pfx_button.clicked.connect(self.create_base_prefix_from_selected)
selector_layout.addWidget(self.create_base_pfx_button)
self.delete_prefix_button = QPushButton()
self.delete_prefix_button.setIcon(QIcon.fromTheme("user-trash"))
self.delete_prefix_button.setToolTip("Удалить выбранный префикс")
@@ -2142,6 +2270,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)
@@ -2174,7 +2309,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)
@@ -2208,8 +2343,8 @@ class WineHelperGUI(QMainWindow):
def _load_created_prefixes(self):
"""Загружает и обновляет список созданных префиксов в выпадающем списке."""
prefixes_root_path = os.path.join(Var.USER_WORK_PATH, "prefixes")
if not os.path.isdir(prefixes_root_path):
self.management_container_groupbox.setVisible(False)
has_prefixes_dir = os.path.isdir(prefixes_root_path)
if not has_prefixes_dir:
return
try:
@@ -2228,18 +2363,16 @@ class WineHelperGUI(QMainWindow):
self.created_prefix_selector.blockSignals(False)
if not prefix_names:
self.management_container_groupbox.setVisible(False)
self.on_created_prefix_selected(-1) # Убедимся, что панель управления сброшена
return
self.management_container_groupbox.setVisible(True)
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)
self.create_base_pfx_button.setEnabled(False)
else:
# Прокручиваем к выбранному элементу, чтобы он был виден в списке
self.created_prefix_selector.view().scrollTo(
@@ -2249,6 +2382,7 @@ class WineHelperGUI(QMainWindow):
self.current_managed_prefix_name = prefix_name
self._setup_prefix_management_panel(prefix_name)
self.delete_prefix_button.setEnabled(True)
self.create_base_pfx_button.setEnabled(True)
def delete_selected_prefix(self):
"""Удаляет префикс, выбранный в выпадающем списке на вкладке 'Менеджер префиксов'."""
@@ -2313,6 +2447,50 @@ class WineHelperGUI(QMainWindow):
else:
QMessageBox.critical(self, "Ошибка удаления", f"Не удалось удалить префикс '{prefix_name}'.\nПодробности смотрите в логе.")
def create_base_prefix_from_selected(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\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
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)
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._run_simple_command("create-base-pfx", [prefix_name])
self.command_dialog.exec_()
def _setup_prefix_management_panel(self, prefix_name):
"""Настраивает панель управления префиксом на основе текущего состояния."""
is_prefix_selected = bool(prefix_name)
@@ -2381,8 +2559,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'<p style="line-height: 1.3; font-size: 9pt;">'
html_content += f"<b>Имя:</b> {html.escape(prefix_name)}<br>"
@@ -2531,11 +2710,12 @@ class WineHelperGUI(QMainWindow):
# Для удаления лицензия не нужна, запускаем сразу.
self.run_component_install_command(prefix_name, command, version)
else:
# Установка: сначала показываем лицензионное соглашение.
if not self._show_license_agreement_dialog():
return # Пользователь отклонил лицензию
# Установка: для DXVK и VKD3D лицензию не показываем.
if component not in ['dxvk', 'vkd3d-proton']:
if not self._show_license_agreement_dialog():
return # Пользователь отклонил лицензию
# Если лицензия принята, запускаем установку.
# Запускаем установку.
self.run_component_install_command(prefix_name, command, version)
def open_wine_version_manager(self):
@@ -2566,9 +2746,6 @@ class WineHelperGUI(QMainWindow):
new_version = dialog.selected_version
new_version_display = dialog.selected_display_text
if not self._show_license_agreement_dialog():
return # Пользователь отклонил лицензию
self.run_change_wine_version_command(prefix_name, new_version, new_version_display)
def run_change_wine_version_command(self, prefix_name, new_version, new_version_display):
@@ -2682,6 +2859,88 @@ 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 "0"
dialog = FileAssociationsDialog(current_associations if current_associations != "0" else "", self)
if dialog.exec_() == QDialog.Accepted:
new_associations = dialog.new_associations
# Запускаем обновление, только если значение изменилось
if new_associations != (current_associations if current_associations != "0" else "0"):
self.run_update_associations_command(prefix_name, new_associations)
def run_update_associations_command(self, prefix_name, new_associations):
"""Выполняет команду обновления ассоциаций файлов."""
# --- Прямое редактирование last.conf, чтобы обойти перезапись переменных в winehelper ---
last_conf_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name, "last.conf")
if not os.path.exists(last_conf_path):
QMessageBox.critical(self, "Ошибка", f"Файл конфигурации last.conf не найден для префикса '{prefix_name}'.")
return
try:
with open(last_conf_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
updated = False
for i, line in enumerate(lines):
if line.strip().startswith("export WH_XDG_OPEN="):
lines[i] = f'export WH_XDG_OPEN="{new_associations}"\n'
updated = True
break
if not updated:
lines.append(f'export WH_XDG_OPEN="{new_associations}"\n')
with open(last_conf_path, 'w', encoding='utf-8') as f:
f.writelines(lines)
except IOError as e:
QMessageBox.critical(self, "Ошибка записи", f"Не удалось обновить файл last.conf: {e}")
return
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)
# Переменная WH_XDG_OPEN теперь читается из измененного last.conf
self.command_process.setProcessEnvironment(env)
# Вызываем init-prefix, который теперь прочитает правильное значение из last.conf
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):
"""
Открывает диалог для создания ярлыка для приложения внутри выбранного префикса.
@@ -2782,14 +3041,15 @@ class WineHelperGUI(QMainWindow):
authors_text = QTextEdit()
authors_text.setReadOnly(True)
authors_text.setHtml("""
<div style="text-align: center;">
<h2>Разработчики</h2>
<div style="text-align: center; font-size: 10pt;">
<p><span style="font-size: 11pt;"><b>Разработчики</b></span><br>
Михаил Тергоев (fidel)<br>
Сергей Пальчех (minergenon)</p>
<p><b>Помощники</b><br>
<p><span style="font-size: 11pt;"><b>Помощники</b></span><br>
Иван Мажукин (vanomj)</p>
<p><b>Идея и поддержка:</b><br>
сообщество ALT Linux</p>
<p><span style="font-size: 11pt;"><b>Идея и поддержка</b></span><br>
ООО "Базальт СПО"<br>
ALT Linux Team</p>
<br>
<p>Отдельная благодарность всем, кто вносит свой вклад в развитие проекта,<br>
тестирует и сообщает об ошибках!</p>
@@ -2967,9 +3227,6 @@ class WineHelperGUI(QMainWindow):
self.created_prefix_selector.setCurrentText(prefix_name)
if not self.management_container_groupbox.isVisible():
self.management_container_groupbox.setVisible(True)
def update_installed_apps(self):
"""Обновляет список установленных приложений в виде кнопок"""
# Если активная кнопка находится в списке удаляемых, сбрасываем ее
@@ -3514,7 +3771,7 @@ class WineHelperGUI(QMainWindow):
QMessageBox.critical(self, "Ошибка", f"Не удалось модифицировать команду для отладки: {e}")
return
process = QProcess(self)
process = QProcess()
env = QProcessEnvironment.systemEnvironment()
cmd_start_index = 0
@@ -3532,7 +3789,10 @@ class WineHelperGUI(QMainWindow):
arguments = clean_command[cmd_start_index + 1:]
process.setProcessEnvironment(env)
process.finished.connect(lambda: self._on_app_process_finished(desktop_path))
# Используем functools.partial для надежной передачи аргументов
# и избегания проблем с замыканием в lambda.
process.finished.connect(partial(self._on_app_process_finished, desktop_path))
try:
process.start(program, arguments)
@@ -3551,6 +3811,51 @@ class WineHelperGUI(QMainWindow):
QMessageBox.critical(self, "Ошибка",
f"Не удалось обработать команду запуска:\n{command_str}\n\nОшибка: {str(e)}")
def quit_application(self):
"""Инициирует процесс выхода из приложения."""
self.is_quitting = True
self.close() # Инициируем событие закрытия, которое будет обработано в closeEvent
def closeEvent(self, event):
"""Обрабатывает событие закрытия главного окна."""
# Теперь любое закрытие окна (крестик или выход из меню) инициирует выход
if self.running_apps:
msg_box = QMessageBox(self)
msg_box.setWindowTitle('Подтверждение выхода')
msg_box.setTextFormat(Qt.RichText)
msg_box.setText('<font color="red">Все запущенные приложения будут закрыты вместе с WineHelper.</font><br><br>'
"Вы уверены, что хотите выйти?")
msg_box.setIcon(QMessageBox.Question)
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:
# Отключаем обработчики сигналов от всех запущенных процессов,
# так как мы собираемся их принудительно завершить и выйти.
# Это предотвращает ошибку RuntimeError при закрытии.
for process in self.running_apps.values():
process.finished.disconnect()
# Используем встроенную команду killall для надежного завершения всех процессов wine
print("Завершение всех запущенных приложений через 'winehelper killall'...")
# Используем subprocess.run, который дождется завершения команды
subprocess.run([self.winehelper_path, "killall"], check=False, capture_output=True)
# Принудительно дожидаемся завершения всех дочерних процессов
for process in self.running_apps.values():
process.waitForFinished(5000) # Ждем до 5 секунд
QApplication.instance().quit()
event.accept()
else:
event.ignore()
else:
QApplication.instance().quit() # Если нет запущенных приложений, просто выходим
def uninstall_app(self):
"""Удаляет выбранное установленное приложение и его префикс"""
if not self.current_selected_app or 'desktop_path' not in self.current_selected_app:
@@ -4180,13 +4485,22 @@ class WineHelperGUI(QMainWindow):
self.install_process.terminate()
def _handle_command_output(self):
"""Обрабатывает вывод для диалога команды"""
"""Обрабатывает вывод для общих команд в модальном диалоге."""
if hasattr(self, 'command_process') and self.command_process:
output = self.command_process.readAllStandardOutput().data().decode('utf-8', errors='ignore').strip()
# Используем readAll, чтобы получить и stdout, и stderr
output_bytes = self.command_process.readAll()
output = output_bytes.data().decode('utf-8', errors='ignore').strip()
if output and hasattr(self, 'command_log_output'):
self.command_log_output.append(output)
QApplication.processEvents()
def _run_simple_command(self, command, args=None):
"""Запускает простую команду winehelper и выводит лог."""
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_command_finished)
self.command_process.start(self.winehelper_path, [command] + (args or []))
def _handle_command_finished(self, exit_code, exit_status):
"""Обрабатывает завершение для диалога команды"""
if exit_code == 0:
@@ -4287,6 +4601,8 @@ def main():
# Сохраняем ссылку на сервер, чтобы он не был удален сборщиком мусора
window.server = server
window.show()
# Создаем иконку в системном трее после создания окна
window.create_tray_icon()
return app.exec_()
return 1