Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b60a79ae3
|
No files matched your search
@@ -9,14 +9,9 @@
|
||||
|
||||
### Changed
|
||||
|
||||
- Загрузка игр GOG переведена на форк `linux-gaming-ru/gogdl` с выбором CDN по скорости, исправленной статистикой загрузки и ограничением повторных запросов; ранее установленный gogdl автоматически заменяется форком
|
||||
### Fixed
|
||||
|
||||
- Отключена проверка обновлений AppImage при запуске с `--silent`, чтобы не показывать диалог обновления
|
||||
- Исправлена рекурсивная ошибка фильтра событий клавиатуры при закрытии приложения
|
||||
- Исправлена высокая нагрузка на CPU из-за непрерывной перерисовки таблиц в настройках exe
|
||||
- В меню системного трея при запуске игры с `--silent` добавлены отсутствовавшие действия приостановки и возобновления игры
|
||||
- Исправлена гонка при тихом запуске игр
|
||||
|
||||
### Contributors
|
||||
|
||||
|
||||
@@ -16,15 +16,6 @@ export DEPLOY_PYTHON=1
|
||||
export MAIN_BIN=portprotonqt
|
||||
export OPTIMIZE_LAUNCH=1
|
||||
|
||||
# sharun 2.2.7 needs the matching deployment and preload format from 1.4.1.
|
||||
ANYLINUX_TOOLS="https://raw.githubusercontent.com/pkgforge-dev/Anylinux-AppImages/bf5e3c44dc2daf8b5de51cdf6b2d11589d047104/useful-tools"
|
||||
export SHARUN_LINK="https://github.com/pkgforge-dev/Anylinux-sharun/releases/download/2.2.7/sharun-$ARCH"
|
||||
export ANYLINUX_LIB_SOURCE="$ANYLINUX_TOOLS/lib/anylinux.c"
|
||||
export HOOKSRC="$ANYLINUX_TOOLS/hooks"
|
||||
wget --retry-connrefused --tries=30 "$ANYLINUX_TOOLS/quick-sharun.sh" -O ./quick-sharun
|
||||
echo '6cd9e45a099d7173aa906aaca51e48e51db78069ead14ef8e6683db4cbb4be44 ./quick-sharun' | sha256sum -c -
|
||||
chmod +x ./quick-sharun
|
||||
|
||||
# Adjust comp settings to bypass oom-killer
|
||||
export DWARFS_COMP="zstd:level=15 -S22 -B5"
|
||||
|
||||
@@ -47,7 +38,7 @@ GAMEPAD_LIBRARY=$(find /usr/lib -type f \
|
||||
-path '*/site-packages/portprotonqt/libportprotonqt_gamepad.so' \
|
||||
-print -quit)
|
||||
test -n "$GAMEPAD_LIBRARY"
|
||||
./quick-sharun \
|
||||
quick-sharun \
|
||||
/usr/bin/portprotonqt* \
|
||||
/usr/bin/bash \
|
||||
/usr/bin/update-desktop-database \
|
||||
@@ -107,4 +98,4 @@ done
|
||||
rm -rf "$PYSIDE_DIR"/QtAsyncio
|
||||
|
||||
# Turn AppDir into AppImage
|
||||
./quick-sharun --make-appimage
|
||||
quick-sharun --make-appimage
|
||||
@@ -2428,8 +2428,9 @@ pw_specialk_set_ini_value () {
|
||||
[[ -f "$ini_file" ]] && sed -i '/./,$!d' "$ini_file"
|
||||
|
||||
if grep -q "^\[$section\]" "$ini_file" ; then
|
||||
if sed -n "/^\[$section\]/,/^\[/{p}" "$ini_file" | grep -q "^$key=" ; then
|
||||
sed -i "/^\[$section\]/,/^\[/{s/^$key=.*/$key=$value/}" "$ini_file"
|
||||
if sed -n "/^\[$section\]/,/^\[/{p}" "$ini_file" \
|
||||
| grep -q "^[[:space:]]*$key[[:space:]]*=" ; then
|
||||
sed -i "/^\[$section\]/,/^\[/{s/^[[:space:]]*$key[[:space:]]*=.*/$key=$value/}" "$ini_file"
|
||||
else
|
||||
sed -i "/^\[$section\]/a $key=$value" "$ini_file"
|
||||
fi
|
||||
@@ -2522,6 +2523,171 @@ pw_specialk_sync_files () {
|
||||
|
||||
}
|
||||
|
||||
pw_reshade_sync_files () {
|
||||
local root="${PW_PLUGINS_PATH}/reshade_windows" runtime detect arch dll highest ogl vulkan source
|
||||
local manifest_reg package installed installed_arch installed_version extract_dir reshade_exe reshade_font font_win screenshot_path backup
|
||||
local PATH_TO_GAME="${PATH_TO_GAME:-}"
|
||||
runtime="$root/reshade/${PW_RESHADE_VER}_Addon"
|
||||
reshade_exe="${PW_RESHADE_EXE:-$PW_EXE_FILE}"
|
||||
if [[ ! -f "$reshade_exe" ]] ; then
|
||||
print_warning "ReShade executable not found: $reshade_exe. Using $PW_EXE_FILE."
|
||||
reshade_exe="$PW_EXE_FILE"
|
||||
fi
|
||||
if [[ "$PW_USE_RESHADE" == 1 ]] ; then
|
||||
detect=$(python_module graphics_detector "$reshade_exe")
|
||||
file "$reshade_exe" | grep -qE 'x86-64|PE32\+' && arch=64 || arch=32
|
||||
highest=$(echo "$detect" | grep '^HIGHEST_DX=' | cut -d= -f2)
|
||||
ogl=$(echo "$detect" | grep '^USES_OGL=' | cut -d= -f2)
|
||||
vulkan=$(echo "$detect" | grep '^USES_VULKAN=' | cut -d= -f2)
|
||||
source=$(echo "$detect" | grep '^SOURCE=' | cut -d= -f2-)
|
||||
[[ "${source##*/}" == UnityPlayer.dll && "$highest" != None ]] && vulkan=false
|
||||
fi
|
||||
if [[ -n "$PW_RESHADE_EXE" ]] ; then
|
||||
PATH_TO_GAME="$(dirname "$(realpath "$reshade_exe")")"
|
||||
fi
|
||||
if [[ "$PW_USE_RESHADE" == 1 ]] \
|
||||
&& [[ ! -f "$runtime/ReShade32.dll" || ! -f "$runtime/ReShade32.json" \
|
||||
|| ! -f "$runtime/ReShade64.dll" || ! -f "$runtime/ReShade64.json" ]] ; then
|
||||
try_download "https://reshade.me/downloads/ReShade_Setup_${PW_RESHADE_VER}_Addon.exe" \
|
||||
"${PORT_DATA_PATH}/data/tmp/ReShade_Setup_${PW_RESHADE_VER}_Addon.exe"
|
||||
create_new_dir "$runtime"
|
||||
unpack "${PORT_DATA_PATH}/data/tmp/ReShade_Setup_${PW_RESHADE_VER}_Addon.exe" \
|
||||
"$runtime" silent || fatal "Failed to extract ReShade."
|
||||
try_remove_file "${PORT_DATA_PATH}/data/tmp/ReShade_Setup_${PW_RESHADE_VER}_Addon.exe"
|
||||
fi
|
||||
if [[ "$PW_USE_RESHADE" == 1 && ! -f "$root/d3dcompiler_47.dll" ]] ; then
|
||||
try_download "https://github.com/itsOwen/LetMeReShade/releases/download/LetMeReShade_Binaries/d3dcompiler_47.dll" "$root/d3dcompiler_47.dll"
|
||||
fi
|
||||
if [[ "$PW_USE_RESHADE" == 1 && ! -f "$root/d3d8.dll" ]] ; then
|
||||
try_download "https://github.com/crosire/d3d8to9/releases/download/v${PW_D3D8TO9_VER}/d3d8.dll" "$root/d3d8.dll"
|
||||
fi
|
||||
if [[ "$PW_USE_RESHADE" == 1 && ! -d "$root/shaders/Shaders" ]] ; then
|
||||
try_remove_dir "$root/shaders"
|
||||
create_new_dir "$root/shaders/Shaders"
|
||||
create_new_dir "$root/shaders/Textures"
|
||||
for package in reshade_shaders sweetfx_shaders immerse_shaders astrayfx_shaders prod80_shaders retroarch_shaders ; do
|
||||
extract_dir="${PORT_DATA_PATH}/data/tmp/$package"
|
||||
if [[ "$package" == immerse_shaders ]] ; then
|
||||
try_download "https://github.com/martymcmodding/iMMERSE/archive/refs/heads/main.tar.gz" \
|
||||
"${PORT_DATA_PATH}/data/tmp/${package}.tar.gz"
|
||||
else
|
||||
try_download "https://github.com/itsOwen/LetMeReShade/releases/download/LetMeReShade_Binaries/${package}.tar.gz" \
|
||||
"${PORT_DATA_PATH}/data/tmp/${package}.tar.gz"
|
||||
fi
|
||||
try_remove_dir "$extract_dir"
|
||||
create_new_dir "$extract_dir"
|
||||
unpack "${PORT_DATA_PATH}/data/tmp/${package}.tar.gz" "$extract_dir" \
|
||||
silent || fatal "Failed to extract $package."
|
||||
find "$extract_dir" -type d -iname Shaders -exec cp -rf '{}/.' "$root/shaders/Shaders/" \;
|
||||
find "$extract_dir" -type d -iname Textures -exec cp -rf '{}/.' "$root/shaders/Textures/" \;
|
||||
try_remove_dir "$extract_dir"
|
||||
try_remove_file "${PORT_DATA_PATH}/data/tmp/${package}.tar.gz"
|
||||
done
|
||||
fi
|
||||
if [[ "$PW_USE_RESHADE" == 1 && "$vulkan" == true ]] ; then
|
||||
dll="Vulkan${arch}"
|
||||
elif [[ "$PW_USE_RESHADE" == 1 ]] ; then
|
||||
case "$highest" in
|
||||
"DirectX 10"|"DirectX 11"|"DirectX 12") dll=dxgi.dll ;;
|
||||
"DirectX 8"|"DirectX 9") dll=d3d9.dll ;;
|
||||
"DirectDraw") return 0 ;;
|
||||
*) [[ "$ogl" == true ]] && dll=opengl32.dll || dll=dxgi.dll ;;
|
||||
esac
|
||||
fi
|
||||
case "$PW_RESHADE_API" in
|
||||
vulkan) vulkan=true; dll="Vulkan${arch}" ;;
|
||||
dxgi.dll|d3d9.dll|opengl32.dll) vulkan=false; dll="$PW_RESHADE_API" ;;
|
||||
esac
|
||||
if [[ -f "$PATH_TO_GAME/reshade_version" ]] \
|
||||
&& { [[ "$PW_USE_RESHADE" != 1 ]] \
|
||||
|| [[ "$(<"$PATH_TO_GAME/reshade_version")" != "${PW_RESHADE_VER}_Addon"$'\n'"$dll" ]]; } ; then
|
||||
installed_version=$(sed -n '1p' "$PATH_TO_GAME/reshade_version")
|
||||
installed=$(sed -n '2p' "$PATH_TO_GAME/reshade_version")
|
||||
if [[ "$installed" == Vulkan32 || "$installed" == Vulkan64 ]] ; then
|
||||
installed_arch=${installed#Vulkan}
|
||||
manifest_reg="Z:${root//\//\\}\\reshade\\${installed_version}\\ReShade${installed_arch}.json"
|
||||
manifest_reg=${manifest_reg//\\/\\\\\\\\}
|
||||
get_and_set_reg_file --delete 'Software\Khronos\Vulkan\ImplicitLayers' \
|
||||
"$manifest_reg" "" "" user
|
||||
if [[ "$installed_arch" == 32 ]] ; then
|
||||
get_and_set_reg_file --delete 'Software\Wow6432Node\Khronos\Vulkan\ImplicitLayers' \
|
||||
"$manifest_reg" "" "" system
|
||||
else
|
||||
get_and_set_reg_file --delete 'Software\Khronos\Vulkan\ImplicitLayers' \
|
||||
"$manifest_reg" "" "" system
|
||||
fi
|
||||
fi
|
||||
try_remove_file "$PATH_TO_GAME/$installed"
|
||||
if [[ -L "$PATH_TO_GAME/${installed}.b" \
|
||||
&& $(readlink -f "$PATH_TO_GAME/${installed}.b") == "$root/reshade/"* ]] ; then
|
||||
unlink "$PATH_TO_GAME/${installed}.b"
|
||||
fi
|
||||
[[ -e "$PATH_TO_GAME/${installed}.b" ]] \
|
||||
&& mv -f "$PATH_TO_GAME/${installed}.b" "$PATH_TO_GAME/$installed"
|
||||
[[ -L "$PATH_TO_GAME/d3dcompiler_47.dll" ]] && unlink "$PATH_TO_GAME/d3dcompiler_47.dll"
|
||||
[[ -L "$PATH_TO_GAME/d3d8.dll" ]] && unlink "$PATH_TO_GAME/d3d8.dll"
|
||||
[[ -e "$PATH_TO_GAME/d3d8.dll.b" ]] \
|
||||
&& mv -f "$PATH_TO_GAME/d3d8.dll.b" "$PATH_TO_GAME/d3d8.dll"
|
||||
[[ -L "$PATH_TO_GAME/ReShade_shaders" ]] && unlink "$PATH_TO_GAME/ReShade_shaders"
|
||||
try_remove_file "$PATH_TO_GAME/ReShade.ini"
|
||||
try_remove_file "$PATH_TO_GAME/ReShadePreset.ini"
|
||||
try_remove_file "$PATH_TO_GAME/ReShade.log"
|
||||
try_remove_file "$PATH_TO_GAME/reshade_version"
|
||||
fi
|
||||
[[ "$PW_USE_RESHADE" != 1 ]] && return 0
|
||||
[[ -L "$PATH_TO_GAME/ReShade_shaders" ]] && unlink "$PATH_TO_GAME/ReShade_shaders"
|
||||
try_force_link_dir "$root/shaders" "$PATH_TO_GAME/ReShade_shaders"
|
||||
pw_specialk_set_ini_value "$PATH_TO_GAME/ReShade.ini" "GENERAL" "EffectSearchPaths" '.\\ReShade_shaders\\Shaders'
|
||||
pw_specialk_set_ini_value "$PATH_TO_GAME/ReShade.ini" "GENERAL" "TextureSearchPaths" '.\\ReShade_shaders\\Textures'
|
||||
if [[ -n "$PW_RESHADE_FONT" ]] ; then
|
||||
reshade_font=$(fc-match --format='%{file}' "$PW_RESHADE_FONT")
|
||||
else
|
||||
reshade_font=/usr/share/fonts/ttf/dejavu/DejaVuSans.ttf
|
||||
fi
|
||||
if [[ -f "$reshade_font" ]] ; then
|
||||
font_win="Z:${reshade_font//\//\\\\}"
|
||||
pw_specialk_set_ini_value "$PATH_TO_GAME/ReShade.ini" "STYLE" "Font" "$font_win"
|
||||
pw_specialk_set_ini_value "$PATH_TO_GAME/ReShade.ini" "STYLE" "EditorFont" "$font_win"
|
||||
elif [[ -n "$PW_RESHADE_FONT" ]] ; then
|
||||
print_warning "ReShade font not found: $PW_RESHADE_FONT"
|
||||
fi
|
||||
screenshot_path='.\\'
|
||||
if [[ -n "$PW_RESHADE_SCREENSHOT_PATH" ]] ; then
|
||||
create_new_dir "$PW_RESHADE_SCREENSHOT_PATH"
|
||||
screenshot_path="Z:${PW_RESHADE_SCREENSHOT_PATH//\//\\\\}"
|
||||
fi
|
||||
pw_specialk_set_ini_value "$PATH_TO_GAME/ReShade.ini" "INPUT" "KeyScreenshot" "$PW_RESHADE_SCREENSHOT_KEY"
|
||||
pw_specialk_set_ini_value "$PATH_TO_GAME/ReShade.ini" "INPUT" "KeyOverlay" "$PW_RESHADE_OVERLAY_KEY"
|
||||
pw_specialk_set_ini_value "$PATH_TO_GAME/ReShade.ini" "SCREENSHOT" "SavePath" "$screenshot_path"
|
||||
if [[ "$vulkan" == true ]] ; then
|
||||
manifest_reg="Z:${runtime//\//\\}\\ReShade${arch}.json"
|
||||
manifest_reg=${manifest_reg//\\/\\\\\\\\}
|
||||
get_and_set_reg_file --add 'Software\Khronos\Vulkan\ImplicitLayers' "$manifest_reg" REG_DWORD 0 user
|
||||
if [[ "$arch" == 32 ]] ; then
|
||||
get_and_set_reg_file --add 'Software\Wow6432Node\Khronos\Vulkan\ImplicitLayers' "$manifest_reg" REG_DWORD 0 system
|
||||
else
|
||||
get_and_set_reg_file --add 'Software\Khronos\Vulkan\ImplicitLayers' "$manifest_reg" REG_DWORD 0 system
|
||||
fi
|
||||
var_winedlloverride_update "vulkan-1=n,b"
|
||||
printf '%s\n%s\n' "${PW_RESHADE_VER}_Addon" "$dll" > "$PATH_TO_GAME/reshade_version"
|
||||
return 0
|
||||
fi
|
||||
[[ ! -f "$PATH_TO_GAME/reshade_version" \
|
||||
&& -e "$PATH_TO_GAME/$dll" && ! -e "$PATH_TO_GAME/${dll}.b" \
|
||||
&& ( ! -L "$PATH_TO_GAME/$dll" \
|
||||
|| $(readlink -f "$PATH_TO_GAME/$dll") != "$root/reshade/"* ) ]] \
|
||||
&& mv -f "$PATH_TO_GAME/$dll" "$PATH_TO_GAME/${dll}.b"
|
||||
try_force_link_file "$runtime/ReShade${arch}.dll" "$PATH_TO_GAME/$dll"
|
||||
try_force_link_file "$root/d3dcompiler_47.dll" "$PATH_TO_GAME/d3dcompiler_47.dll"
|
||||
var_winedlloverride_update "d3dcompiler_47=n;${dll%.*}=n,b"
|
||||
if [[ "$highest" == "DirectX 8" && "$dll" == d3d9.dll ]] ; then
|
||||
[[ -e "$PATH_TO_GAME/d3d8.dll" && ! -e "$PATH_TO_GAME/d3d8.dll.b" ]] && mv -f "$PATH_TO_GAME/d3d8.dll" "$PATH_TO_GAME/d3d8.dll.b"
|
||||
try_force_link_file "$root/d3d8.dll" "$PATH_TO_GAME/d3d8.dll"
|
||||
var_winedlloverride_update "d3d8=n,b"
|
||||
fi
|
||||
printf '%s\n%s\n' "${PW_RESHADE_VER}_Addon" "$dll" > "$PATH_TO_GAME/reshade_version"
|
||||
}
|
||||
|
||||
pw_prepare_gamescope_launch () {
|
||||
[[ "$PW_USE_NATIVE_WAYLAND" == "1" ]] && export PW_GAMESCOPE="0"
|
||||
|
||||
@@ -2957,6 +3123,7 @@ start_portproton () {
|
||||
|
||||
pw_optiscaler_sync_files
|
||||
pw_specialk_sync_files
|
||||
pw_reshade_sync_files
|
||||
|
||||
if [[ "${PW_USE_RAY_TRACING}" == "1" ]] ; then
|
||||
var_vkd3d_config_update dxr
|
||||
@@ -4052,7 +4219,6 @@ steamplay_launch () {
|
||||
|
||||
pw_optiscaler_sync_files
|
||||
pw_specialk_sync_files
|
||||
|
||||
PORT_WINE_PREFIX="$STEAM_COMPAT_DATA_PATH/pfx"
|
||||
|
||||
pw_prepare_gamescope_launch
|
||||
@@ -4118,6 +4284,8 @@ steamplay_launch () {
|
||||
fi
|
||||
fi
|
||||
|
||||
pw_reshade_sync_files
|
||||
|
||||
# Winetricks
|
||||
if [[ -n "${PW_DLL_INSTALL:-}" ]] ; then
|
||||
update_winetricks
|
||||
@@ -4331,7 +4499,9 @@ gui_edit_db () {
|
||||
KEY_EDIT_DB_GUI=$RANDOM
|
||||
DISABLE_EDIT_DB_LIST=""
|
||||
PW_EDIT_DB_LIST=(PW_MANGOHUD PW_MANGOHUD_USER_CONF PW_VKBASALT PW_VKBASALT_USER_CONF
|
||||
PW_VKBASALT_EFFECTS PW_VKBASALT_FFX_CAS PW_DGVOODOO2 PW_USE_SPECIALK
|
||||
PW_VKBASALT_EFFECTS PW_VKBASALT_FFX_CAS PW_DGVOODOO2 PW_USE_SPECIALK PW_USE_RESHADE
|
||||
PW_RESHADE_EXE PW_RESHADE_API PW_RESHADE_FONT PW_RESHADE_SCREENSHOT_PATH
|
||||
PW_RESHADE_SCREENSHOT_KEY PW_RESHADE_OVERLAY_KEY
|
||||
PW_USE_ESYNC PW_USE_FSYNC PW_USE_NTSYNC PW_USE_RAY_TRACING PW_USE_NVAPI_AND_DLSS PW_USE_OPTISCALER
|
||||
PW_USE_LS_FRAME_GEN PW_WINE_FULLSCREEN_FSR PW_HIDE_NVIDIA_GPU PW_VIRTUAL_DESKTOP PW_USE_TERMINAL
|
||||
PW_USE_GAMEMODE PW_USE_INHIBIT_SLEEP PW_USE_D3D_EXTRAS PW_FIX_VIDEO_IN_GAME PW_REDUCE_PULSE_LATENCY
|
||||
@@ -4365,6 +4535,10 @@ gui_edit_db () {
|
||||
DISABLE_EDIT_DB_LIST+=" PW_VKBASALT PW_VKBASALT_USER_CONF PW_USE_RAY_TRACING PW_USE_OBS_VKCAPTURE PW_USE_GALLIUM_ZINK PW_USE_WINED3D_VULKAN"
|
||||
fi
|
||||
[[ "${PW_USE_SPECIALK}" == "1" ]] && DISABLE_EDIT_DB_LIST+=" PW_DGVOODOO2"
|
||||
[[ "${PW_USE_RESHADE}" == "1" ]] && DISABLE_EDIT_DB_LIST+=" PW_USE_OPTISCALER PW_USE_SPECIALK"
|
||||
if [[ "${PW_USE_OPTISCALER}" == "1" || "${PW_USE_SPECIALK}" == "1" ]] ; then
|
||||
DISABLE_EDIT_DB_LIST+=" PW_USE_RESHADE"
|
||||
fi
|
||||
|
||||
[[ ! -e "/dev/ntsync" ]] && DISABLE_EDIT_DB_LIST+=" PW_USE_NTSYNC"
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ export VKD3D_SAREK_VER="sarek-2.6.0"
|
||||
export PW_LSFG_VK_VER="2.0.0-dev25-p11"
|
||||
export PW_OPTISCALER_VER="v03-0.9.0-pre8"
|
||||
export PW_SPECIALK_VER="SK_26_5_15"
|
||||
export PW_RESHADE_VER="6.8.0"
|
||||
export PW_D3D8TO9_VER="1.15.1"
|
||||
export PW_GALLIUM_NINE_VER="0.10"
|
||||
export DOTPFX_VER="48v9"
|
||||
export DEFPFX_VER="v1"
|
||||
@@ -33,6 +35,13 @@ check_variables PROTON_USE_XALIA "0"
|
||||
check_variables PW_USE_RUNTIME "1"
|
||||
check_variables PW_USE_D7VK "0"
|
||||
check_variables PW_USE_SPECIALK "0"
|
||||
check_variables PW_USE_RESHADE "0"
|
||||
check_variables PW_RESHADE_API "auto"
|
||||
check_variables PW_RESHADE_EXE ""
|
||||
check_variables PW_RESHADE_FONT ""
|
||||
check_variables PW_RESHADE_SCREENSHOT_PATH ""
|
||||
check_variables PW_RESHADE_SCREENSHOT_KEY "44,0,0,0"
|
||||
check_variables PW_RESHADE_OVERLAY_KEY "36,0,0,0"
|
||||
check_variables PW_USE_ESYNC "0"
|
||||
check_variables PW_USE_FSYNC "0"
|
||||
check_variables PW_USE_NTSYNC "0"
|
||||
|
||||
+9
-11
@@ -807,14 +807,13 @@ def main():
|
||||
return
|
||||
QMessageBox.warning(window, _("Error"), _("Failed to update AppImage."))
|
||||
|
||||
if not is_silent_launch:
|
||||
window.appimageUpdateWorker = AppImageUpdateWorker("check")
|
||||
window.appimageUpdateWorker.update_available.connect(show_appimage_update_prompt)
|
||||
window.appimageUpdateWorker.finished.connect(
|
||||
lambda: setattr(window, "appimageUpdateWorker", None)
|
||||
)
|
||||
window.appimageUpdateWorker.finished.connect(window.appimageUpdateWorker.deleteLater)
|
||||
QTimer.singleShot(APPIMAGE_UPDATE_START_DELAY_MS, window.appimageUpdateWorker.start)
|
||||
window.appimageUpdateWorker = AppImageUpdateWorker("check")
|
||||
window.appimageUpdateWorker.update_available.connect(show_appimage_update_prompt)
|
||||
window.appimageUpdateWorker.finished.connect(
|
||||
lambda: setattr(window, "appimageUpdateWorker", None)
|
||||
)
|
||||
window.appimageUpdateWorker.finished.connect(window.appimageUpdateWorker.deleteLater)
|
||||
QTimer.singleShot(APPIMAGE_UPDATE_START_DELAY_MS, window.appimageUpdateWorker.start)
|
||||
|
||||
class GOGDLUpdateWorker(QThread):
|
||||
"""Update gogdl without blocking the UI thread."""
|
||||
@@ -901,15 +900,14 @@ def main():
|
||||
finally:
|
||||
self.finished.emit()
|
||||
|
||||
# Initial setup can stop Wine and must not race a silent game launch.
|
||||
if start_sh and not silent_game_request:
|
||||
if start_sh:
|
||||
worker = InitialCommandWorker(start_sh)
|
||||
window.initialCommandWorker = worker
|
||||
worker.finished.connect(
|
||||
lambda: setattr(window, "initialCommandWorker", None)
|
||||
)
|
||||
worker.start()
|
||||
elif not start_sh:
|
||||
else:
|
||||
logger.warning("PortProton start command not available, skipping initial command")
|
||||
|
||||
# --- Cleanup ---
|
||||
|
||||
@@ -55,6 +55,7 @@ from portprotonqt.dialogs.dialog_utils import create_dialog_hints_widget, update
|
||||
from portprotonqt.dialogs.proton_manager import show_proton_manager
|
||||
from portprotonqt.dialogs.settings_mangohud import MANGOHUD_ENV_KEYS, MangoHudSettingsMixin
|
||||
from portprotonqt.dialogs.settings_gamescope import GAMESCOPE_ENV_KEYS, GamescopeSettingsMixin
|
||||
from portprotonqt.dialogs.settings_reshade import RESHADE_ENV_KEYS, ReShadeSettingsMixin
|
||||
from portprotonqt.dialogs.settings_vkbasalt import VKBASALT_ENV_KEYS, VkBasaltSettingsMixin
|
||||
from portprotonqt.localization import _, format_setting_name_for_display
|
||||
from portprotonqt.logger import get_logger
|
||||
@@ -85,6 +86,8 @@ DEFAULT_WINE_SETTING_KEYS = {
|
||||
'PW_PREFIX_NAME': 'PW_DEFAULT_PREFIX_NAME',
|
||||
'PW_VULKAN_USE': 'PW_DEFAULT_VULKAN_USE',
|
||||
}
|
||||
|
||||
|
||||
def _normalize_prefix_directories(prefixes_dir):
|
||||
if not os.path.isdir(prefixes_dir):
|
||||
return
|
||||
@@ -149,6 +152,7 @@ class ExeSettingsDialog(
|
||||
DraggableDialog,
|
||||
MangoHudSettingsMixin,
|
||||
GamescopeSettingsMixin,
|
||||
ReShadeSettingsMixin,
|
||||
VkBasaltSettingsMixin,
|
||||
):
|
||||
"""Dialog for configuring executable-specific settings."""
|
||||
@@ -207,6 +211,7 @@ class ExeSettingsDialog(
|
||||
self.advanced_settings_by_key = {}
|
||||
self.init_mangohud_state()
|
||||
self.init_gamescope_state()
|
||||
self.init_reshade_state()
|
||||
self.init_vkbasalt_state()
|
||||
self.blocked_keys = set()
|
||||
self.numa_nodes = {}
|
||||
@@ -332,6 +337,8 @@ class ExeSettingsDialog(
|
||||
self.mangohud_tab_layout = QVBoxLayout(self.mangohud_tab)
|
||||
self.vkbasalt_tab = QWidget()
|
||||
self.vkbasalt_tab_layout = QVBoxLayout(self.vkbasalt_tab)
|
||||
self.reshade_tab = QWidget()
|
||||
self.reshade_tab_layout = QVBoxLayout(self.reshade_tab)
|
||||
self.gamescope_tab = QWidget()
|
||||
self.gamescope_tab_layout = QVBoxLayout(self.gamescope_tab)
|
||||
|
||||
@@ -339,6 +346,7 @@ class ExeSettingsDialog(
|
||||
self.tab_widget.addTab(self.advanced_tab, _("Advanced"))
|
||||
self.tab_widget.addTab(self.mangohud_tab, "MangoHud")
|
||||
self.tab_widget.addTab(self.vkbasalt_tab, "vkBasalt")
|
||||
self.tab_widget.addTab(self.reshade_tab, "ReShade")
|
||||
if self.gamescope_available:
|
||||
self.tab_widget.addTab(self.gamescope_tab, "Gamescope")
|
||||
|
||||
@@ -462,6 +470,7 @@ class ExeSettingsDialog(
|
||||
|
||||
self.setup_mangohud_tab()
|
||||
self.setup_vkbasalt_tab()
|
||||
self.setup_reshade_tab()
|
||||
if self.gamescope_available:
|
||||
self.setup_gamescope_tab()
|
||||
|
||||
@@ -557,6 +566,7 @@ class ExeSettingsDialog(
|
||||
or key in MANGOHUD_ENV_KEYS
|
||||
or key in GAMESCOPE_ENV_KEYS
|
||||
or key in VKBASALT_ENV_KEYS
|
||||
or key in RESHADE_ENV_KEYS
|
||||
or key in TOGGLE_BOOL_KEYS
|
||||
):
|
||||
if val.startswith('"') and val.endswith('"') and len(val) >= 2:
|
||||
@@ -591,6 +601,8 @@ class ExeSettingsDialog(
|
||||
self.current_settings[key] = ''
|
||||
for key in VKBASALT_ENV_KEYS:
|
||||
self.current_settings[key] = ''
|
||||
for key in RESHADE_ENV_KEYS:
|
||||
self.current_settings[key] = ''
|
||||
elif not self.user_conf:
|
||||
self.current_settings.setdefault('PW_MANGOHUD', '0')
|
||||
self.current_settings.setdefault('PW_VKBASALT', '0')
|
||||
@@ -619,6 +631,7 @@ class ExeSettingsDialog(
|
||||
self.populate_advanced()
|
||||
self.populate_mangohud()
|
||||
self.populate_vkbasalt()
|
||||
self.populate_reshade()
|
||||
if self.gamescope_available:
|
||||
self.populate_gamescope()
|
||||
self.populate_favorites(select_tab=True)
|
||||
@@ -1391,6 +1404,7 @@ class ExeSettingsDialog(
|
||||
|
||||
self._filter_mangohud_settings(search_text)
|
||||
self._filter_vkbasalt_settings(search_text)
|
||||
self._filter_reshade_settings(search_text)
|
||||
self._filter_gamescope_settings(search_text)
|
||||
|
||||
def apply_changes(self):
|
||||
@@ -1465,6 +1479,7 @@ class ExeSettingsDialog(
|
||||
if self.gamescope_available:
|
||||
gamescope_changes = self._collect_gamescope_changes()
|
||||
vkbasalt_changes = self._collect_vkbasalt_changes()
|
||||
reshade_changes = self._collect_reshade_changes()
|
||||
if self.user_conf:
|
||||
if self.current_settings.get('PW_MANGOHUD') != '1':
|
||||
mangohud_changes = [
|
||||
@@ -1481,7 +1496,14 @@ class ExeSettingsDialog(
|
||||
change for change in gamescope_changes
|
||||
if change.startswith('PW_GAMESCOPE=')
|
||||
]
|
||||
specialized_changes = mangohud_changes + vkbasalt_changes + gamescope_changes
|
||||
if self.current_settings.get('PW_USE_RESHADE') != '1':
|
||||
reshade_changes = [
|
||||
change for change in reshade_changes
|
||||
if change.startswith('PW_USE_RESHADE=')
|
||||
]
|
||||
specialized_changes = (
|
||||
mangohud_changes + vkbasalt_changes + reshade_changes + gamescope_changes
|
||||
)
|
||||
if self.user_conf:
|
||||
defaults = ('', '0', '0.00', 'Home')
|
||||
specialized_changes = [
|
||||
@@ -1646,8 +1668,7 @@ class ExeSettingsDialog(
|
||||
palette.setBrush(QPalette.ColorRole.Text, item.foreground())
|
||||
label.setAutoFillBackground(selected)
|
||||
label.setPalette(palette)
|
||||
if label.foregroundRole() != QPalette.ColorRole.Text:
|
||||
label.setForegroundRole(QPalette.ColorRole.Text)
|
||||
label.setForegroundRole(QPalette.ColorRole.Text)
|
||||
text = item.data(Qt.ItemDataRole.AccessibleTextRole)
|
||||
name, separator, description = text.partition("\n")
|
||||
formatted_text = escape(name)
|
||||
@@ -1664,11 +1685,7 @@ class ExeSettingsDialog(
|
||||
if label.text() != formatted_text:
|
||||
label.setText(formatted_text)
|
||||
width = max(1, table.columnWidth(0))
|
||||
height_key = [width, formatted_text, label.font().toString()]
|
||||
if label.property("ppqt_height_key") != height_key:
|
||||
height = label.heightForWidth(width) + table.fontMetrics().height()
|
||||
label.setProperty("ppqt_height_key", height_key)
|
||||
table.setRowHeight(row, height)
|
||||
table.setRowHeight(row, label.heightForWidth(width) + table.fontMetrics().height())
|
||||
for row in range(table.rowCount()):
|
||||
widget = table.cellWidget(row, 1)
|
||||
if widget is None:
|
||||
@@ -1682,6 +1699,8 @@ class ExeSettingsDialog(
|
||||
)
|
||||
return False
|
||||
|
||||
if isinstance(obj, QWidget) and self._handle_reshade_key_button_event(obj, event):
|
||||
return True
|
||||
if isinstance(obj, QWidget) and self._handle_vkbasalt_key_button_event(obj, event):
|
||||
return True
|
||||
|
||||
|
||||
@@ -0,0 +1,446 @@
|
||||
"""ReShade settings support for executable settings dialog."""
|
||||
|
||||
import os
|
||||
from typing import Any, cast
|
||||
|
||||
from PySide6.QtCore import QEvent, Qt
|
||||
from PySide6.QtGui import QKeyEvent, QKeySequence
|
||||
from PySide6.QtWidgets import (
|
||||
QGridLayout,
|
||||
QGroupBox,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QPushButton,
|
||||
QScrollArea,
|
||||
QSizePolicy,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from portprotonqt.custom_widgets import AutoSizeButton, CustomComboBox
|
||||
from portprotonqt.localization import _
|
||||
from portprotonqt.theme_manager import ThemeManager
|
||||
|
||||
RESHADE_ENV_KEYS = [
|
||||
'PW_RESHADE_EXE',
|
||||
'PW_RESHADE_API',
|
||||
'PW_RESHADE_FONT',
|
||||
'PW_RESHADE_SCREENSHOT_PATH',
|
||||
'PW_RESHADE_SCREENSHOT_KEY',
|
||||
'PW_RESHADE_OVERLAY_KEY',
|
||||
]
|
||||
RESHADE_API_OPTIONS = {
|
||||
"Auto": "auto",
|
||||
"DXGI": "dxgi.dll",
|
||||
"DirectX 8/9": "d3d9.dll",
|
||||
"OpenGL": "opengl32.dll",
|
||||
"Vulkan": "vulkan",
|
||||
}
|
||||
RESHADE_SPECIAL_KEYS = {
|
||||
Qt.Key.Key_Cancel: 3,
|
||||
Qt.Key.Key_Backspace: 8,
|
||||
Qt.Key.Key_Tab: 9,
|
||||
Qt.Key.Key_Clear: 12,
|
||||
Qt.Key.Key_Return: 13,
|
||||
Qt.Key.Key_Enter: 13,
|
||||
Qt.Key.Key_Pause: 19,
|
||||
Qt.Key.Key_CapsLock: 20,
|
||||
Qt.Key.Key_Escape: 27,
|
||||
Qt.Key.Key_Space: 32,
|
||||
Qt.Key.Key_PageUp: 33,
|
||||
Qt.Key.Key_PageDown: 34,
|
||||
Qt.Key.Key_End: 35,
|
||||
Qt.Key.Key_Home: 36,
|
||||
Qt.Key.Key_Left: 37,
|
||||
Qt.Key.Key_Up: 38,
|
||||
Qt.Key.Key_Right: 39,
|
||||
Qt.Key.Key_Down: 40,
|
||||
Qt.Key.Key_Print: 44,
|
||||
Qt.Key.Key_Insert: 45,
|
||||
Qt.Key.Key_Delete: 46,
|
||||
Qt.Key.Key_Help: 47,
|
||||
Qt.Key.Key_Meta: 91,
|
||||
Qt.Key.Key_Menu: 93,
|
||||
Qt.Key.Key_Sleep: 95,
|
||||
Qt.Key.Key_NumLock: 144,
|
||||
Qt.Key.Key_ScrollLock: 145,
|
||||
Qt.Key.Key_Semicolon: 186,
|
||||
Qt.Key.Key_Equal: 187,
|
||||
Qt.Key.Key_Comma: 188,
|
||||
Qt.Key.Key_Minus: 189,
|
||||
Qt.Key.Key_Period: 190,
|
||||
Qt.Key.Key_Slash: 191,
|
||||
Qt.Key.Key_QuoteLeft: 192,
|
||||
Qt.Key.Key_BracketLeft: 219,
|
||||
Qt.Key.Key_Backslash: 220,
|
||||
Qt.Key.Key_BracketRight: 221,
|
||||
Qt.Key.Key_Apostrophe: 222,
|
||||
Qt.Key.Key_Back: 166,
|
||||
Qt.Key.Key_Forward: 167,
|
||||
Qt.Key.Key_Refresh: 168,
|
||||
Qt.Key.Key_Stop: 169,
|
||||
Qt.Key.Key_Search: 170,
|
||||
Qt.Key.Key_Favorites: 171,
|
||||
Qt.Key.Key_HomePage: 172,
|
||||
Qt.Key.Key_VolumeMute: 173,
|
||||
Qt.Key.Key_VolumeDown: 174,
|
||||
Qt.Key.Key_VolumeUp: 175,
|
||||
Qt.Key.Key_MediaNext: 176,
|
||||
Qt.Key.Key_MediaPrevious: 177,
|
||||
Qt.Key.Key_MediaStop: 178,
|
||||
Qt.Key.Key_MediaPlay: 179,
|
||||
Qt.Key.Key_LaunchMail: 180,
|
||||
Qt.Key.Key_LaunchMedia: 181,
|
||||
Qt.Key.Key_Launch0: 182,
|
||||
Qt.Key.Key_Launch1: 183,
|
||||
}
|
||||
RESHADE_KEYPAD_KEYS = {
|
||||
Qt.Key.Key_Asterisk: 106,
|
||||
Qt.Key.Key_Plus: 107,
|
||||
Qt.Key.Key_Comma: 108,
|
||||
Qt.Key.Key_Minus: 109,
|
||||
Qt.Key.Key_Period: 110,
|
||||
Qt.Key.Key_Slash: 111,
|
||||
}
|
||||
RESHADE_FIRST_FUNCTION_KEY = 112
|
||||
RESHADE_LAST_FUNCTION_KEY = 135
|
||||
RESHADE_FIRST_NUMPAD_KEY = 96
|
||||
|
||||
|
||||
class ReShadeSettingsMixin:
|
||||
"""Mixin with ReShade settings UI and serialization logic."""
|
||||
|
||||
theme: Any
|
||||
exe_path: str | None
|
||||
current_settings: dict[str, str]
|
||||
original_values: dict[str, str]
|
||||
reshade_tab: QWidget
|
||||
reshade_tab_layout: QVBoxLayout
|
||||
|
||||
def init_reshade_state(self) -> None:
|
||||
self.reshade_original_values = {}
|
||||
self.reshade_key_target = None
|
||||
|
||||
def setup_reshade_tab(self) -> None:
|
||||
"""Create ReShade tab widgets."""
|
||||
scroll = QScrollArea()
|
||||
scroll.setWidgetResizable(True)
|
||||
scroll.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||
scroll.setStyleSheet(self.theme.SCROLL_STYLE + self.theme.TRANSPARENT_BACKGROUND_STYLE)
|
||||
container = QWidget()
|
||||
layout = QVBoxLayout(container)
|
||||
layout.setSpacing(self.theme.exeSettingsGroupBoxBlockSpacing)
|
||||
|
||||
actions = QGroupBox(_("Actions"))
|
||||
actions.setStyleSheet(self.theme.QGROUP_BOX_STYLE)
|
||||
actions_layout = QGridLayout(actions)
|
||||
self.reshade_enable_button = self._create_reshade_button("")
|
||||
self.reshade_enable_button.clicked.connect(self.toggle_reshade_enable)
|
||||
actions_layout.addWidget(self.reshade_enable_button, 0, 0)
|
||||
layout.addWidget(actions)
|
||||
|
||||
self._add_reshade_settings_group(layout)
|
||||
layout.addStretch()
|
||||
|
||||
scroll.setWidget(container)
|
||||
self.reshade_tab_layout.addWidget(scroll)
|
||||
|
||||
def _add_reshade_settings_group(self, parent_layout: QVBoxLayout) -> None:
|
||||
self.reshade_settings_group = QGroupBox(_("Settings"))
|
||||
self.reshade_settings_group.setStyleSheet(self.theme.QGROUP_BOX_STYLE)
|
||||
settings_layout = QGridLayout(self.reshade_settings_group)
|
||||
self.reshade_exe_edit = self._add_reshade_file_row(
|
||||
settings_layout, 0, "EXE", ".exe"
|
||||
)
|
||||
self.reshade_api_combo = CustomComboBox(theme=self.theme)
|
||||
self.reshade_api_combo.view().window().setWindowFlags(
|
||||
Qt.WindowType.Popup | Qt.WindowType.FramelessWindowHint
|
||||
)
|
||||
self.reshade_api_combo.view().window().setAttribute(
|
||||
Qt.WidgetAttribute.WA_TranslucentBackground
|
||||
)
|
||||
self.reshade_api_combo.setStyleSheet(
|
||||
self.theme.COMBOBOX_STYLE + self.theme.SCROLL_STYLE
|
||||
)
|
||||
self.reshade_api_combo.addItems(list(RESHADE_API_OPTIONS))
|
||||
settings_layout.addWidget(QLabel("API"), 1, 0)
|
||||
settings_layout.addWidget(self.reshade_api_combo, 1, 1)
|
||||
self.reshade_font_combo = CustomComboBox(theme=self.theme)
|
||||
self.reshade_font_combo.view().window().setWindowFlags(
|
||||
Qt.WindowType.Popup | Qt.WindowType.FramelessWindowHint
|
||||
)
|
||||
self.reshade_font_combo.view().window().setAttribute(
|
||||
Qt.WidgetAttribute.WA_TranslucentBackground
|
||||
)
|
||||
self.reshade_font_combo.setStyleSheet(
|
||||
self.theme.COMBOBOX_STYLE + self.theme.SCROLL_STYLE
|
||||
)
|
||||
self.reshade_font_combo.addItem(_("Default"), "")
|
||||
settings_layout.addWidget(QLabel(_("Font")), 2, 0)
|
||||
settings_layout.addWidget(self.reshade_font_combo, 2, 1)
|
||||
self.reshade_screenshot_edit = self._add_reshade_directory_row(
|
||||
settings_layout, 3, _("Screenshots")
|
||||
)
|
||||
self.reshade_screenshot_key_button = self._create_reshade_key_button(
|
||||
"screenshot"
|
||||
)
|
||||
settings_layout.addWidget(QLabel(_("Screenshot key")), 4, 0)
|
||||
settings_layout.addWidget(self.reshade_screenshot_key_button, 4, 1)
|
||||
self.reshade_overlay_key_button = self._create_reshade_key_button("overlay")
|
||||
settings_layout.addWidget(QLabel(_("Overlay key")), 5, 0)
|
||||
settings_layout.addWidget(self.reshade_overlay_key_button, 5, 1)
|
||||
parent_layout.addWidget(self.reshade_settings_group)
|
||||
self._load_reshade_fonts()
|
||||
|
||||
def _create_reshade_button(self, label: str) -> QPushButton:
|
||||
button = QPushButton(label)
|
||||
button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||
button.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
button.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||||
return button
|
||||
|
||||
def _add_reshade_file_row(
|
||||
self, layout: QGridLayout, row: int, label: str, file_filter: str
|
||||
) -> QLineEdit:
|
||||
line_edit = QLineEdit()
|
||||
line_edit.setStyleSheet(self.theme.ADDGAME_INPUT_STYLE)
|
||||
button = AutoSizeButton(
|
||||
_("Browse…"), icon=ThemeManager().get_icon("folder", as_path=True)
|
||||
)
|
||||
button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||
button.clicked.connect(
|
||||
lambda _checked=False: self._select_reshade_file(line_edit, file_filter)
|
||||
)
|
||||
field_layout = QHBoxLayout()
|
||||
field_layout.addWidget(line_edit)
|
||||
field_layout.addWidget(button)
|
||||
layout.addWidget(QLabel(label), row, 0)
|
||||
layout.addLayout(field_layout, row, 1)
|
||||
return line_edit
|
||||
|
||||
def _select_reshade_file(self, line_edit: QLineEdit, file_filter: str) -> None:
|
||||
from portprotonqt.dialogs.file_explorer import FileExplorer
|
||||
|
||||
current_path = line_edit.text().strip()
|
||||
initial_path = os.path.dirname(current_path) if os.path.isfile(current_path) else None
|
||||
explorer = FileExplorer(
|
||||
self, theme=self.theme, file_filter=file_filter, initial_path=initial_path
|
||||
)
|
||||
explorer.file_signal.file_selected.connect(line_edit.setText)
|
||||
explorer.exec()
|
||||
|
||||
def _add_reshade_directory_row(
|
||||
self, layout: QGridLayout, row: int, label: str
|
||||
) -> QLineEdit:
|
||||
line_edit = QLineEdit()
|
||||
line_edit.setStyleSheet(self.theme.ADDGAME_INPUT_STYLE)
|
||||
button = AutoSizeButton(
|
||||
_("Browse…"), icon=ThemeManager().get_icon("folder", as_path=True)
|
||||
)
|
||||
button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||
button.clicked.connect(
|
||||
lambda _checked=False: self._select_reshade_directory(line_edit)
|
||||
)
|
||||
field_layout = QHBoxLayout()
|
||||
field_layout.addWidget(line_edit)
|
||||
field_layout.addWidget(button)
|
||||
layout.addWidget(QLabel(label), row, 0)
|
||||
layout.addLayout(field_layout, row, 1)
|
||||
return line_edit
|
||||
|
||||
def _select_reshade_directory(self, line_edit: QLineEdit) -> None:
|
||||
from portprotonqt.dialogs.file_explorer import FileExplorer
|
||||
|
||||
initial_path = line_edit.text().strip() or os.path.dirname(self.exe_path or "")
|
||||
explorer = FileExplorer(
|
||||
self, theme=self.theme, initial_path=initial_path, directory_only=True
|
||||
)
|
||||
explorer.file_signal.file_selected.connect(line_edit.setText)
|
||||
explorer.exec()
|
||||
|
||||
def _create_reshade_key_button(self, target: str) -> QPushButton:
|
||||
button = self._create_reshade_button("")
|
||||
button.clicked.connect(lambda _checked=False: self._capture_reshade_key(target))
|
||||
button.installEventFilter(cast(QWidget, self))
|
||||
return button
|
||||
|
||||
def _capture_reshade_key(self, target: str) -> None:
|
||||
self.reshade_key_target = target
|
||||
button = self._get_reshade_key_button(target)
|
||||
button.setText(_("Press a button to choose"))
|
||||
button.grabKeyboard()
|
||||
|
||||
def _get_reshade_key_button(self, target: str) -> QPushButton:
|
||||
if target == "screenshot":
|
||||
return self.reshade_screenshot_key_button
|
||||
return self.reshade_overlay_key_button
|
||||
|
||||
def _handle_reshade_key_button_event(self, obj: QWidget, event: QEvent) -> bool:
|
||||
target = self.reshade_key_target
|
||||
if target is None or obj is not self._get_reshade_key_button(target):
|
||||
return False
|
||||
if event.type() != QEvent.Type.KeyPress:
|
||||
return False
|
||||
key_event = cast(QKeyEvent, event)
|
||||
key_value = self._reshade_virtual_key(
|
||||
key_event.key(), key_event.modifiers()
|
||||
)
|
||||
if key_value is None:
|
||||
return True
|
||||
modifiers = key_event.modifiers()
|
||||
value = ",".join(str(item) for item in (
|
||||
key_value,
|
||||
int(bool(modifiers & Qt.KeyboardModifier.ControlModifier)),
|
||||
int(bool(modifiers & Qt.KeyboardModifier.ShiftModifier)),
|
||||
int(bool(modifiers & Qt.KeyboardModifier.AltModifier)),
|
||||
))
|
||||
self.current_settings[f'PW_RESHADE_{target.upper()}_KEY'] = value
|
||||
button = self._get_reshade_key_button(target)
|
||||
button.setText(QKeySequence(key_event.keyCombination()).toString())
|
||||
button.releaseKeyboard()
|
||||
self.reshade_key_target = None
|
||||
return True
|
||||
|
||||
def _reshade_virtual_key(
|
||||
self, key: int, modifiers: Qt.KeyboardModifier
|
||||
) -> int | None:
|
||||
is_keypad = bool(modifiers & Qt.KeyboardModifier.KeypadModifier)
|
||||
if is_keypad and Qt.Key.Key_0.value <= key <= Qt.Key.Key_9.value:
|
||||
return RESHADE_FIRST_NUMPAD_KEY + key - Qt.Key.Key_0.value
|
||||
if is_keypad:
|
||||
try:
|
||||
keypad_key = RESHADE_KEYPAD_KEYS.get(Qt.Key(key))
|
||||
except ValueError:
|
||||
keypad_key = None
|
||||
if keypad_key is not None:
|
||||
return keypad_key
|
||||
if Qt.Key.Key_0.value <= key <= Qt.Key.Key_9.value:
|
||||
return key
|
||||
if Qt.Key.Key_A.value <= key <= Qt.Key.Key_Z.value:
|
||||
return key
|
||||
if Qt.Key.Key_F1.value <= key <= Qt.Key.Key_F24.value:
|
||||
return RESHADE_FIRST_FUNCTION_KEY + key - Qt.Key.Key_F1.value
|
||||
try:
|
||||
return RESHADE_SPECIAL_KEYS.get(Qt.Key(key))
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def _format_reshade_key(self, value: str) -> str:
|
||||
try:
|
||||
key, control, shift, alt = (int(item) for item in value.split(","))
|
||||
except ValueError:
|
||||
return value
|
||||
qt_key = next(
|
||||
(item for item, code in RESHADE_SPECIAL_KEYS.items() if code == key), None
|
||||
)
|
||||
if qt_key is not None:
|
||||
name = QKeySequence(qt_key).toString()
|
||||
elif RESHADE_FIRST_FUNCTION_KEY <= key <= RESHADE_LAST_FUNCTION_KEY:
|
||||
name = f"F{key - RESHADE_FIRST_FUNCTION_KEY + 1}"
|
||||
else:
|
||||
name = chr(key)
|
||||
modifiers = [
|
||||
label for active, label in (
|
||||
(control, "Ctrl"), (shift, "Shift"), (alt, "Alt")
|
||||
) if active
|
||||
]
|
||||
return "+".join([*modifiers, name])
|
||||
|
||||
def _load_reshade_fonts(self) -> None:
|
||||
selected_family = self.current_settings.get('PW_RESHADE_FONT', '')
|
||||
font_options = cast(Any, self)._get_mangohud_font_options()
|
||||
families = {name for name, _path in font_options}
|
||||
self.reshade_font_combo.clear()
|
||||
self.reshade_font_combo.addItem(_("Default"), "")
|
||||
for family in sorted(families, key=str.casefold):
|
||||
self.reshade_font_combo.addItem(family, family)
|
||||
self._select_reshade_font(selected_family)
|
||||
|
||||
def _select_reshade_font(self, family: str) -> None:
|
||||
for index in range(self.reshade_font_combo.count()):
|
||||
if self.reshade_font_combo.itemData(index) == family:
|
||||
self.reshade_font_combo.setCurrentIndex(index)
|
||||
return
|
||||
if family:
|
||||
self.reshade_font_combo.addItem(family, family)
|
||||
self.reshade_font_combo.setCurrentIndex(self.reshade_font_combo.count() - 1)
|
||||
|
||||
def populate_reshade(self) -> None:
|
||||
"""Populate ReShade tab from current settings."""
|
||||
reshade_exe = self.current_settings.get('PW_RESHADE_EXE', '') or self.exe_path or ''
|
||||
self.reshade_exe_edit.setText(reshade_exe)
|
||||
api_value = self.current_settings.get('PW_RESHADE_API', 'auto')
|
||||
api_label = next(
|
||||
(label for label, value in RESHADE_API_OPTIONS.items() if value == api_value),
|
||||
"Auto",
|
||||
)
|
||||
self.reshade_api_combo.setCurrentText(api_label)
|
||||
font_family = self.current_settings.get('PW_RESHADE_FONT', '')
|
||||
self._select_reshade_font(font_family)
|
||||
screenshot_path = self.current_settings.get('PW_RESHADE_SCREENSHOT_PATH', '')
|
||||
screenshot_key = self.current_settings.get(
|
||||
'PW_RESHADE_SCREENSHOT_KEY', '44,0,0,0'
|
||||
)
|
||||
overlay_key = self.current_settings.get(
|
||||
'PW_RESHADE_OVERLAY_KEY', '36,0,0,0'
|
||||
)
|
||||
self.reshade_screenshot_edit.setText(screenshot_path)
|
||||
self.reshade_screenshot_key_button.setText(
|
||||
self._format_reshade_key(screenshot_key)
|
||||
)
|
||||
self.reshade_overlay_key_button.setText(self._format_reshade_key(overlay_key))
|
||||
self.current_settings['PW_RESHADE_SCREENSHOT_KEY'] = screenshot_key
|
||||
self.current_settings['PW_RESHADE_OVERLAY_KEY'] = overlay_key
|
||||
self.reshade_original_values = {
|
||||
key: self.current_settings.get(key, '') for key in RESHADE_ENV_KEYS
|
||||
}
|
||||
self.reshade_original_values['PW_RESHADE_EXE'] = reshade_exe
|
||||
self.reshade_original_values['PW_RESHADE_SCREENSHOT_KEY'] = screenshot_key
|
||||
self.reshade_original_values['PW_RESHADE_OVERLAY_KEY'] = overlay_key
|
||||
self._update_reshade_widgets()
|
||||
|
||||
def toggle_reshade_enable(self) -> None:
|
||||
enabled = self.current_settings.get('PW_USE_RESHADE') == '1'
|
||||
self.current_settings['PW_USE_RESHADE'] = '0' if enabled else '1'
|
||||
self._update_reshade_widgets()
|
||||
|
||||
def _update_reshade_widgets(self) -> None:
|
||||
enabled = self.current_settings.get('PW_USE_RESHADE') == '1'
|
||||
self.reshade_enable_button.setText(
|
||||
_("Disable {0}").format("ReShade")
|
||||
if enabled else _("Enable {0}").format("ReShade")
|
||||
)
|
||||
self.reshade_enable_button.setStyleSheet(
|
||||
self.theme.ACTION_BUTTON_ACTIVE_STYLE if enabled else self.theme.ACTION_BUTTON_STYLE
|
||||
)
|
||||
self.reshade_settings_group.setVisible(enabled)
|
||||
|
||||
def _collect_reshade_changes(self) -> list[str]:
|
||||
changes = []
|
||||
enabled = '1' if self.current_settings.get('PW_USE_RESHADE') == '1' else '0'
|
||||
original_enabled = '1' if self.original_values.get('PW_USE_RESHADE') == '1' else '0'
|
||||
if enabled != original_enabled:
|
||||
changes.append(f"PW_USE_RESHADE={enabled}")
|
||||
values = {
|
||||
'PW_RESHADE_EXE': self.reshade_exe_edit.text().strip(),
|
||||
'PW_RESHADE_API': RESHADE_API_OPTIONS[self.reshade_api_combo.currentText()],
|
||||
'PW_RESHADE_FONT': str(self.reshade_font_combo.currentData() or ''),
|
||||
'PW_RESHADE_SCREENSHOT_PATH': self.reshade_screenshot_edit.text().strip(),
|
||||
'PW_RESHADE_SCREENSHOT_KEY': self.current_settings['PW_RESHADE_SCREENSHOT_KEY'],
|
||||
'PW_RESHADE_OVERLAY_KEY': self.current_settings['PW_RESHADE_OVERLAY_KEY'],
|
||||
}
|
||||
for key, value in values.items():
|
||||
if value != self.reshade_original_values.get(key, ''):
|
||||
changes.append(f"{key}={value}")
|
||||
return changes
|
||||
|
||||
def _filter_reshade_settings(self, search_text: str) -> None:
|
||||
enabled = self.current_settings.get('PW_USE_RESHADE') == '1'
|
||||
self.reshade_settings_group.setVisible(
|
||||
enabled and (
|
||||
not search_text
|
||||
or search_text in "exe api font screenshot overlay"
|
||||
or search_text in _("Settings").lower()
|
||||
)
|
||||
)
|
||||
+10
-14
@@ -4,6 +4,7 @@ import hashlib
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
import zlib
|
||||
@@ -24,8 +25,7 @@ GOG_LOGIN_URL = (
|
||||
"redirect_uri=https%3A%2F%2Fembed.gog.com%2Fon_login_success%3Forigin%3Dclient&"
|
||||
"response_type=code&layout=galaxy"
|
||||
)
|
||||
GOGDL_RELEASE_SOURCE = "linux-gaming-ru/gogdl"
|
||||
GOGDL_RELEASE_URL = f"https://api.github.com/repos/{GOGDL_RELEASE_SOURCE}/releases/latest"
|
||||
GOGDL_RELEASE_URL = "https://api.github.com/repos/Heroic-Games-Launcher/heroic-gogdl/releases/latest"
|
||||
GOGDL_AUTH_TIMEOUT = 60
|
||||
GOGDL_UPDATE_INTERVAL = 30 * 24 * 60 * 60
|
||||
GOG_USER_TIMEOUT = 15
|
||||
@@ -77,11 +77,11 @@ class GOGAPI:
|
||||
self.games_dir = Path.home() / "Games"
|
||||
|
||||
def get_gogdl_path(self) -> str | None:
|
||||
"""Return the managed gogdl fork executable when installed."""
|
||||
"""Return an available gogdl executable."""
|
||||
bundled = self.bin_dir / "gogdl"
|
||||
if bundled.is_file() and os.access(bundled, os.X_OK) and self._get_gogdl_release_tag():
|
||||
if bundled.is_file() and os.access(bundled, os.X_OK):
|
||||
return str(bundled)
|
||||
return None
|
||||
return shutil.which("gogdl")
|
||||
|
||||
def build_command(self, arguments: list[str]) -> list[str]:
|
||||
"""Build a gogdl command with private configuration paths."""
|
||||
@@ -94,7 +94,7 @@ class GOGAPI:
|
||||
return [gogdl, "--auth-config-path", str(self.auth_path), *arguments]
|
||||
|
||||
def ensure_gogdl(self) -> str:
|
||||
"""Download and verify the latest linux-gaming-ru gogdl binary."""
|
||||
"""Download and verify the latest official Heroic gogdl binary."""
|
||||
existing = self.get_gogdl_path()
|
||||
if existing:
|
||||
return existing
|
||||
@@ -102,9 +102,9 @@ class GOGAPI:
|
||||
return self._install_gogdl_release(asset, release_tag)
|
||||
|
||||
def update_gogdl(self) -> str:
|
||||
"""Replace other gogdl binaries and update the managed fork."""
|
||||
"""Update the bundled gogdl binary to the latest official release."""
|
||||
bundled = self.bin_dir / "gogdl"
|
||||
if self.get_gogdl_path():
|
||||
if bundled.is_file() and os.access(bundled, os.X_OK):
|
||||
try:
|
||||
last_check = self.gogdl_version_path.stat().st_mtime
|
||||
except OSError:
|
||||
@@ -143,17 +143,13 @@ class GOGAPI:
|
||||
|
||||
def _get_gogdl_release_tag(self) -> str:
|
||||
try:
|
||||
version = self.gogdl_version_path.read_text(encoding="utf-8").strip()
|
||||
return self.gogdl_version_path.read_text(encoding="utf-8").strip()
|
||||
except OSError:
|
||||
return ""
|
||||
source, _, tag = version.partition(":")
|
||||
return tag if source == GOGDL_RELEASE_SOURCE else ""
|
||||
|
||||
def _install_gogdl_release(self, asset: dict, release_tag: str) -> str:
|
||||
path = self._download_gogdl_asset(asset)
|
||||
self.gogdl_version_path.write_text(
|
||||
f"{GOGDL_RELEASE_SOURCE}:{release_tag}", encoding="utf-8"
|
||||
)
|
||||
self.gogdl_version_path.write_text(release_tag, encoding="utf-8")
|
||||
return path
|
||||
|
||||
def _download_gogdl_asset(self, asset: dict) -> str:
|
||||
|
||||
@@ -93,6 +93,10 @@ class KeyboardInputMixin(InputMixin):
|
||||
keyboard.stop_backspace_repeat()
|
||||
|
||||
def eventFilter(self, obj: QObject, event: QEvent) -> bool:
|
||||
app = QApplication.instance()
|
||||
if not app:
|
||||
return QObject.eventFilter(cast(QObject, self), obj, event)
|
||||
|
||||
if event.type() == QEvent.Type.Show:
|
||||
if isinstance(obj, QMenu):
|
||||
SoundManager().play("open")
|
||||
@@ -158,7 +162,7 @@ class KeyboardInputMixin(InputMixin):
|
||||
logger.debug(f"Skipping event filter for non-QObject: {type(obj).__name__}")
|
||||
return False
|
||||
|
||||
return False
|
||||
return QObject.eventFilter(cast(QObject, self), obj, event)
|
||||
|
||||
def _qt_event_to_input_key(self, event: QEvent) -> int | None:
|
||||
native_key = self._native_scan_to_input_key(event)
|
||||
|
||||
@@ -118,6 +118,7 @@ install_data(
|
||||
'dialogs/settings_dialog.py',
|
||||
'dialogs/settings_mangohud.py',
|
||||
'dialogs/settings_gamescope.py',
|
||||
'dialogs/settings_reshade.py',
|
||||
'dialogs/settings_vkbasalt.py',
|
||||
'dialogs/proton_manager.py',
|
||||
'dialogs/prefix_backup.py',
|
||||
|
||||
@@ -149,6 +149,7 @@ def get_toggle_settings():
|
||||
'PW_USE_SPECIALK': _("Enable SpecialK (injection library for fixing graphics, latency and more)"),
|
||||
# Upscalers and Frame Generation
|
||||
'PW_USE_OPTISCALER': _("Enable OptiScaler (replacement upscaler / frame generator)"),
|
||||
'PW_USE_RESHADE': _("Enable ReShade post-processing"),
|
||||
'PW_USE_LS_FRAME_GEN': _("Enable Lossless Scaling frame generation (experimental)"),
|
||||
'PW_WINE_FULLSCREEN_FSR': _("FSR upscaling in fullscreen with ProtonGE below native resolution"),
|
||||
# Ray Tracing
|
||||
|
||||
@@ -152,38 +152,6 @@ def test_silent_exe_uses_existing_instance(
|
||||
startup.qt_app.exec.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("silent", [False, True])
|
||||
def test_silent_launch_skips_startup_workers(
|
||||
silent_startup: SimpleNamespace, monkeypatch: pytest.MonkeyPatch, silent: bool,
|
||||
) -> None:
|
||||
from PySide6.QtCore import QThread
|
||||
from portprotonqt import appimage_updater
|
||||
|
||||
startup = silent_startup
|
||||
window = MagicMock()
|
||||
startup.window.side_effect = None
|
||||
startup.window.return_value = window
|
||||
monkeypatch.setattr(startup.app, "QTimer", MagicMock())
|
||||
monkeypatch.setattr(QThread, "start", MagicMock())
|
||||
update_worker = MagicMock()
|
||||
monkeypatch.setattr(appimage_updater, "AppImageUpdateWorker", update_worker)
|
||||
monkeypatch.setattr(startup.app.display_config, "get_auto_fullscreen_gamepad", lambda: False)
|
||||
monkeypatch.setattr(startup.app.display_config, "get_start_minimized", lambda: False)
|
||||
monkeypatch.setattr(startup.app.display_config, "set_fullscreen", MagicMock())
|
||||
flags = ["--silent"] if silent else []
|
||||
monkeypatch.setattr(sys, "argv", ["portprotonqt", *flags, str(startup.exe)])
|
||||
|
||||
with pytest.raises(SystemExit) as result:
|
||||
startup.app.main()
|
||||
|
||||
assert result.value.code == 0
|
||||
assert window.silent_launch_mode is silent
|
||||
assert ("initialCommandWorker" in vars(window)) is (not silent)
|
||||
assert update_worker.called is (not silent)
|
||||
assert ("appimageUpdateWorker" in vars(window)) is (not silent)
|
||||
startup.window.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("flags,target", [
|
||||
([], "exe"),
|
||||
(["--silent", "--log"], "exe"),
|
||||
|
||||
+6
-76
@@ -1,6 +1,5 @@
|
||||
import os
|
||||
import zlib
|
||||
from contextlib import nullcontext
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock
|
||||
@@ -8,7 +7,7 @@ from unittest.mock import Mock
|
||||
import orjson
|
||||
import pytest
|
||||
|
||||
from portprotonqt.gog_api import GOGAPI, GOGDL_RELEASE_SOURCE, GOGDL_UPDATE_INTERVAL, GOG_PRODUCT_LOCALES
|
||||
from portprotonqt.gog_api import GOGAPI, GOGDL_UPDATE_INTERVAL, GOG_PRODUCT_LOCALES
|
||||
from portprotonqt.localization import LOCALE_MAP
|
||||
|
||||
|
||||
@@ -393,82 +392,13 @@ def test_ensure_gogdl_downloads_matching_architecture(tmp_path: Path, monkeypatc
|
||||
response = Mock()
|
||||
response.json.return_value = {"assets": [asset], "tag_name": "v1.2.1"}
|
||||
monkeypatch.setattr("portprotonqt.gog_api.platform.machine", lambda: "x86_64")
|
||||
request = Mock(return_value=response)
|
||||
monkeypatch.setattr("portprotonqt.gog_api.requests.get", request)
|
||||
monkeypatch.setattr("portprotonqt.gog_api.requests.get", lambda *args, **kwargs: response)
|
||||
monkeypatch.setattr(
|
||||
api, "_install_gogdl_release",
|
||||
lambda selected, tag: f'{selected["name"]}:{tag}',
|
||||
)
|
||||
|
||||
assert api.ensure_gogdl() == "gogdl_linux_x86_64:v1.2.1"
|
||||
assert request.call_args.args[0] == f"https://api.github.com/repos/{GOGDL_RELEASE_SOURCE}/releases/latest"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("operation", ["ensure_gogdl", "update_gogdl"])
|
||||
@pytest.mark.parametrize("version", ["", "v1.3.2", "Heroic-Games-Launcher/heroic-gogdl:v1.3.2"])
|
||||
def test_gogdl_replaces_existing_upstream(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, operation: str, version: str
|
||||
) -> None:
|
||||
monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path))
|
||||
api = GOGAPI()
|
||||
api.bin_dir.mkdir(parents=True)
|
||||
binary = api.bin_dir / "gogdl"
|
||||
binary.write_bytes(b"upstream")
|
||||
binary.chmod(0o755)
|
||||
if version:
|
||||
api.gogdl_version_path.write_text(version)
|
||||
latest = Mock(return_value=({"name": "gogdl_linux_x86_64"}, "v1.3.2"))
|
||||
monkeypatch.setattr(api, "_get_latest_gogdl_release", latest)
|
||||
|
||||
def download(_asset: dict) -> str:
|
||||
binary.write_bytes(b"fork")
|
||||
return str(binary)
|
||||
|
||||
monkeypatch.setattr(api, "_download_gogdl_asset", download)
|
||||
assert api.get_gogdl_path() is None
|
||||
assert getattr(api, operation)() == str(binary)
|
||||
latest.assert_called_once_with()
|
||||
assert binary.read_bytes() == b"fork"
|
||||
assert api.gogdl_version_path.read_text() == f"{GOGDL_RELEASE_SOURCE}:v1.3.2"
|
||||
assert api.get_gogdl_path() == str(binary)
|
||||
|
||||
|
||||
def test_gogdl_does_not_use_system_binary(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path))
|
||||
api = GOGAPI()
|
||||
which = Mock(return_value="/usr/bin/gogdl")
|
||||
monkeypatch.setattr("shutil.which", which)
|
||||
install = Mock(return_value=str(api.bin_dir / "gogdl"))
|
||||
monkeypatch.setattr(api, "_get_latest_gogdl_release", lambda: ({}, "v1.3.2"))
|
||||
monkeypatch.setattr(api, "_install_gogdl_release", install)
|
||||
|
||||
assert api.get_gogdl_path() is None
|
||||
assert api.ensure_gogdl() == str(api.bin_dir / "gogdl")
|
||||
install.assert_called_once_with({}, "v1.3.2")
|
||||
which.assert_not_called()
|
||||
|
||||
|
||||
def test_failed_gogdl_migration_keeps_old_files(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path))
|
||||
api = GOGAPI()
|
||||
api.bin_dir.mkdir(parents=True)
|
||||
binary = api.bin_dir / "gogdl"
|
||||
binary.write_bytes(b"upstream")
|
||||
binary.chmod(0o755)
|
||||
api.gogdl_version_path.write_text("v1.3.2")
|
||||
response = Mock()
|
||||
response.iter_content.return_value = [b"corrupted download"]
|
||||
monkeypatch.setattr("portprotonqt.gog_api.requests.get", lambda *args, **kwargs: nullcontext(response))
|
||||
asset = {"browser_download_url": "https://github.com/linux-gaming-ru/gogdl/releases/download/v1.3.2/gogdl_linux_x86_64", "digest": "sha256:invalid"}
|
||||
monkeypatch.setattr(api, "_get_latest_gogdl_release", lambda: (asset, "v1.3.2"))
|
||||
|
||||
with pytest.raises(OSError, match="checksum verification failed"):
|
||||
api.update_gogdl()
|
||||
|
||||
assert binary.read_bytes() == b"upstream"
|
||||
assert api.gogdl_version_path.read_text() == "v1.3.2"
|
||||
assert not binary.with_suffix(".part").exists()
|
||||
assert api.get_gogdl_path() is None
|
||||
|
||||
|
||||
def test_update_gogdl_skips_current_release(tmp_path: Path, monkeypatch) -> None:
|
||||
@@ -480,7 +410,7 @@ def test_update_gogdl_skips_current_release(tmp_path: Path, monkeypatch) -> None
|
||||
gogdl_path.parent.mkdir()
|
||||
gogdl_path.touch()
|
||||
gogdl_path.chmod(0o755)
|
||||
api.gogdl_version_path.write_text(f"{GOGDL_RELEASE_SOURCE}:v1.2.1", encoding="utf-8")
|
||||
api.gogdl_version_path.write_text("v1.2.1", encoding="utf-8")
|
||||
latest_release = Mock(return_value=({}, "v1.2.1"))
|
||||
monkeypatch.setattr(api, "_get_latest_gogdl_release", latest_release)
|
||||
install = Mock()
|
||||
@@ -500,7 +430,7 @@ def test_update_gogdl_checks_release_after_month(tmp_path: Path, monkeypatch) ->
|
||||
gogdl_path.parent.mkdir()
|
||||
gogdl_path.touch()
|
||||
gogdl_path.chmod(0o755)
|
||||
api.gogdl_version_path.write_text(f"{GOGDL_RELEASE_SOURCE}:v1.2.1", encoding="utf-8")
|
||||
api.gogdl_version_path.write_text("v1.2.1", encoding="utf-8")
|
||||
os.utime(api.gogdl_version_path, (1, 1))
|
||||
last_check = api.gogdl_version_path.stat().st_mtime
|
||||
monkeypatch.setattr(
|
||||
@@ -523,7 +453,7 @@ def test_update_gogdl_records_failed_monthly_check(tmp_path: Path, monkeypatch)
|
||||
gogdl_path.parent.mkdir()
|
||||
gogdl_path.touch()
|
||||
gogdl_path.chmod(0o755)
|
||||
api.gogdl_version_path.write_text(f"{GOGDL_RELEASE_SOURCE}:v1.2.1", encoding="utf-8")
|
||||
api.gogdl_version_path.write_text("v1.2.1", encoding="utf-8")
|
||||
os.utime(api.gogdl_version_path, (1, 1))
|
||||
last_check = api.gogdl_version_path.stat().st_mtime
|
||||
monkeypatch.setattr(
|
||||
@@ -562,7 +492,7 @@ def test_install_gogdl_release_records_version(tmp_path: Path, monkeypatch) -> N
|
||||
monkeypatch.setattr(api, "_download_gogdl_asset", lambda _asset: "/bin/gogdl")
|
||||
|
||||
assert api._install_gogdl_release({}, "v1.2.1") == "/bin/gogdl"
|
||||
assert api.gogdl_version_path.read_text(encoding="utf-8") == f"{GOGDL_RELEASE_SOURCE}:v1.2.1"
|
||||
assert api.gogdl_version_path.read_text(encoding="utf-8") == "v1.2.1"
|
||||
|
||||
|
||||
def test_authenticate_returns_gogdl_error(monkeypatch) -> None:
|
||||
|
||||
@@ -6,9 +6,8 @@ from typing import Any, cast
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from PySide6.QtCore import QEvent, QObject, QStringListModel, Qt
|
||||
from PySide6.QtWidgets import QApplication, QCheckBox, QComboBox, QDialog, QFrame, QHBoxLayout, QLineEdit, QListView, QMenu, QPushButton, QSlider, QStackedWidget, QTableWidget, QTableWidgetItem, QVBoxLayout, QWidget
|
||||
from PySide6.QtWidgets import QApplication, QCheckBox, QComboBox, QDialog, QFrame, QHBoxLayout, QLineEdit, QListView, QMenu, QPushButton, QSlider, QStackedWidget, QTableWidget, QTableWidgetItem, QWidget
|
||||
from pytest import MonkeyPatch, raises, mark
|
||||
from shiboken6 import delete
|
||||
|
||||
import portprotonqt.input_manager as input_manager
|
||||
import portprotonqt.input_manager.buttons as input_buttons
|
||||
@@ -36,34 +35,6 @@ HIDDEN_CARD_X = 20
|
||||
NEXT_CARD_X = 40
|
||||
|
||||
|
||||
@mark.parametrize("event_type", [QEvent.Type.Destroy, QEvent.Type.ChildRemoved, QEvent.Type.DeferredDelete])
|
||||
def test_keyboard_filter_ignores_deleted_layout(event_type: QEvent.Type) -> None:
|
||||
app = QApplication.instance() or QApplication([])
|
||||
manager = InputManager.__new__(InputManager)
|
||||
QObject.__init__(manager)
|
||||
layout = QVBoxLayout()
|
||||
delete(layout)
|
||||
|
||||
assert manager.eventFilter(layout, QEvent(event_type)) is False
|
||||
assert app is not None
|
||||
|
||||
|
||||
@mark.parametrize("event_type", [QEvent.Type.Destroy, QEvent.Type.ChildRemoved, QEvent.Type.DeferredDelete])
|
||||
def test_keyboard_filter_teardown_does_not_lookup_application(
|
||||
monkeypatch: MonkeyPatch, event_type: QEvent.Type,
|
||||
) -> None:
|
||||
app = QApplication.instance() or QApplication([])
|
||||
manager = InputManager.__new__(InputManager)
|
||||
QObject.__init__(manager)
|
||||
watched = QObject()
|
||||
lookup = MagicMock(side_effect=AssertionError("Application lookup during teardown"))
|
||||
monkeypatch.setattr(QApplication, "instance", lookup)
|
||||
|
||||
assert manager.eventFilter(watched, QEvent(event_type)) is False
|
||||
lookup.assert_not_called()
|
||||
assert app is not None
|
||||
|
||||
|
||||
@mark.parametrize("initial", [True, False])
|
||||
def test_gamepad_auto_fullscreen_shows_boot_on_hotplug(monkeypatch: MonkeyPatch, initial: bool) -> None:
|
||||
app = QApplication.instance() or QApplication([])
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""Progressive library loading, drawing and input regressions."""
|
||||
from collections.abc import Callable, Generator
|
||||
from pathlib import Path
|
||||
from time import perf_counter
|
||||
import logging
|
||||
from types import SimpleNamespace
|
||||
from typing import Any, cast
|
||||
from unittest.mock import MagicMock
|
||||
@@ -397,7 +399,7 @@ def real_cover_scene(
|
||||
|
||||
|
||||
@pytest.mark.parametrize("animation_type", ["gradient", "glow"])
|
||||
def test_real_cover_loading_yields_and_focuses_first_paint(
|
||||
def test_real_cover_loading_and_first_paint_focus_latency(
|
||||
real_cover_scene: tuple[GameLibraryManager, Path],
|
||||
monkeypatch: pytest.MonkeyPatch, animation_type: str,
|
||||
) -> None:
|
||||
@@ -405,14 +407,14 @@ def test_real_cover_loading_yields_and_focuses_first_paint(
|
||||
monkeypatch.setattr(manager.theme, "GAME_CARD_ANIMATION",
|
||||
{**manager.theme.GAME_CARD_ANIMATION, "card_animation_type": animation_type})
|
||||
threads: list[bool] = []
|
||||
paints: list[bool] = []
|
||||
ticks: list[tuple[int, int]] = []
|
||||
paints: list[tuple[float, bool]] = []
|
||||
ticks: list[float] = []
|
||||
original_paint = GameCard.paintEvent
|
||||
original_cover = GameCard.on_cover_loaded
|
||||
|
||||
def record_paint(card: GameCard, event: Any) -> None:
|
||||
if card.name == "Game 0":
|
||||
paints.append(card._focused or card._hovered)
|
||||
paints.append((perf_counter(), card._focused or card._hovered))
|
||||
original_paint(card, event)
|
||||
|
||||
def record_cover(card: GameCard, pixmap: QPixmap) -> None:
|
||||
@@ -420,7 +422,7 @@ def test_real_cover_loading_yields_and_focuses_first_paint(
|
||||
original_cover(card, pixmap)
|
||||
|
||||
def sample() -> None:
|
||||
ticks.append((len(manager.game_card_cache), len(threads)))
|
||||
ticks.append(perf_counter())
|
||||
if len(threads) == len(games) and not manager._incremental_add_queue:
|
||||
loop.quit()
|
||||
|
||||
@@ -432,14 +434,18 @@ def test_real_cover_loading_yields_and_focuses_first_paint(
|
||||
timer.setInterval(5)
|
||||
timer.timeout.connect(sample)
|
||||
timer.start()
|
||||
QTimer.singleShot(5000, loop.quit)
|
||||
QTimer.singleShot(2000, loop.quit)
|
||||
manager.set_games(games)
|
||||
loop.exec()
|
||||
timer.stop()
|
||||
assert len(threads) == len(games) and all(threads), threads
|
||||
assert paints and paints[0], paints
|
||||
assert any(0 < cards < len(games) for cards, _ in ticks), ticks
|
||||
assert any(0 < covers < len(games) for _, covers in ticks), ticks
|
||||
highlighted = next((paint for paint in paints if paint[1]), None)
|
||||
assert highlighted is not None
|
||||
focus_delay = highlighted[0] - paints[0][0]
|
||||
max_gap = max(after - before for before, after in zip(ticks, ticks[1:], strict=False))
|
||||
logging.getLogger(__name__).info("%s: highlight %.1f ms, UI pause %.1f ms", animation_type, focus_delay * 1000, max_gap * 1000)
|
||||
assert focus_delay < 0.05, paints
|
||||
assert max_gap < 0.1, ticks
|
||||
|
||||
|
||||
@pytest.mark.parametrize("scenario", [("grid", 0), ("grid", 1), ("vertical", 0), ("vertical", 1)])
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"""Tests for config/portproton.py — exec_line parsing, icon sanitization, launcher tail extraction."""
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from pytest import MonkeyPatch
|
||||
from pytest import MonkeyPatch, mark
|
||||
|
||||
from portprotonqt.config.portproton import (
|
||||
extract_exec_target_path,
|
||||
@@ -192,3 +193,180 @@ class TestThemedLaunchIconNames:
|
||||
|
||||
def test_reg_has_icon(self):
|
||||
assert "reg" in THEMED_LAUNCH_ICON_NAMES[".reg"]
|
||||
|
||||
|
||||
def test_run_after_batch_is_created_next_to_exe() -> None:
|
||||
helper = Path("build-aux/share/portproton/scripts/functions_helper").read_text(
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
assert 'run_after_dir="$(dirname "${PW_EXE_FILE}")"' in helper
|
||||
assert 'pw_exe_file_win="$("${WINELOADER}" winepath -w "${PW_EXE_FILE}"' in helper
|
||||
assert "chcp 65001 >nul" in helper
|
||||
assert 'start "" "${pw_exe_file_win}" ${LAUNCH_PARAMETERS}' in helper
|
||||
assert 'start "" /unix "${PW_RUN_AFTER_EXE}"' in helper
|
||||
assert 'LAUNCH_PARAMETERS="" proxy_launch_parameters="" \\' in helper
|
||||
assert 'pw_run "${PW_VD_TMP[@]}" "${run_after_bat}"' in helper
|
||||
|
||||
|
||||
def test_game_launch_marker_is_emitted_for_wine_and_proton() -> None:
|
||||
helper = Path("build-aux/share/portproton/scripts/functions_helper").read_text(
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
marker = "printf '%s\\n' 'PORTPROTONQT_GAME_LAUNCH_STARTED'"
|
||||
assert helper.count(marker) == 2
|
||||
|
||||
|
||||
def test_user_conf_get_without_name_lists_active_values(tmp_path: Path) -> None:
|
||||
helper = Path("build-aux/share/portproton/scripts/functions_helper").read_text()
|
||||
definition = helper.split("manage_user_conf_value () {", 1)[1].split(
|
||||
"use_exiftool () {", 1
|
||||
)[0]
|
||||
user_conf = tmp_path / "user.conf"
|
||||
user_conf.write_text(
|
||||
'# comment\nexport PW_USE_ESYNC="1"\n# export PW_USE_FSYNC="1"\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
script = "manage_user_conf_value () {" + definition + '\nmanage_user_conf_value get\n'
|
||||
|
||||
result = subprocess.run(
|
||||
["bash", "-c", script], capture_output=True, text=True,
|
||||
env={"PATH": os.defpath, "USER_CONF": str(user_conf)}, check=False,
|
||||
)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert result.stdout == 'PW_USE_ESYNC="1"\n'
|
||||
|
||||
|
||||
@mark.parametrize("runtime,logging", [("0", "0"), ("0", "1"), ("1", "0"), ("1", "1")])
|
||||
def test_wine_exit_code_survives_log_output_and_wineserver_wait(
|
||||
tmp_path: Path, runtime: str, logging: str,
|
||||
) -> None:
|
||||
helper = Path("build-aux/share/portproton/scripts/functions_helper").read_text()
|
||||
definition = helper.split("pw_run () {", 1)[1].split("export -f pw_run", 1)[0]
|
||||
script = "pw_run () {" + definition + '''
|
||||
check_variables () { :; }
|
||||
pw_launch_wrapper () { return 37; }
|
||||
wait_wineserver () { return 0; }
|
||||
print_info () { :; }
|
||||
PATH_TO_GAME="$1"
|
||||
PW_TMPFS_PATH="$1"
|
||||
PW_LOG_FILE="$1/wine.log"
|
||||
PW_EXE_FILE="$1/game.exe"
|
||||
PW_USE_RUNTIME="$2"
|
||||
PW_LOG="$3"
|
||||
pw_run "$PW_EXE_FILE"
|
||||
exit $?
|
||||
'''
|
||||
(tmp_path / "game.exe").touch()
|
||||
|
||||
result = subprocess.run(
|
||||
["bash", "-c", script, "bash", str(tmp_path), runtime, logging],
|
||||
capture_output=True, text=True, env={"PATH": os.defpath}, check=False,
|
||||
)
|
||||
|
||||
assert result.returncode == 37, result.stderr
|
||||
assert result.stdout.count("PORTPROTONQT_GAME_EXIT_CODE=37") == 1
|
||||
|
||||
|
||||
def test_vk_gpu_info_uses_build_aux_binary() -> None:
|
||||
helper = Path("build-aux/share/portproton/scripts/functions_helper").read_text(
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
assert '/../../../bin/vk_gpu_info"' in helper
|
||||
assert "dev-scripts/vk_gpu_info" not in helper
|
||||
|
||||
|
||||
def test_reshade_prefers_directx_for_unity() -> None:
|
||||
helper = Path("build-aux/share/portproton/scripts/functions_helper").read_text(
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
assert "source=$(echo \"$detect\" | grep '^SOURCE=' | cut -d= -f2-)" in helper
|
||||
assert (
|
||||
'[[ "${source##*/}" == UnityPlayer.dll && "$highest" != None ]] '
|
||||
"&& vulkan=false"
|
||||
) in helper
|
||||
|
||||
|
||||
def test_reshade_and_optiscaler_are_mutually_exclusive() -> None:
|
||||
helper = Path("build-aux/share/portproton/scripts/functions_helper").read_text(
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
assert 'DISABLE_EDIT_DB_LIST+=" PW_USE_OPTISCALER PW_USE_SPECIALK"' in helper
|
||||
assert 'DISABLE_EDIT_DB_LIST+=" PW_USE_RESHADE"' in helper
|
||||
|
||||
|
||||
def test_reshade_repeated_link_does_not_create_nested_shaders(tmp_path: Path) -> None:
|
||||
helper = Path("build-aux/share/portproton/scripts/functions_helper").read_text(
|
||||
encoding="utf-8",
|
||||
)
|
||||
link_function = helper.split("try_force_link_dir () {", 1)[1].split("\n}", 1)[0]
|
||||
shaders = tmp_path / "shaders"
|
||||
shaders.mkdir()
|
||||
game = tmp_path / "game"
|
||||
game.mkdir()
|
||||
script = f'try_force_link_dir () {{{link_function}\n}}\n'
|
||||
link_call = helper.split(' [[ "$PW_USE_RESHADE" != 1 ]] && return 0', 1)[1]
|
||||
link_call = link_call.split(' pw_specialk_set_ini_value', 1)[0]
|
||||
script += f'root="$1"\nPATH_TO_GAME="$2"\n{link_call * 2}'
|
||||
|
||||
subprocess.run(
|
||||
["bash", "-c", script, "bash", str(tmp_path), str(game)],
|
||||
check=True,
|
||||
)
|
||||
|
||||
assert (game / "ReShade_shaders").resolve() == shaders
|
||||
assert not (shaders / "shaders").exists()
|
||||
|
||||
|
||||
def test_reshade_disable_cleans_unreal_shipping_directory(tmp_path: Path) -> None:
|
||||
helper = Path("build-aux/share/portproton/scripts/functions_helper").read_text(
|
||||
encoding="utf-8",
|
||||
)
|
||||
sync_function = helper.split("pw_reshade_sync_files () {", 1)[1].split("\n}", 1)[0]
|
||||
game_root = tmp_path / "game"
|
||||
game_root.mkdir()
|
||||
game_exe = game_root / "Praest.exe"
|
||||
game_exe.touch()
|
||||
game = game_root / "Praest" / "Binaries" / "Win64"
|
||||
game.mkdir(parents=True)
|
||||
runtime = tmp_path / "plugins" / "reshade_windows" / "reshade" / "6.8.0_Addon"
|
||||
runtime.mkdir(parents=True)
|
||||
reshade_dll = runtime / "ReShade32.dll"
|
||||
reshade_dll.touch()
|
||||
(game / "opengl32.dll").symlink_to(reshade_dll)
|
||||
(game / "opengl32.dll.b").symlink_to(reshade_dll)
|
||||
(game / "ReShadePreset.ini").write_text("[GENERAL]\n")
|
||||
(game / "reshade_version").write_text("6.8.0_Addon\nopengl32.dll\n")
|
||||
script = f'pw_reshade_sync_files () {{{sync_function}\n}}\n'
|
||||
script += 'try_remove_file () { rm -f "$1"; }\npw_reshade_sync_files\n'
|
||||
|
||||
subprocess.run(
|
||||
["bash", "-c", script],
|
||||
check=True,
|
||||
env={
|
||||
**os.environ,
|
||||
"PW_EXE_FILE": str(game_exe),
|
||||
"PW_PLUGINS_PATH": str(tmp_path / "plugins"),
|
||||
"PW_USE_RESHADE": "0",
|
||||
"PW_RESHADE_API": "",
|
||||
"PW_RESHADE_EXE": "",
|
||||
"PATH_TO_GAME": str(game),
|
||||
},
|
||||
)
|
||||
|
||||
assert not (game / "opengl32.dll").exists()
|
||||
assert not (game / "opengl32.dll.b").exists()
|
||||
assert not (game / "ReShadePreset.ini").exists()
|
||||
|
||||
|
||||
def test_ini_writer_matches_spaces_around_equals() -> None:
|
||||
helper = Path("build-aux/share/portproton/scripts/functions_helper").read_text(
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
assert '^[[:space:]]*$key[[:space:]]*=' in helper
|
||||
@@ -37,6 +37,7 @@ from portprotonqt.dialogs.settings_mangohud import (
|
||||
MANGOHUD_TOGGLE_DESCRIPTIONS,
|
||||
MangoHudSettingsMixin,
|
||||
)
|
||||
from portprotonqt.dialogs.settings_reshade import ReShadeSettingsMixin
|
||||
from portprotonqt.dialogs.settings_vkbasalt import VkBasaltSettingsMixin
|
||||
|
||||
|
||||
@@ -90,6 +91,45 @@ def test_settings_dialog_search_accepts_typing_without_search_focus() -> None:
|
||||
QApplication.sendPostedEvents(None, QEvent.Type.DeferredDelete)
|
||||
|
||||
|
||||
def test_reshade_collects_enable_exe_api_and_font() -> None:
|
||||
settings = ReShadeSettingsMixin()
|
||||
settings.current_settings = {
|
||||
"PW_USE_RESHADE": "1",
|
||||
"PW_RESHADE_SCREENSHOT_KEY": "44,0,0,0",
|
||||
"PW_RESHADE_OVERLAY_KEY": "36,0,0,0",
|
||||
}
|
||||
settings.original_values = {"PW_USE_RESHADE": "0"}
|
||||
settings.reshade_original_values = {
|
||||
"PW_RESHADE_EXE": "",
|
||||
"PW_RESHADE_API": "auto",
|
||||
"PW_RESHADE_FONT": "",
|
||||
"PW_RESHADE_SCREENSHOT_PATH": "",
|
||||
"PW_RESHADE_SCREENSHOT_KEY": "44,0,0,0",
|
||||
"PW_RESHADE_OVERLAY_KEY": "36,0,0,0",
|
||||
}
|
||||
settings.reshade_exe_edit = QLineEdit("/games/game.exe")
|
||||
settings.reshade_api_combo = CustomComboBox()
|
||||
settings.reshade_api_combo.addItems(["Auto", "Vulkan"])
|
||||
settings.reshade_api_combo.setCurrentText("Vulkan")
|
||||
settings.reshade_font_combo = CustomComboBox()
|
||||
settings.reshade_font_combo.addItem("DejaVu Sans", "DejaVu Sans")
|
||||
settings.reshade_screenshot_edit = QLineEdit("/screenshots")
|
||||
|
||||
assert settings._collect_reshade_changes() == [
|
||||
"PW_USE_RESHADE=1",
|
||||
"PW_RESHADE_EXE=/games/game.exe",
|
||||
"PW_RESHADE_API=vulkan",
|
||||
"PW_RESHADE_FONT=DejaVu Sans",
|
||||
"PW_RESHADE_SCREENSHOT_PATH=/screenshots",
|
||||
]
|
||||
assert settings._reshade_virtual_key(
|
||||
Qt.Key.Key_Semicolon.value, Qt.KeyboardModifier.NoModifier
|
||||
) == 186
|
||||
assert settings._reshade_virtual_key(
|
||||
Qt.Key.Key_1.value, Qt.KeyboardModifier.KeypadModifier
|
||||
) == 97
|
||||
|
||||
|
||||
def test_mangohud_search_ignores_unavailable_categories() -> None:
|
||||
settings = MangoHudSettingsMixin()
|
||||
settings.mangohud_category_groups = {}
|
||||
@@ -631,48 +671,3 @@ def test_winetricks_centered_checkboxes_and_install_selection(
|
||||
dialog.install_selected(force=True)
|
||||
dialog._start_install_process.assert_called_once_with(["art2kmin"], True)
|
||||
dialog.deleteLater()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("theme_name", ["standart", "standart-light", "classic", "classic-light"])
|
||||
def test_settings_table_stops_repainting_when_idle(theme_name: str) -> None:
|
||||
from PySide6.QtCore import QObject
|
||||
from PySide6.QtWidgets import QTableWidgetItem
|
||||
from portprotonqt.dialogs.settings_dialog import ExeSettingsDialog
|
||||
from portprotonqt.theme_manager import ThemeWrapper, load_theme
|
||||
|
||||
theme = ThemeWrapper(load_theme(theme_name))
|
||||
dialog = cast(ExeSettingsDialog, SimpleNamespace(theme=theme))
|
||||
|
||||
class PaintFilter(QObject):
|
||||
paints = 0
|
||||
|
||||
def eventFilter(self, obj: QObject, event: QEvent) -> bool:
|
||||
if event.type() == QEvent.Type.Paint:
|
||||
self.paints += 1
|
||||
return ExeSettingsDialog.eventFilter(dialog, obj, event)
|
||||
return False
|
||||
|
||||
table = QTableWidget(3, 3)
|
||||
table.resize(800, 400)
|
||||
table.setStyleSheet(theme.WINETRICKS_TABBLE_STYLE)
|
||||
table.setColumnWidth(0, 500)
|
||||
table.setColumnHidden(2, True)
|
||||
table.viewport().setProperty("ppqt_settings_values", True)
|
||||
paint_filter = PaintFilter(table)
|
||||
table.viewport().installEventFilter(paint_filter)
|
||||
for row in range(3):
|
||||
table.setItem(row, 0, QTableWidgetItem("Setting\nDescription " * 5))
|
||||
combo = CustomComboBox(theme=theme)
|
||||
combo.addItem("Value")
|
||||
table.setCellWidget(row, 1, combo)
|
||||
table.show()
|
||||
for selected_row, width in ((0, 500), (1, 300)):
|
||||
table.selectRow(selected_row)
|
||||
table.setColumnWidth(0, width)
|
||||
for _ in range(10):
|
||||
QApplication.processEvents()
|
||||
QTest.qWait(100)
|
||||
paints = paint_filter.paints
|
||||
QTest.qWait(100)
|
||||
assert paint_filter.paints == paints
|
||||
table.close()
|
||||
Reference in new issue
Block a user