Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56e103793e | ||
|
|
0391b894ee
|
No files matched your search
@@ -7,6 +7,7 @@
|
||||
|
||||
### Added
|
||||
|
||||
- Для Run After / Wrapper добавлен раскрываемый список с отдельным полем для каждой записи, удалением записей и кнопкой полной очистки
|
||||
- В фильтр библиотеки добавлен выбор нескольких источников одновременно с сохранением выбранной комбинации
|
||||
- При добавлении игр в Steam добавлена загрузка обложек из SteamGridDB, если Steam AppID не найден
|
||||
- В диалоги добавления и редактирования EXE добавлен выбор пользовательской иконки
|
||||
@@ -20,6 +21,7 @@
|
||||
|
||||
### Changed
|
||||
|
||||
- В Run After / Wrapper выбор файлов ограничен `.exe` и файлами с правом исполнения в Linux
|
||||
- В режиме CLOUD данные ProtonDB и ссылки на обложки SteamGridDB загружаются через `deckyloader.ru`
|
||||
- Повторный запуск с `--silent` теперь запрашивает подтверждение: при согласии текущий PortProtonQt и запущенные игры завершаются, затем приложение запускается заново для новой игры: при отказе новый запуск полностью отменяется, текущая сессия продолжает работать
|
||||
- Найденные Steam AppID сохраняются в кеше, чтобы при повторной загрузке библиотеки пропускать чтение метаданных EXE и поиск по каталогу Steam
|
||||
|
||||
@@ -1572,7 +1572,6 @@ pw_kill_autostart () {
|
||||
export -f pw_kill_autostart
|
||||
|
||||
stop_portproton () {
|
||||
pw_remove_session_env
|
||||
[[ "$int_xneur" == "1" ]] && xneur &
|
||||
|
||||
if [[ "$PW_USE_US_LAYOUT" == "1" ]] \
|
||||
@@ -3846,7 +3845,6 @@ start_portproton () {
|
||||
pw_mangohud_check
|
||||
pw_vkbasalt_check
|
||||
pw_lsfg_vk_check
|
||||
pw_save_session_env || print_warning "Could not save Wine session environment"
|
||||
}
|
||||
|
||||
pw_run () {
|
||||
@@ -3998,80 +3996,6 @@ pw_run () {
|
||||
}
|
||||
export -f pw_run
|
||||
|
||||
pw_save_session_env () {
|
||||
local session_dir="${PORT_DATA_PATH}/data/tmp/wine-session-$(id -u)"
|
||||
local session_tmp
|
||||
|
||||
( umask 077; mkdir -p "$session_dir" ) || return 1
|
||||
[[ ! -L "$session_dir" && -O "$session_dir" ]] || return 1
|
||||
[[ "$(stat -c %a "$session_dir")" == 700 ]] || return 1
|
||||
|
||||
session_tmp="$(mktemp "$session_dir/env.XXXXXX")" || return 1
|
||||
if ( export PW_TERM; export -p > "$session_tmp" ) \
|
||||
&& mv -fT "$session_tmp" "$session_dir/session.env" ; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
rm -f "$session_tmp"
|
||||
return 1
|
||||
}
|
||||
|
||||
pw_remove_session_env () {
|
||||
local session_dir="${PORT_DATA_PATH}/data/tmp/wine-session-$(id -u)"
|
||||
[[ ! -L "$session_dir" && -O "$session_dir" ]] || return 0
|
||||
local session_env="$session_dir/session.env"
|
||||
[[ ! -L "$session_env" && -O "$session_env" ]] || return 0
|
||||
if grep -qsx "declare -x PW_START_PID=\"${PW_START_PID}\"" "$session_env" ; then
|
||||
rm -f "$session_env"
|
||||
fi
|
||||
}
|
||||
|
||||
pw_session_tool () {
|
||||
local tool="$1" session_pid session_env
|
||||
local session_dir="${PORT_DATA_PATH}/data/tmp/wine-session-$(id -u)"
|
||||
local session_wait_seconds="${2:-30}" wait_left
|
||||
wait_left="$session_wait_seconds"
|
||||
|
||||
case "$tool" in
|
||||
winefile|taskmgr|winecfg|regedit|cmd|uninstaller) ;;
|
||||
*) print_error "Unsupported Wine tool: $tool" ; return 1 ;;
|
||||
esac
|
||||
|
||||
session_env="$session_dir/session.env"
|
||||
while (( wait_left > 0 )) ; do
|
||||
if [[ ! -L "$session_dir" && -O "$session_dir" ]] \
|
||||
&& [[ "$(stat -c %a "$session_dir")" == 700 ]] \
|
||||
&& [[ ! -L "$session_env" && -O "$session_env" ]] \
|
||||
&& [[ "$(stat -c %a "$session_env")" == 600 ]] \
|
||||
&& session_pid="$(sed -n 's/^declare -x PW_START_PID="\([0-9]*\)"$/\1/p' "$session_env")" \
|
||||
&& [[ -n "$session_pid" ]] \
|
||||
&& kill -0 "$session_pid" 2>/dev/null ; then
|
||||
break
|
||||
fi
|
||||
|
||||
sleep 1
|
||||
(( --wait_left ))
|
||||
done
|
||||
|
||||
if (( wait_left == 0 )) ; then
|
||||
print_error "Wine session unavailable after ${session_wait_seconds} seconds"
|
||||
return 2
|
||||
fi
|
||||
|
||||
source "$session_env" || return 1
|
||||
PATH_TO_GAME="${WINEPREFIX}/drive_c"
|
||||
[[ -d "$PATH_TO_GAME" ]] || return 1
|
||||
|
||||
unset LAUNCH_PARAMETERS PW_RUN_AFTER_EXE PW_RUN_GAMESCOPE PW_USE_TERMINAL
|
||||
PW_TMPFS_PATH="$(mktemp -d "$session_dir/tool.XXXXXX")" || return 1
|
||||
export PW_TMPFS_PATH
|
||||
export PW_LOG_FILE="$PW_TMPFS_PATH/wine.log"
|
||||
|
||||
[[ "$tool" == cmd ]] && PW_USE_TERMINAL=1
|
||||
[[ "$tool" == winecfg || "$tool" == regedit ]] && export GST_PLUGIN_SYSTEM_PATH_1_0=""
|
||||
pw_run "$tool" || return 1
|
||||
}
|
||||
|
||||
pw_launch_wrapper () {
|
||||
if [[ -z "${PW_RUN_AFTER_EXE:-}" ]] ; then
|
||||
"$@"
|
||||
@@ -4088,6 +4012,25 @@ pw_launch_wrapper () {
|
||||
return
|
||||
fi
|
||||
|
||||
local -a wrapper_scripts=()
|
||||
local wrapper_script
|
||||
local wrapper_scripts_valid=1
|
||||
if [[ "${PW_RUN_AFTER_EXE}" == *","* ]] ; then
|
||||
IFS=',' read -r -a wrapper_scripts <<< "${PW_RUN_AFTER_EXE}"
|
||||
for wrapper_script in "${wrapper_scripts[@]}" ; do
|
||||
if [[ ! -f "${wrapper_script}" || "${wrapper_script,,}" == *.exe ]] ; then
|
||||
wrapper_scripts_valid=0
|
||||
fi
|
||||
done
|
||||
if [[ "${wrapper_scripts_valid}" == 1 ]] ; then
|
||||
for wrapper_script in "${wrapper_scripts[@]}" ; do
|
||||
"${wrapper_script}"
|
||||
done
|
||||
"$@"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
local -a wrapper_cmd
|
||||
eval "wrapper_cmd=(${PW_RUN_AFTER_EXE})"
|
||||
if [[ ${#wrapper_cmd[@]} -eq 0 ]] ; then
|
||||
@@ -4269,9 +4212,22 @@ portwine_launch () {
|
||||
fi
|
||||
done
|
||||
else
|
||||
local -a run_after_exes=() run_after_environment=()
|
||||
local run_after_entry run_after_index
|
||||
local run_after_valid=1
|
||||
if [[ -f "${PW_RUN_AFTER_EXE:-}" ]] ; then
|
||||
run_after_exes=("${PW_RUN_AFTER_EXE}")
|
||||
else
|
||||
IFS=',' read -r -a run_after_exes <<< "${PW_RUN_AFTER_EXE:-}"
|
||||
fi
|
||||
for run_after_entry in "${run_after_exes[@]}" ; do
|
||||
if [[ ! -f "${run_after_entry}" || "${run_after_entry,,}" != *.exe ]] ; then
|
||||
run_after_valid=0
|
||||
fi
|
||||
done
|
||||
if [[ -n "${PW_RUN_AFTER_EXE:-}" ]] \
|
||||
&& [[ -f "${PW_RUN_AFTER_EXE}" ]] \
|
||||
&& [[ "${PW_RUN_AFTER_EXE,,}" =~ \.exe$ ]] \
|
||||
&& [[ "${run_after_valid}" == 1 ]] \
|
||||
&& [[ ${#run_after_exes[@]} -gt 0 ]] \
|
||||
&& [[ "${PW_EXE_FILE,,}" =~ \.exe$ ]]
|
||||
then
|
||||
local wait_delay wait_delay_int run_after_bat
|
||||
@@ -4295,6 +4251,7 @@ portwine_launch () {
|
||||
pw_exe_file_win="z:${PW_EXE_FILE//\//\\}"
|
||||
fi
|
||||
pw_exe_file_win="${pw_exe_file_win//\//\\}"
|
||||
local PW_RUN_AFTER_EXE="${run_after_exes[0]}"
|
||||
run_after_exe_win="$("${WINELOADER}" winepath -w "${PW_RUN_AFTER_EXE}" 2>/dev/null | tr -d '\r')"
|
||||
if [[ -z "${run_after_exe_win}" ]] ; then
|
||||
if [[ "${PW_RUN_AFTER_EXE}" == "${wine_drive_c}/"* ]] ; then
|
||||
@@ -4313,10 +4270,30 @@ timeout /t ${wait_delay_int} /nobreak
|
||||
start "" "%PW_RUN_AFTER_SECOND_PATH%"
|
||||
EOF
|
||||
|
||||
LAUNCH_PARAMETERS="" proxy_launch_parameters="" \
|
||||
PW_RUN_AFTER_MAIN_PATH="${pw_exe_file_win}" \
|
||||
PW_RUN_AFTER_SECOND_PATH="${run_after_exe_win}" \
|
||||
pw_run "${PW_VD_TMP[@]}" "${run_after_bat}"
|
||||
local run_after_first_win="${run_after_exe_win}"
|
||||
for ((run_after_index=1; run_after_index<${#run_after_exes[@]}; run_after_index++)) ; do
|
||||
run_after_entry="${run_after_exes[run_after_index]}"
|
||||
run_after_exe_win="$("${WINELOADER}" winepath -w "${run_after_entry}" 2>/dev/null | tr -d '\r')"
|
||||
if [[ -z "${run_after_exe_win}" ]] ; then
|
||||
if [[ "${run_after_entry}" == "${wine_drive_c}/"* ]] ; then
|
||||
run_after_exe_win="c:\\${run_after_entry#"${wine_drive_c}/"}"
|
||||
else
|
||||
run_after_exe_win="z:${run_after_entry//\//\\}"
|
||||
fi
|
||||
fi
|
||||
run_after_environment+=("PW_RUN_AFTER_PATH_${run_after_index}=${run_after_exe_win//\//\\}")
|
||||
printf 'start "" "%%PW_RUN_AFTER_PATH_%s%%"\r\n' "${run_after_index}" >> "${run_after_bat}"
|
||||
done
|
||||
|
||||
(
|
||||
if [[ ${#run_after_environment[@]} -gt 0 ]] ; then
|
||||
export "${run_after_environment[@]}"
|
||||
fi
|
||||
PW_RUN_AFTER_EXE="" LAUNCH_PARAMETERS="" proxy_launch_parameters="" \
|
||||
PW_RUN_AFTER_MAIN_PATH="${pw_exe_file_win}" \
|
||||
PW_RUN_AFTER_SECOND_PATH="${run_after_first_win}" \
|
||||
pw_run "${PW_VD_TMP[@]}" "${run_after_bat}"
|
||||
)
|
||||
try_remove_file "${run_after_bat}"
|
||||
else
|
||||
case "${PW_EXE_FILE,,}" in
|
||||
|
||||
@@ -67,21 +67,6 @@ export PW_LOG_FILE="${PORT_DATA_PATH}/PortProton.log"
|
||||
# shellcheck source=/dev/null
|
||||
source "$PORT_SCRIPTS_PATH/functions_helper"
|
||||
|
||||
if [[ "${1:-}" == "cli" && "${2:-}" == "--wine-session-tool" ]] ; then
|
||||
session_unavailable_status=2
|
||||
session_wait_seconds=30
|
||||
[[ -n "${4:-}" && -n "${5:-}" ]] && session_wait_seconds=1
|
||||
if pw_session_tool "${3:-}" "$session_wait_seconds" ; then
|
||||
exit 0
|
||||
else
|
||||
session_tool_status=$?
|
||||
fi
|
||||
if [[ "$session_tool_status" != "$session_unavailable_status" || -z "${4:-}" || -z "${5:-}" ]] ; then
|
||||
python_module dbus_tools notify -i error "Wine" "Failed to start Wine tool: ${3:-}" &>/dev/null
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
export PORT_WINE_TMP_PATH="${PORT_DATA_PATH}/data/tmp"
|
||||
create_new_dir "$PORT_WINE_TMP_PATH"
|
||||
rm -f "$PORT_WINE_TMP_PATH"/*.{exe,msi,tar}*
|
||||
@@ -295,7 +280,11 @@ case "$1" in
|
||||
--show-ppdb Show the content of .ppdb file for specified .exe file
|
||||
--backup-prefix Backup specified prefix to a file
|
||||
--restore-prefix Restore prefix from backup file
|
||||
--wine-session-tool Open a Wine tool in the current session, or use optional WINE and prefix
|
||||
--winefile Open wine file explorer, requires WINE version and prefix name
|
||||
--winecfg Open wine configuration, requires WINE version and prefix name
|
||||
--winecmd Open wine command prompt, requires WINE version and prefix name
|
||||
--winereg Open wine registry editor, requires WINE version and prefix name
|
||||
--wine_uninstaller Open wine uninstaller, requires WINE version and prefix name
|
||||
--winetricks-list-all List winetricks DLLs, fonts, and settings
|
||||
--winetricks-install Install winetricks items, requires WINE version and prefix name
|
||||
--clear_pfx Clear specified prefix, requires WINE version and prefix name
|
||||
@@ -311,7 +300,7 @@ Usage examples:
|
||||
portproton cli --del-user-conf PW_MANGOHUD
|
||||
portproton cli --backup-prefix DEFAULT /path/to/backup/directory
|
||||
portproton cli --restore-prefix /path/to/backup/file.ppack
|
||||
portproton cli --wine-session-tool winecfg WINE_LG DEFAULT
|
||||
portproton cli --winecfg WINE_LG DEFAULT
|
||||
portproton cli --winetricks-list-all WINE_LG DEFAULT
|
||||
portproton cli --winetricks-install WINE_LG DEFAULT --no-force corefonts
|
||||
portproton cli --mangohud-preview \"fps,frametime,cpu_temp,gpu_temp\"
|
||||
@@ -473,21 +462,37 @@ Usage examples:
|
||||
pw_unpack_prefix "$2"
|
||||
exit $?
|
||||
;;
|
||||
--wine-session-tool)
|
||||
tool="$2"
|
||||
case "$tool" in
|
||||
winefile|taskmgr|winecfg|regedit|cmd|uninstaller) ;;
|
||||
*) print_error "Unsupported Wine tool: $tool" ; exit 1 ;;
|
||||
esac
|
||||
get_wine_and_pfx "$3" "$4"
|
||||
--winefile)
|
||||
get_wine_and_pfx "$2" "$3"
|
||||
start_portproton
|
||||
[[ "$tool" == winecfg || "$tool" == regedit ]] && export GST_PLUGIN_SYSTEM_PATH_1_0=""
|
||||
if [[ "$tool" == cmd ]] ; then
|
||||
cd "${PORT_DATA_PATH}/data/prefixes/${PW_PREFIX_NAME}/drive_c" || fatal "Prefix drive_c directory not found"
|
||||
PW_USE_TERMINAL=1 pw_run "$tool"
|
||||
else
|
||||
pw_run "$tool"
|
||||
fi
|
||||
pw_run winefile
|
||||
stop_portproton
|
||||
;;
|
||||
--winecfg)
|
||||
get_wine_and_pfx "$2" "$3"
|
||||
start_portproton
|
||||
export GST_PLUGIN_SYSTEM_PATH_1_0=""
|
||||
pw_run winecfg
|
||||
stop_portproton
|
||||
;;
|
||||
--winecmd)
|
||||
get_wine_and_pfx "$2" "$3"
|
||||
start_portproton
|
||||
cd "${PORT_DATA_PATH}/data/prefixes/${PW_PREFIX_NAME}/drive_c" || fatal "Prefix drive_c directory not found"
|
||||
PW_USE_TERMINAL=1 pw_run cmd
|
||||
stop_portproton
|
||||
;;
|
||||
--winereg)
|
||||
get_wine_and_pfx "$2" "$3"
|
||||
start_portproton
|
||||
export GST_PLUGIN_SYSTEM_PATH_1_0=""
|
||||
pw_run regedit
|
||||
stop_portproton
|
||||
;;
|
||||
--wine_uninstaller)
|
||||
get_wine_and_pfx "$2" "$3"
|
||||
start_portproton
|
||||
pw_run uninstaller
|
||||
stop_portproton
|
||||
;;
|
||||
--winetricks-list-all)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# Generate shell completion files for bash, zsh, and fish
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
@@ -607,7 +607,9 @@ class FileExplorer(DraggableDialog):
|
||||
if not self.directory_only:
|
||||
files = [f for f in items if os.path.isfile(os.path.join(self.current_path, f))]
|
||||
if self.file_filter:
|
||||
if isinstance(self.file_filter, str):
|
||||
if callable(self.file_filter):
|
||||
files = [f for f in files if self.file_filter(os.path.join(self.current_path, f))]
|
||||
elif isinstance(self.file_filter, str):
|
||||
files = [f for f in files if f.lower().endswith(self.file_filter)]
|
||||
elif isinstance(self.file_filter, tuple):
|
||||
files = [f for f in files if any(f.lower().endswith(ext) for ext in self.file_filter)]
|
||||
|
||||
@@ -290,6 +290,11 @@ class ExeSettingsDialog(
|
||||
"""Resolve run-after executable relative to the main executable."""
|
||||
if not exe_path:
|
||||
return exe_path
|
||||
if "," in exe_path and not os.path.isfile(os.path.expanduser(exe_path)):
|
||||
return ",".join(
|
||||
self._resolve_run_after_exe_path(path.strip())
|
||||
for path in exe_path.split(",") if path.strip()
|
||||
)
|
||||
normalized = os.path.normpath(os.path.expanduser(exe_path))
|
||||
if os.path.isabs(normalized):
|
||||
return normalized
|
||||
@@ -300,6 +305,70 @@ class ExeSettingsDialog(
|
||||
return normalized
|
||||
return os.path.normpath(os.path.join(game_dir, normalized))
|
||||
|
||||
def _toggle_run_after_entries(self, target: QLineEdit) -> None:
|
||||
table = self.advanced_table
|
||||
row = next(index for index in range(table.rowCount())
|
||||
if table.cellWidget(index, 1) is target.parentWidget())
|
||||
expanded = bool(target.property("ppqt_run_after_expanded"))
|
||||
target.setProperty("ppqt_run_after_expanded", not expanded)
|
||||
if expanded:
|
||||
while row + 1 < table.rowCount():
|
||||
item = table.item(row + 1, 0)
|
||||
if item is None or item.data(Qt.ItemDataRole.UserRole) != "run_after_entry":
|
||||
break
|
||||
table.removeRow(row + 1)
|
||||
return
|
||||
entries = [target.text()] if os.path.isfile(target.text()) else target.text().split(",")
|
||||
if not target.text():
|
||||
entries = [""]
|
||||
for index, path in enumerate(entries):
|
||||
entry_row = row + index + 1
|
||||
table.insertRow(entry_row)
|
||||
item = QTableWidgetItem(path)
|
||||
item.setData(Qt.ItemDataRole.UserRole, "run_after_entry")
|
||||
item.setFlags(Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled)
|
||||
table.setItem(entry_row, 0, item)
|
||||
entry_edit = QLineEdit(path)
|
||||
entry_edit.setStyleSheet(self.theme.LINE_EDIT_STYLE)
|
||||
entry_edit.installEventFilter(self)
|
||||
entry_edit.textChanged.connect(
|
||||
lambda value, offset=index, widget=target: self._update_run_after_entry(widget, offset, value)
|
||||
)
|
||||
table.setCellWidget(entry_row, 0, entry_edit)
|
||||
button = AutoSizeButton("", icon=ThemeManager().get_icon("delete", as_path=True))
|
||||
button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||
button.clicked.connect(
|
||||
lambda _checked=False, offset=index, widget=target: self._remove_run_after_entry(widget, offset)
|
||||
)
|
||||
table.setCellWidget(entry_row, 1, button)
|
||||
table.resizeRowsToContents()
|
||||
|
||||
def _update_run_after_entry(self, target: QLineEdit, index: int, value: str) -> None:
|
||||
entries = target.text().split(",")
|
||||
if index < len(entries):
|
||||
entries[index] = value
|
||||
target.setText(",".join(entries))
|
||||
|
||||
def _remove_run_after_entry(self, target: QLineEdit, index: int) -> None:
|
||||
entries = [target.text()] if os.path.isfile(target.text()) else target.text().split(",")
|
||||
entries.pop(index)
|
||||
self._toggle_run_after_entries(target)
|
||||
target.setText(",".join(entries))
|
||||
if entries:
|
||||
self._toggle_run_after_entries(target)
|
||||
target.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
|
||||
def _clear_run_after_entries(self, target: QLineEdit) -> None:
|
||||
table = self.advanced_table
|
||||
for row in reversed(range(table.rowCount())):
|
||||
item = table.item(row, 0)
|
||||
if item and item.data(Qt.ItemDataRole.UserRole) == "run_after_entry":
|
||||
table.removeRow(row)
|
||||
target.setProperty("ppqt_run_after_expanded", False)
|
||||
target.clear()
|
||||
if target.isVisible():
|
||||
target.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
|
||||
def _get_setting_file_selector_path(self, current_path: str) -> str:
|
||||
"""Get initial path for setting file selectors."""
|
||||
initial_path = os.path.expanduser("~")
|
||||
@@ -403,7 +472,7 @@ class ExeSettingsDialog(
|
||||
self.advanced_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
|
||||
self.advanced_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
|
||||
self.advanced_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
|
||||
self.advanced_table.horizontalHeader().resizeSection(1, 230)
|
||||
self.advanced_table.horizontalHeader().resizeSection(1, self.theme.RUN_AFTER_VALUE_WIDTH)
|
||||
self.advanced_table.setWordWrap(True)
|
||||
self.advanced_table.verticalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
|
||||
self.advanced_table.setTextElideMode(Qt.TextElideMode.ElideNone)
|
||||
@@ -951,10 +1020,12 @@ class ExeSettingsDialog(
|
||||
if setting['key'] == 'PW_RUN_AFTER_EXE':
|
||||
text_container = QWidget()
|
||||
text_container.setProperty("ppqt_run_after_exe_widget", True)
|
||||
text_layout = QHBoxLayout(text_container)
|
||||
text_layout.setContentsMargins(0, 0, 0, 0)
|
||||
text_layout.setSpacing(6)
|
||||
text_layout.addWidget(line_edit)
|
||||
container_layout = QVBoxLayout(text_container)
|
||||
container_layout.setContentsMargins(0, 0, 0, 0)
|
||||
container_layout.addWidget(line_edit)
|
||||
line_edit.hide()
|
||||
text_layout = QHBoxLayout()
|
||||
container_layout.addLayout(text_layout)
|
||||
|
||||
browse_button = AutoSizeButton("...", icon=ThemeManager().get_icon("folder", as_path=True))
|
||||
browse_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||
@@ -966,26 +1037,38 @@ class ExeSettingsDialog(
|
||||
def open_run_after_exe_selector(
|
||||
_checked: bool = False,
|
||||
target_line_edit=line_edit,
|
||||
file_filter="",
|
||||
file_filter=lambda path: path.lower().endswith(".exe") or os.access(path, os.X_OK),
|
||||
):
|
||||
from portprotonqt.dialogs.file_explorer import FileExplorer
|
||||
|
||||
initial_path = self._get_setting_file_selector_path(
|
||||
target_line_edit.text().strip()
|
||||
)
|
||||
|
||||
file_explorer = FileExplorer(
|
||||
self,
|
||||
theme=self.theme,
|
||||
file_filter=file_filter,
|
||||
initial_path=initial_path,
|
||||
initial_path=self._get_setting_file_selector_path(
|
||||
target_line_edit.text().rsplit(",", 1)[-1].strip()
|
||||
),
|
||||
)
|
||||
file_explorer.file_signal.file_selected.connect(
|
||||
lambda file_path, target=target_line_edit: target.setText(os.path.normpath(file_path))
|
||||
lambda path, target=target_line_edit: target.setText(
|
||||
",".join(filter(None, [target.text().strip(), os.path.normpath(path)]))
|
||||
)
|
||||
)
|
||||
file_explorer.exec()
|
||||
|
||||
browse_button.clicked.connect(open_run_after_exe_selector)
|
||||
expand_button = AutoSizeButton("≡")
|
||||
expand_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||
expand_button.clicked.connect(
|
||||
lambda _checked=False, target=line_edit: self._toggle_run_after_entries(target)
|
||||
)
|
||||
clear_button = AutoSizeButton("", icon=ThemeManager().get_icon("delete", as_path=True))
|
||||
clear_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||
clear_button.clicked.connect(
|
||||
lambda _checked=False, target=line_edit: self._clear_run_after_entries(target)
|
||||
)
|
||||
text_layout.addWidget(clear_button)
|
||||
text_layout.addWidget(expand_button)
|
||||
text_layout.addWidget(browse_button)
|
||||
self.advanced_table.setCellWidget(row, 1, text_container)
|
||||
else:
|
||||
@@ -1732,15 +1815,20 @@ class ExeSettingsDialog(
|
||||
item = table.item(row, 0)
|
||||
if item is None:
|
||||
continue
|
||||
widget = table.cellWidget(row, 0)
|
||||
if widget is not None and not isinstance(widget, QLabel):
|
||||
continue
|
||||
text = item.data(Qt.ItemDataRole.AccessibleTextRole)
|
||||
if text is None:
|
||||
text = item.text()
|
||||
item.setData(Qt.ItemDataRole.AccessibleTextRole, text)
|
||||
item.setText("")
|
||||
label = cast(QLabel | None, table.cellWidget(row, 0))
|
||||
if label is None:
|
||||
text = item.text()
|
||||
label = QLabel()
|
||||
label.setTextFormat(Qt.TextFormat.RichText)
|
||||
label.setWordWrap(True)
|
||||
label.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents)
|
||||
item.setData(Qt.ItemDataRole.AccessibleTextRole, text)
|
||||
item.setText("")
|
||||
table.setCellWidget(row, 0, label)
|
||||
label.setAutoFillBackground(False)
|
||||
label.setAttribute(Qt.WidgetAttribute.WA_NoSystemBackground)
|
||||
@@ -1755,7 +1843,6 @@ class ExeSettingsDialog(
|
||||
label.setPalette(palette)
|
||||
if label.foregroundRole() != QPalette.ColorRole.Text:
|
||||
label.setForegroundRole(QPalette.ColorRole.Text)
|
||||
text = item.data(Qt.ItemDataRole.AccessibleTextRole)
|
||||
name, separator, description = text.partition("\n")
|
||||
formatted_text = escape(name)
|
||||
if separator:
|
||||
|
||||
@@ -476,7 +476,14 @@ class SettingsInputMixin(InputMixin):
|
||||
if table != self.settings_dialog.advanced_table:
|
||||
table.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
return
|
||||
entry_widget = table.cellWidget(row, 0)
|
||||
if isinstance(entry_widget, QLineEdit):
|
||||
entry_widget.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
return
|
||||
cell_widget = table.cellWidget(row, 1)
|
||||
if isinstance(cell_widget, QPushButton):
|
||||
cell_widget.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
return
|
||||
if isinstance(cell_widget, QComboBox):
|
||||
line_edit = cell_widget.lineEdit()
|
||||
if cell_widget.isEditable() and line_edit is not None:
|
||||
@@ -492,15 +499,33 @@ class SettingsInputMixin(InputMixin):
|
||||
and cell_widget.property("ppqt_run_after_exe_widget")
|
||||
):
|
||||
line_edit = cell_widget.findChild(QLineEdit)
|
||||
if line_edit and line_edit.isEnabled():
|
||||
if line_edit and line_edit.isEnabled() and not line_edit.isHidden():
|
||||
line_edit.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
return
|
||||
button = cell_widget.findChild(QPushButton)
|
||||
if button and button.isEnabled():
|
||||
button.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
return
|
||||
table.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
|
||||
def _move_settings_inline_button_focus(self, table: QTableWidget, value: int) -> bool:
|
||||
"""Move focus between inline line edit and button in settings cell."""
|
||||
if not self.settings_dialog or table != self.settings_dialog.advanced_table:
|
||||
return False
|
||||
focused = QApplication.focusWidget()
|
||||
for row in range(table.rowCount()):
|
||||
entry_widget = table.cellWidget(row, 0)
|
||||
delete_button = table.cellWidget(row, 1)
|
||||
if not isinstance(entry_widget, QLineEdit) or not isinstance(delete_button, QPushButton):
|
||||
continue
|
||||
if focused not in (entry_widget, delete_button):
|
||||
continue
|
||||
widget = delete_button if focused == entry_widget else entry_widget
|
||||
if not widget.isEnabled():
|
||||
return False
|
||||
table.setCurrentCell(row, 1)
|
||||
widget.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
return True
|
||||
if table.currentColumn() != 1:
|
||||
return False
|
||||
|
||||
@@ -508,15 +533,14 @@ class SettingsInputMixin(InputMixin):
|
||||
if cell_widget is None or not cell_widget.property("ppqt_run_after_exe_widget"):
|
||||
return False
|
||||
|
||||
line_edit = cell_widget.findChild(QLineEdit)
|
||||
button = cell_widget.findChild(QPushButton)
|
||||
widgets = [cell_widget.findChild(QLineEdit), *cell_widget.findChildren(QPushButton)]
|
||||
widgets = [widget for widget in widgets if widget and widget.isEnabled() and not widget.isHidden()]
|
||||
focused = QApplication.focusWidget()
|
||||
if value > 0 and focused == line_edit and button and button.isEnabled():
|
||||
button.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
return True
|
||||
if value < 0 and focused == button and line_edit and line_edit.isEnabled():
|
||||
line_edit.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
return True
|
||||
if focused in widgets:
|
||||
index = widgets.index(focused) + (1 if value > 0 else -1)
|
||||
if 0 <= index < len(widgets):
|
||||
widgets[index].setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
return True
|
||||
return False
|
||||
|
||||
def _focus_first_row_in_current_settings_table(self):
|
||||
|
||||
@@ -9,7 +9,7 @@ msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: support@linux-gaming.ru\n"
|
||||
"POT-Creation-Date: 2026-03-22 13:50+0500\n"
|
||||
"PO-Revision-Date: 2026-10-24 09:27+0000\n"
|
||||
"PO-Revision-Date: 2026-10-26 12:27+0000\n"
|
||||
"Last-Translator: Andrei Stepanov <adem4ik@gmail.com>\n"
|
||||
"Language-Team: Russian <https://translate.codeberg.org/projects/portprotonqt/"
|
||||
"portprotonqt/ru/>\n"
|
||||
@@ -996,9 +996,8 @@ msgstr "Пользовательская обложка:"
|
||||
msgid "Enter local path or URL for cover image"
|
||||
msgstr "Введите локальный путь или URL обложки"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Custom Icon:"
|
||||
msgstr "Пользовательская обложка:"
|
||||
msgstr "Пользовательский значок:"
|
||||
|
||||
msgid "Add shortcut to:"
|
||||
msgstr "Добавить ярлык:"
|
||||
|
||||
@@ -160,12 +160,11 @@ class MainWindowWineTabMixin(_MainWindowTypingBase):
|
||||
tools_grid.setSpacing(6)
|
||||
|
||||
tools = [
|
||||
("winecfg", _("Wine Configuration")),
|
||||
("regedit", _("Registry Editor")),
|
||||
("winefile", _("File Explorer")),
|
||||
("cmd", _("Command Prompt")),
|
||||
("uninstaller", _("Uninstaller")),
|
||||
("taskmgr", _("Task Manager")),
|
||||
("--winecfg", _("Wine Configuration")),
|
||||
("--winereg", _("Registry Editor")),
|
||||
("--winefile", _("File Explorer")),
|
||||
("--winecmd", _("Command Prompt")),
|
||||
("--wine_uninstaller", _("Uninstaller")),
|
||||
]
|
||||
|
||||
for i, (tool_cmd, tool_name) in enumerate(tools):
|
||||
@@ -238,7 +237,7 @@ class MainWindowWineTabMixin(_MainWindowTypingBase):
|
||||
env_vars = os.environ.copy()
|
||||
self._check_missing_prefix_by_name_before_launch(prefix, env_vars)
|
||||
start_sh = self.start_sh
|
||||
cmd = start_sh + ["cli", "--wine-session-tool", cli_arg, wine, prefix]
|
||||
cmd = start_sh + ["cli", cli_arg, wine, prefix]
|
||||
|
||||
# Show progress bar before launch
|
||||
self.wine_progress_bar.setVisible(True)
|
||||
@@ -270,7 +269,16 @@ class MainWindowWineTabMixin(_MainWindowTypingBase):
|
||||
|
||||
def _check_wine_process(self, cli_arg):
|
||||
"""Check if target .exe process started."""
|
||||
target_exe = f"{cli_arg}.exe"
|
||||
exe_map = {
|
||||
"--winecfg": "winecfg.exe",
|
||||
"--winereg": "regedit.exe",
|
||||
"--winefile": "winefile.exe",
|
||||
"--winecmd": "cmd.exe",
|
||||
"--wine_uninstaller": "uninstaller.exe",
|
||||
}
|
||||
target_exe = exe_map.get(cli_arg, "")
|
||||
if not target_exe:
|
||||
return
|
||||
|
||||
# Check processes via psutil
|
||||
for proc in psutil.process_iter(attrs=["name"]):
|
||||
|
||||
@@ -370,3 +370,5 @@ WINETRICKS_CHECK_COLUMN_WIDTH = 80
|
||||
WINETRICKS_ROW_HEIGHT = 36
|
||||
|
||||
GAME_FOLDER_IMPORT_DIALOG_SIZE = (800, 600)
|
||||
|
||||
RUN_AFTER_VALUE_WIDTH = 230
|
||||
@@ -3,7 +3,6 @@ import subprocess
|
||||
import signal
|
||||
import psutil
|
||||
import os
|
||||
from PySide6.QtCore import QProcess
|
||||
from PySide6.QtWidgets import QSystemTrayIcon, QMenu, QApplication, QMessageBox
|
||||
from PySide6.QtGui import QIcon, QAction
|
||||
from portprotonqt.logger import get_logger
|
||||
@@ -46,39 +45,6 @@ def _restart_args(args: list[str]) -> list[str]:
|
||||
return restart_args
|
||||
|
||||
|
||||
def create_wine_tools_menu(parent: QMenu, start_sh: list[str]) -> QMenu:
|
||||
"""Create tools that join the running game's Wine session."""
|
||||
menu = QMenu("Wine", parent)
|
||||
tools = (
|
||||
("winefile", _("File Explorer")),
|
||||
("taskmgr", _("Task Manager")),
|
||||
("winecfg", _("Wine Configuration")),
|
||||
("regedit", _("Registry Editor")),
|
||||
("cmd", _("Command Prompt")),
|
||||
("uninstaller", _("Uninstaller")),
|
||||
)
|
||||
for tool, title in tools:
|
||||
action = menu.addAction(title)
|
||||
action.triggered.connect(
|
||||
lambda checked=False, name=tool: launch_session_wine_tool(start_sh, name)
|
||||
)
|
||||
return menu
|
||||
|
||||
|
||||
def launch_session_wine_tool(start_sh: list[str], tool: str) -> None:
|
||||
"""Start a Wine tool without running the game's startup or cleanup."""
|
||||
if not start_sh:
|
||||
logger.error("Cannot launch Wine tool: PortProton command is missing")
|
||||
QMessageBox.warning(None, _("Error"), _("Failed to start process."))
|
||||
return
|
||||
started, _pid = QProcess.startDetached(
|
||||
start_sh[0], start_sh[1:] + ["cli", "--wine-session-tool", tool]
|
||||
)
|
||||
if not started:
|
||||
logger.error("Failed to start Wine tool %s", tool)
|
||||
QMessageBox.warning(None, _("Error"), _("Failed to start process."))
|
||||
|
||||
|
||||
class TrayManager:
|
||||
"""Tray management module for PortProtonQt.
|
||||
|
||||
@@ -117,12 +83,8 @@ class TrayManager:
|
||||
self.pause_game_action.setEnabled(False)
|
||||
self.pause_game_action.triggered.connect(self.toggle_game_pause)
|
||||
|
||||
self.wine_tools_menu = create_wine_tools_menu(self.tray_menu, self.main_window.start_sh)
|
||||
self.wine_tools_menu.setEnabled(False)
|
||||
|
||||
self.tray_menu.addAction(self.pause_game_action)
|
||||
self.tray_menu.addAction(self.stop_game_action)
|
||||
self.tray_menu.addMenu(self.wine_tools_menu)
|
||||
self.tray_menu.addSeparator()
|
||||
self.tray_menu.addSeparator()
|
||||
|
||||
@@ -141,7 +103,6 @@ class TrayManager:
|
||||
|
||||
self.tray_menu.addAction(self.pause_game_action)
|
||||
self.tray_menu.addAction(self.stop_game_action)
|
||||
self.tray_menu.addMenu(self.wine_tools_menu)
|
||||
self.update_game_actions()
|
||||
if self.minimal_mode:
|
||||
return
|
||||
@@ -190,7 +151,6 @@ class TrayManager:
|
||||
target_exe = getattr(self.main_window, "target_exe", None)
|
||||
has_running_game = bool(game_processes or target_exe)
|
||||
self.stop_game_action.setEnabled(has_running_game)
|
||||
self.wine_tools_menu.setEnabled(has_running_game)
|
||||
self.pause_game_action.setEnabled(bool(processes))
|
||||
paused = self._are_processes_paused(processes)
|
||||
self.pause_game_action.setText(_("Resume Game") if paused else _("Pause Game"))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Tests for themed file explorer icons."""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from typing import cast
|
||||
@@ -29,6 +30,30 @@ from portprotonqt.dialogs.file_explorer import (
|
||||
_application = QApplication.instance() or QApplication([])
|
||||
|
||||
|
||||
def test_executable_filter_keeps_exe_and_linux_executables(tmp_path: Path) -> None:
|
||||
from PySide6.QtCore import QMimeDatabase
|
||||
from PySide6.QtWidgets import QLabel
|
||||
|
||||
for name in ("Game.EXE", "launcher", "notes.txt"):
|
||||
(tmp_path / name).touch()
|
||||
(tmp_path / "launcher").chmod(0o755)
|
||||
(tmp_path / "folder").mkdir()
|
||||
explorer = SimpleNamespace(
|
||||
file_list=QListWidget(), current_path=str(tmp_path), directory_only=False,
|
||||
thumbnail_cache={}, pending_thumbnails={}, deferred_thumbnails={}, file_items={},
|
||||
path_history={}, path_label=QLabel(), mime_db=QMimeDatabase(),
|
||||
file_filter=lambda path: path.lower().endswith(".exe") or os.access(path, os.X_OK),
|
||||
_get_list_icon=lambda name: None, _get_file_type_icon=lambda path: None,
|
||||
schedule_thumbnail_loading=lambda: None,
|
||||
)
|
||||
|
||||
FileExplorer.update_file_list(cast(FileExplorer, explorer))
|
||||
|
||||
assert [explorer.file_list.item(i).text() for i in range(explorer.file_list.count())] == [
|
||||
"../", "folder/", "Game.EXE", "launcher",
|
||||
]
|
||||
|
||||
|
||||
def test_gamepad_opens_file_explorer_context_menu(monkeypatch: MonkeyPatch) -> None:
|
||||
explorer = FileExplorer.__new__(FileExplorer)
|
||||
QDialog.__init__(explorer)
|
||||
|
||||
@@ -1632,3 +1632,28 @@ def test_confirm_on_open_combo_activates_highlighted_row() -> None:
|
||||
assert not combo.view().isVisible()
|
||||
combo.close()
|
||||
application.processEvents()
|
||||
|
||||
|
||||
@mark.parametrize("direction", [-1, 1])
|
||||
@mark.parametrize("column", [0, 1])
|
||||
def test_run_after_dpad_reaches_delete_from_entry(direction: int, column: int) -> None:
|
||||
from portprotonqt.input_manager.settings import SettingsInputMixin
|
||||
|
||||
app = QApplication.instance() or QApplication([])
|
||||
table = QTableWidget(1, 2)
|
||||
entry = QLineEdit("game.exe")
|
||||
remove = QPushButton("delete")
|
||||
table.setCellWidget(0, 0, entry)
|
||||
table.setCellWidget(0, 1, remove)
|
||||
manager = SimpleNamespace(settings_dialog=SimpleNamespace(advanced_table=table))
|
||||
table.show()
|
||||
table.activateWindow()
|
||||
table.setCurrentCell(0, column)
|
||||
entry.setFocus()
|
||||
app.processEvents()
|
||||
assert SettingsInputMixin._move_settings_inline_button_focus(cast(SettingsInputMixin, manager), table, direction)
|
||||
assert QApplication.focusWidget() == remove
|
||||
assert SettingsInputMixin._move_settings_inline_button_focus(cast(SettingsInputMixin, manager), table, -direction)
|
||||
assert QApplication.focusWidget() == entry
|
||||
table.close()
|
||||
table.deleteLater()
|
||||
+4
-101
@@ -42,7 +42,7 @@ from portprotonqt.game_library_manager import FullLibraryTile, GameLibraryManage
|
||||
from portprotonqt.input_manager import InputManager
|
||||
from portprotonqt.main_window import MainWindow
|
||||
from portprotonqt.search_utils import SearchOptimizer, search_index
|
||||
from portprotonqt.tray_manager import TrayManager, create_wine_tools_menu, launch_session_wine_tool
|
||||
from portprotonqt.tray_manager import TrayManager
|
||||
from portprotonqt.themes.standart.styles.constants import GAME_CARD_ANIMATION
|
||||
from portprotonqt.portproton_api import remove_empty_custom_data_dirs
|
||||
import portprotonqt.tabs.autoinstall_tab as autoinstall_tab_module
|
||||
@@ -374,71 +374,18 @@ def test_minimal_tray_contains_game_actions() -> None:
|
||||
_application = QApplication.instance() or QApplication([])
|
||||
manager = TrayManager.__new__(TrayManager)
|
||||
manager.tray_menu = QMenu()
|
||||
owner = QWidget()
|
||||
manager.pause_game_action = QAction("Pause Game", owner)
|
||||
manager.stop_game_action = QAction("Stop Game", owner)
|
||||
manager.wine_tools_menu = create_wine_tools_menu(manager.tray_menu, ["portproton"])
|
||||
manager.pause_game_action = QAction("Pause Game", manager.tray_menu)
|
||||
manager.stop_game_action = QAction("Stop Game", manager.tray_menu)
|
||||
manager.minimal_mode = True
|
||||
manager.update_game_actions = MagicMock()
|
||||
|
||||
manager.refresh_tray_menu()
|
||||
manager.refresh_tray_menu()
|
||||
|
||||
assert manager.tray_menu.actions() == [
|
||||
manager.pause_game_action,
|
||||
manager.stop_game_action,
|
||||
manager.wine_tools_menu.menuAction(),
|
||||
]
|
||||
assert manager.update_game_actions.call_count == 2
|
||||
|
||||
|
||||
@mark.parametrize("tool_index,tool", list(enumerate(
|
||||
("winefile", "taskmgr", "winecfg", "regedit", "cmd", "uninstaller")
|
||||
)))
|
||||
def test_wine_tools_menu_launches_session_tool(
|
||||
monkeypatch: MonkeyPatch, tool_index: int, tool: str,
|
||||
) -> None:
|
||||
_application = QApplication.instance() or QApplication([])
|
||||
parent = QMenu()
|
||||
start = MagicMock(return_value=(True, 123))
|
||||
monkeypatch.setattr("portprotonqt.tray_manager.QProcess.startDetached", start)
|
||||
menu = create_wine_tools_menu(parent, ["bash", "/path with spaces/start.sh"])
|
||||
|
||||
assert len(menu.actions()) == 6
|
||||
menu.actions()[tool_index].trigger()
|
||||
|
||||
start.assert_called_once_with(
|
||||
"bash", ["/path with spaces/start.sh", "cli", "--wine-session-tool", tool]
|
||||
)
|
||||
|
||||
|
||||
def test_wine_tool_start_failure_is_reported(monkeypatch: MonkeyPatch) -> None:
|
||||
_application = QApplication.instance() or QApplication([])
|
||||
warning = MagicMock()
|
||||
monkeypatch.setattr("portprotonqt.tray_manager.QMessageBox.warning", warning)
|
||||
monkeypatch.setattr(
|
||||
"portprotonqt.tray_manager.QProcess.startDetached", lambda *args: (False, 0)
|
||||
)
|
||||
|
||||
launch_session_wine_tool(["portproton"], "winefile")
|
||||
|
||||
warning.assert_called_once()
|
||||
|
||||
|
||||
@mark.parametrize("running", [False, True])
|
||||
def test_wine_tools_menu_requires_running_game(running: bool) -> None:
|
||||
_application = QApplication.instance() or QApplication([])
|
||||
manager = TrayManager.__new__(TrayManager)
|
||||
manager.main_window = SimpleNamespace(game_processes=[], target_exe="game.exe" if running else None)
|
||||
manager.stop_game_action = QAction()
|
||||
manager.pause_game_action = QAction()
|
||||
manager.wine_tools_menu = QMenu()
|
||||
manager._game_process_tree = MagicMock(return_value=[])
|
||||
|
||||
manager.update_game_actions()
|
||||
|
||||
assert manager.wine_tools_menu.isEnabled() is running
|
||||
|
||||
manager.update_game_actions.assert_called_once_with()
|
||||
|
||||
def test_tray_pauses_game_process_tree(monkeypatch: MonkeyPatch) -> None:
|
||||
child = MagicMock(pid=12)
|
||||
@@ -3431,47 +3378,3 @@ def test_custom_combo_opens_readonly_field_on_mouse_release(readonly: bool) -> N
|
||||
assert combo.view().isVisible() is readonly
|
||||
combo.hidePopup()
|
||||
combo.close()
|
||||
|
||||
|
||||
@mark.parametrize("tool", ["winecfg", "regedit", "winefile", "cmd", "uninstaller", "taskmgr"])
|
||||
@mark.parametrize("running", [False, True])
|
||||
def test_wine_settings_tools_use_unified_command(
|
||||
monkeypatch: MonkeyPatch, tool: str, running: bool,
|
||||
) -> None:
|
||||
process = MagicMock()
|
||||
monkeypatch.setattr("portprotonqt.tabs.wine_tab.QProcess", MagicMock(return_value=process))
|
||||
window = MagicMock()
|
||||
window.game_processes = [MagicMock()] if running else []
|
||||
window.target_exe = "game.exe" if running else None
|
||||
window.start_sh = ["bash", "/path with spaces/start.sh"]
|
||||
window.wineCombo.currentText.return_value = "WINE_LG"
|
||||
window.prefixCombo.currentText.return_value = "my prefix"
|
||||
|
||||
WineMixin.launch_generic_tool(window, tool)
|
||||
|
||||
process.start.assert_called_once_with(
|
||||
"bash", ["/path with spaces/start.sh", "cli", "--wine-session-tool", tool, "WINE_LG", "MY_PREFIX"],
|
||||
)
|
||||
window._start_wine_process_monitor.assert_called_once_with(tool)
|
||||
|
||||
|
||||
|
||||
def test_silent_tray_includes_wine_tools_menu(monkeypatch: MonkeyPatch) -> None:
|
||||
_application = QApplication.instance() or QApplication([])
|
||||
manager = TrayManager.__new__(TrayManager)
|
||||
manager.tray_menu = QMenu()
|
||||
owner = QWidget()
|
||||
manager.pause_game_action = QAction("Pause Game", owner)
|
||||
manager.stop_game_action = QAction("Stop Game", owner)
|
||||
manager.wine_tools_menu = create_wine_tools_menu(manager.tray_menu, ["portproton"])
|
||||
manager.update_game_actions = MagicMock()
|
||||
start = MagicMock(return_value=(True, 123))
|
||||
monkeypatch.setattr("portprotonqt.tray_manager.QProcess.startDetached", start)
|
||||
|
||||
manager.set_minimal_mode()
|
||||
|
||||
tools = manager.tray_menu.actions()[2].menu()
|
||||
assert isinstance(tools, QMenu)
|
||||
assert len(tools.actions()) == 6
|
||||
tools.actions()[1].trigger()
|
||||
start.assert_called_once_with("portproton", ["cli", "--wine-session-tool", "taskmgr"])
|
||||
@@ -43,20 +43,22 @@ gui_config () {
|
||||
|
||||
|
||||
@mark.parametrize("second_name", ["Game.exe", "Trainer.exe"])
|
||||
def test_run_after_accepts_same_exe(tmp_path: Path, second_name: str) -> None:
|
||||
@mark.parametrize("copies", [1, 3])
|
||||
def test_run_after_accepts_same_exe(tmp_path: Path, second_name: str, copies: int) -> None:
|
||||
helper = Path("build-aux/share/portproton/scripts/functions_helper").read_text()
|
||||
condition = helper.split(' if [[ -n "${PW_RUN_AFTER_EXE:-}" ]]', 1)[1].split(
|
||||
condition = helper.split(' local -a run_after_exes=', 1)[1].split(
|
||||
" then", 1,
|
||||
)[0]
|
||||
condition = ' local -a run_after_exes=' + condition
|
||||
main_exe = tmp_path / "Game.exe"
|
||||
second_exe = tmp_path / second_name
|
||||
main_exe.touch()
|
||||
second_exe.touch()
|
||||
result = subprocess.run(
|
||||
["bash", "-c", 'if [[ -n "${PW_RUN_AFTER_EXE:-}" ]]' + condition
|
||||
+ "then exit 0; else exit 1; fi"],
|
||||
["bash", "-c", 'check_run_after() {\n' + condition
|
||||
+ "then return 0; else return 1; fi; }; check_run_after"],
|
||||
env={**os.environ, "PW_EXE_FILE": str(main_exe),
|
||||
"PW_RUN_AFTER_EXE": str(second_exe)},
|
||||
"PW_RUN_AFTER_EXE": ",".join([str(second_exe)] * copies)},
|
||||
capture_output=True, text=True, check=False,
|
||||
)
|
||||
assert result.returncode == 0, result.stderr
|
||||
@@ -112,7 +114,9 @@ def test_run_after_paths_are_passed_in_environment(tmp_path: Path, trainer_path:
|
||||
)[0]
|
||||
main_path = "D:\\Games\\Test Game\\Test Game.exe"
|
||||
result = subprocess.run(
|
||||
["bash", "-c", 'pw_run() { env; }\ncat > "$run_after_bat" <<EOF\n' + block],
|
||||
["bash", "-c", 'pw_run() { env; }\nlaunch() {\n'
|
||||
'local -a run_after_exes=(unused) run_after_environment=()\n'
|
||||
'cat > "$run_after_bat" <<EOF\n' + block + '\n}; launch'],
|
||||
env={**os.environ, "run_after_bat": str(tmp_path / "launch.bat"),
|
||||
"pw_exe_file_win": main_path, "run_after_exe_win": trainer_path,
|
||||
"wait_delay_int": "3", "LAUNCH_PARAMETERS": ""},
|
||||
@@ -120,6 +124,7 @@ def test_run_after_paths_are_passed_in_environment(tmp_path: Path, trainer_path:
|
||||
)
|
||||
assert f"PW_RUN_AFTER_MAIN_PATH={main_path}" in result.stdout
|
||||
assert f"PW_RUN_AFTER_SECOND_PATH={trainer_path}" in result.stdout
|
||||
assert "PW_RUN_AFTER_EXE=\n" in result.stdout
|
||||
batch = (tmp_path / "launch.bat").read_text(encoding="ascii")
|
||||
assert 'start "" "%PW_RUN_AFTER_SECOND_PATH%"' in batch
|
||||
|
||||
@@ -234,139 +239,67 @@ def test_executable_metadata_uses_pefile_and_preserves_lnk_fallback(
|
||||
assert "Company Name: Sony" in metadata
|
||||
|
||||
|
||||
def test_wine_session_missing_returns_distinct_status(tmp_path: Path) -> None:
|
||||
def test_run_after_batch_preserves_duplicate_entries(tmp_path: Path) -> None:
|
||||
helper = Path("build-aux/share/portproton/scripts/functions_helper").read_text()
|
||||
definition = helper.split("pw_session_tool () {", 1)[1].split("pw_launch_wrapper () {", 1)[0]
|
||||
script = "pw_session_tool () {" + definition + '''
|
||||
print_error () { echo "$@" >&2; }
|
||||
sleep () { return 0; }
|
||||
pw_session_tool taskmgr 1
|
||||
'''
|
||||
result = subprocess.run(
|
||||
["bash", "-c", script], cwd=tmp_path, capture_output=True, text=True, check=False,
|
||||
env={"PATH": os.defpath, "PORT_DATA_PATH": str(tmp_path)},
|
||||
)
|
||||
|
||||
assert result.returncode == 2
|
||||
|
||||
|
||||
@mark.parametrize("tool", ["winecfg", "regedit", "winefile", "cmd", "uninstaller", "taskmgr"])
|
||||
def test_wine_session_tool_standalone_uses_selected_wine_and_prefix(tmp_path: Path, tool: str) -> None:
|
||||
start = Path("build-aux/share/portproton/scripts/start.sh").read_text()
|
||||
block = start.split(" --wine-session-tool)\n", 1)[1].split(" --winetricks-list-all)", 1)[0]
|
||||
block = helper.split(' cat > "${run_after_bat}" <<EOF\n', 1)[1].split(
|
||||
' try_remove_file "${run_after_bat}"', 1,
|
||||
)[0]
|
||||
exe = tmp_path / "EVE online.exe"
|
||||
exe.touch()
|
||||
script = '''
|
||||
get_wine_and_pfx () { printf '%s\\n' "$@"; }
|
||||
start_portproton () { mkdir -p "$PORT_DATA_PATH/data/prefixes/$PW_PREFIX_NAME/drive_c"; echo start; }
|
||||
pw_run () { printf '%s\\n' "$@"; }
|
||||
stop_portproton () { echo stop; }
|
||||
''' + 'case "$1" in --wine-session-tool)\n' + block + '\nesac\n'
|
||||
loader() { printf 'Z:%s' "$3"; }
|
||||
pw_run() { env; }
|
||||
launch() {
|
||||
local -a run_after_exes=("$EXE" "$EXE" "$EXE") run_after_environment=()
|
||||
local run_after_index run_after_entry
|
||||
WINELOADER=loader
|
||||
cat > "$run_after_bat" <<EOF
|
||||
''' + block + '\n}; launch'
|
||||
batch_path = tmp_path / "launch.bat"
|
||||
result = subprocess.run(
|
||||
["bash", "-c", script, "bash", "--wine-session-tool", tool, "WINE_LG", "MY_PREFIX"],
|
||||
cwd=tmp_path, capture_output=True, text=True, check=False,
|
||||
env={"PATH": os.defpath, "PORT_DATA_PATH": str(tmp_path), "PW_PREFIX_NAME": "MY_PREFIX"},
|
||||
["bash", "-c", script], capture_output=True, text=True, check=True,
|
||||
env={**os.environ, "EXE": str(exe), "run_after_bat": str(batch_path),
|
||||
"pw_exe_file_win": "Z:\\EVE online.exe",
|
||||
"run_after_exe_win": "Z:\\EVE online.exe", "wait_delay_int": "3"},
|
||||
)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert result.stdout.splitlines() == ["WINE_LG", "MY_PREFIX", "start", tool, "stop"]
|
||||
batch = batch_path.read_text()
|
||||
assert batch.count('start ""') == 4
|
||||
assert '%PW_RUN_AFTER_PATH_1%' in batch
|
||||
assert '%PW_RUN_AFTER_PATH_2%' in batch
|
||||
expected = "Z:" + str(exe).replace("/", "\\")
|
||||
assert f"PW_RUN_AFTER_PATH_1={expected}" in result.stdout
|
||||
assert f"PW_RUN_AFTER_PATH_2={expected}" in result.stdout
|
||||
|
||||
|
||||
@mark.parametrize("tool", ["winefile", "taskmgr", "winecfg", "regedit", "cmd", "uninstaller"])
|
||||
@mark.parametrize("tool_exit_code", [0, 2])
|
||||
def test_wine_session_tools_reuse_prefix_without_game_options(
|
||||
tmp_path: Path, tool: str, tool_exit_code: int,
|
||||
) -> None:
|
||||
def test_run_after_wrapper_runs_script_list_with_duplicates(tmp_path: Path) -> None:
|
||||
helper = Path("build-aux/share/portproton/scripts/functions_helper").read_text()
|
||||
definitions = helper.split("pw_save_session_env () {", 1)[1].split("pw_launch_wrapper () {", 1)[0]
|
||||
script = "pw_save_session_env () {" + definitions + '''
|
||||
print_error () { echo "$@" >&2; }
|
||||
pw_run () {
|
||||
printf '%s\\n' "$1" "$WINEPREFIX" "${PW_USE_TERMINAL:-0}" "${LAUNCH_PARAMETERS:-}" \\
|
||||
"${PW_RUN_AFTER_EXE:-}" "${PW_RUN_GAMESCOPE:-}" "$PW_TMPFS_PATH"
|
||||
return "$TOOL_EXIT_CODE"
|
||||
}
|
||||
PW_TERM="test-terminal"
|
||||
export PW_START_PID="$$" WINEPREFIX="$PORT_DATA_PATH/prefix with spaces"
|
||||
export LAUNCH_PARAMETERS="--game-only" PW_RUN_AFTER_EXE=trainer.exe PW_RUN_GAMESCOPE=gamescope
|
||||
pw_save_session_env || exit 2
|
||||
pw_session_tool "$1"
|
||||
'''
|
||||
(tmp_path / "prefix with spaces/drive_c").mkdir(parents=True)
|
||||
wrapper = helper.split("pw_launch_wrapper () {", 1)[1].split(
|
||||
"export -f pw_launch_wrapper", 1,
|
||||
)[0]
|
||||
first = tmp_path / "first script.sh"
|
||||
second = tmp_path / "second.sh"
|
||||
for script, message in [(first, "first"), (second, "second")]:
|
||||
script.write_text(f"#!/bin/bash\nprintf '{message}\\n'\n")
|
||||
script.chmod(0o700)
|
||||
result = subprocess.run(
|
||||
["bash", "-c", script, "bash", tool], capture_output=True, text=True,
|
||||
env={"PATH": os.defpath, "PORT_DATA_PATH": str(tmp_path),
|
||||
"TOOL_EXIT_CODE": str(tool_exit_code)}, check=False,
|
||||
["bash", "-c", "pw_launch_wrapper () {" + wrapper
|
||||
+ "\npw_launch_wrapper printf 'game\\n'"],
|
||||
env={**os.environ, "PW_RUN_AFTER_EXE": f"{first},{second},{first}"},
|
||||
capture_output=True, text=True, check=True,
|
||||
)
|
||||
|
||||
assert result.returncode == int(tool_exit_code != 0), result.stderr
|
||||
lines = result.stdout.splitlines()
|
||||
assert lines[:6] == [tool, str(tmp_path / "prefix with spaces"), "1" if tool == "cmd" else "0", "", "", ""]
|
||||
assert Path(lines[6]).is_dir()
|
||||
assert Path(lines[6]).stat().st_mode & 0o777 == 0o700
|
||||
session = next((tmp_path / "data/tmp").glob("wine-session-*/session.env"))
|
||||
assert session.stat().st_mode & 0o777 == 0o600
|
||||
assert 'declare -x PW_TERM="test-terminal"' in session.read_text()
|
||||
assert result.stdout.splitlines() == ["first", "second", "first", "game"]
|
||||
|
||||
|
||||
@mark.parametrize("unsafe_directory", [False, True])
|
||||
def test_wine_session_save_does_not_follow_symlinks(tmp_path: Path, unsafe_directory: bool) -> None:
|
||||
def test_run_after_wrapper_preserves_command_with_comma(tmp_path: Path) -> None:
|
||||
helper = Path("build-aux/share/portproton/scripts/functions_helper").read_text()
|
||||
definition = helper.split("pw_save_session_env () {", 1)[1].split("pw_remove_session_env () {", 1)[0]
|
||||
session_dir = tmp_path / "data/tmp" / f"wine-session-{os.getuid()}"
|
||||
session_dir.parent.mkdir(parents=True)
|
||||
victim = tmp_path / "victim"
|
||||
victim.write_text("keep this")
|
||||
if unsafe_directory:
|
||||
target = tmp_path / "other-directory"
|
||||
target.mkdir(mode=0o700)
|
||||
session_dir.symlink_to(target, target_is_directory=True)
|
||||
else:
|
||||
session_dir.mkdir(mode=0o700)
|
||||
(session_dir / "session.env").symlink_to(victim)
|
||||
wrapper = helper.split("pw_launch_wrapper () {", 1)[1].split(
|
||||
"export -f pw_launch_wrapper", 1,
|
||||
)[0]
|
||||
result = subprocess.run(
|
||||
["bash", "-c", "pw_save_session_env () {" + definition + "\npw_save_session_env"],
|
||||
capture_output=True, text=True,
|
||||
env={"PATH": os.defpath, "PORT_DATA_PATH": str(tmp_path)}, check=False,
|
||||
["bash", "-c", "pw_launch_wrapper () {" + wrapper
|
||||
+ "\npw_launch_wrapper printf 'game\\n'"],
|
||||
cwd=tmp_path,
|
||||
env={**os.environ, "PW_RUN_AFTER_EXE": "printf '%s\\n' first,second"},
|
||||
capture_output=True, text=True, check=True,
|
||||
)
|
||||
|
||||
assert victim.read_text() == "keep this"
|
||||
assert result.returncode == (1 if unsafe_directory else 0), result.stderr
|
||||
if not unsafe_directory:
|
||||
assert not (session_dir / "session.env").is_symlink()
|
||||
|
||||
|
||||
def test_wine_session_tool_rejects_unknown_tool_before_reading_environment(tmp_path: Path) -> None:
|
||||
helper = Path("build-aux/share/portproton/scripts/functions_helper").read_text()
|
||||
definition = helper.split("pw_session_tool () {", 1)[1].split("pw_launch_wrapper () {", 1)[0]
|
||||
script = "pw_session_tool () {" + definition + '''
|
||||
print_error () { echo "$@" >&2; }
|
||||
sleep () { exit 99; }
|
||||
pw_session_tool '../../other-command'
|
||||
'''
|
||||
result = subprocess.run(
|
||||
["bash", "-c", script], capture_output=True, text=True,
|
||||
env={"PATH": os.defpath, "PORT_DATA_PATH": str(tmp_path)}, check=False,
|
||||
)
|
||||
|
||||
assert result.returncode == 1
|
||||
assert "Unsupported Wine tool" in result.stderr
|
||||
|
||||
|
||||
def test_wine_session_cleanup_preserves_newer_session(tmp_path: Path) -> None:
|
||||
helper = Path("build-aux/share/portproton/scripts/functions_helper").read_text()
|
||||
definitions = helper.split("pw_save_session_env () {", 1)[1].split("pw_session_tool () {", 1)[0]
|
||||
script = "pw_save_session_env () {" + definitions + '''
|
||||
export PW_START_PID="$$"
|
||||
pw_save_session_env || exit 2
|
||||
PW_START_PID=99999999
|
||||
pw_remove_session_env
|
||||
[[ -f "$PORT_DATA_PATH/data/tmp/wine-session-$(id -u)/session.env" ]] || exit 3
|
||||
PW_START_PID="$$"
|
||||
pw_remove_session_env
|
||||
[[ ! -e "$PORT_DATA_PATH/data/tmp/wine-session-$(id -u)/session.env" ]]
|
||||
'''
|
||||
result = subprocess.run(
|
||||
["bash", "-c", script], capture_output=True, text=True,
|
||||
env={"PATH": os.defpath, "PORT_DATA_PATH": str(tmp_path)}, check=False,
|
||||
)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert result.stdout.splitlines() == ["first,second", "game"]
|
||||
@@ -3,7 +3,7 @@
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from typing import cast
|
||||
from typing import Any, cast
|
||||
|
||||
import pytest
|
||||
from PySide6.QtCore import QEvent, QRect, Qt, QTimer
|
||||
@@ -458,6 +458,22 @@ def test_settings_table_descriptions_and_centered_controls(theme_name: str) -> N
|
||||
table.selectRow(1)
|
||||
ExeSettingsDialog.eventFilter(cast(ExeSettingsDialog, dialog), table.viewport(), QEvent(QEvent.Type.Paint))
|
||||
assert isinstance(table.cellWidget(1, 0), QLabel)
|
||||
label = table.cellWidget(1, 0)
|
||||
ExeSettingsDialog._set_favorite_text_cells(
|
||||
cast(ExeSettingsDialog, dialog), 1, "key", "Updated", "New <description>",
|
||||
)
|
||||
ExeSettingsDialog.eventFilter(cast(ExeSettingsDialog, dialog), table.viewport(), QEvent(QEvent.Type.Paint))
|
||||
assert table.cellWidget(1, 0) is label
|
||||
assert isinstance(label, QLabel)
|
||||
assert label.text() == theme.SETTINGS_SELECTED_TEXT_HTML.format(
|
||||
text="Updated<br>" + theme.SETTINGS_SELECTED_DESCRIPTION_HTML.format(
|
||||
description="New <description>",
|
||||
),
|
||||
)
|
||||
updated_item = table.item(1, 0)
|
||||
assert updated_item is not None
|
||||
assert updated_item.data(Qt.ItemDataRole.AccessibleTextRole) == "Updated\nNew <description>"
|
||||
assert updated_item.text() == ""
|
||||
table.close()
|
||||
|
||||
|
||||
@@ -925,3 +941,93 @@ def test_settings_close_waits_for_pending_save(monkeypatch) -> None:
|
||||
dialog.reject()
|
||||
assert dialog.close_after_save
|
||||
dialog.deleteLater()
|
||||
|
||||
|
||||
def test_run_after_normalization_preserves_duplicates(tmp_path: Path) -> None:
|
||||
from portprotonqt.dialogs.settings_dialog import ExeSettingsDialog
|
||||
|
||||
dialog = SimpleNamespace(exe_path=str(tmp_path / "Game.exe"))
|
||||
dialog._resolve_run_after_exe_path = lambda value: ExeSettingsDialog._resolve_run_after_exe_path(cast(ExeSettingsDialog, dialog), value)
|
||||
value = "EVE.exe,EVE.exe,Trainer.exe"
|
||||
assert dialog._resolve_run_after_exe_path(value) == ",".join(
|
||||
str(tmp_path / name) for name in value.split(",")
|
||||
)
|
||||
|
||||
|
||||
def test_run_after_inline_entries_preserve_duplicates(tmp_path: Path) -> None:
|
||||
from portprotonqt.dialogs.settings_dialog import ExeSettingsDialog
|
||||
|
||||
owner = cast(Any, QWidget())
|
||||
owner.advanced_table = QTableWidget(2, 3)
|
||||
owner.theme = SimpleNamespace(ACTION_BUTTON_STYLE="", LINE_EDIT_STYLE="")
|
||||
container = QWidget()
|
||||
target = QLineEdit(container)
|
||||
owner.advanced_table.setCellWidget(0, 1, container)
|
||||
from PySide6.QtWidgets import QTableWidgetItem
|
||||
|
||||
owner.advanced_table.setItem(1, 0, QTableWidgetItem("next setting"))
|
||||
exe = tmp_path / "Game.exe"
|
||||
exe.touch()
|
||||
target.setText(f"{exe},{exe}")
|
||||
ExeSettingsDialog._toggle_run_after_entries(cast(ExeSettingsDialog, owner), target)
|
||||
assert owner.advanced_table.rowCount() == 4
|
||||
item = owner.advanced_table.item(1, 0)
|
||||
assert item is not None and item.text() == str(exe)
|
||||
item = owner.advanced_table.item(2, 0)
|
||||
assert item is not None and item.text() == str(exe)
|
||||
ExeSettingsDialog._toggle_run_after_entries(cast(ExeSettingsDialog, owner), target)
|
||||
assert owner.advanced_table.rowCount() == 2
|
||||
item = owner.advanced_table.item(1, 0)
|
||||
assert item is not None and item.text() == "next setting"
|
||||
owner.advanced_table.deleteLater()
|
||||
|
||||
|
||||
def test_run_after_entry_paint_preserves_text_and_height() -> None:
|
||||
from PySide6.QtWidgets import QTableWidgetItem
|
||||
from portprotonqt.dialogs.settings_dialog import ExeSettingsDialog
|
||||
|
||||
table = QTableWidget(1, 2)
|
||||
table.viewport().setProperty("ppqt_settings_values", True)
|
||||
table.setItem(0, 0, QTableWidgetItem("Game.exe"))
|
||||
entry = QLineEdit("Game.exe")
|
||||
table.setCellWidget(0, 0, entry)
|
||||
table.resizeRowsToContents()
|
||||
table.show()
|
||||
QApplication.processEvents()
|
||||
height = table.rowHeight(0)
|
||||
|
||||
for _ in range(2):
|
||||
ExeSettingsDialog.eventFilter(
|
||||
cast(ExeSettingsDialog, SimpleNamespace()),
|
||||
table.viewport(), QEvent(QEvent.Type.Paint),
|
||||
)
|
||||
|
||||
assert entry.text() == "Game.exe"
|
||||
item = table.item(0, 0)
|
||||
assert item is not None and item.text() == "Game.exe"
|
||||
assert table.rowHeight(0) == height
|
||||
assert entry.height() >= entry.sizeHint().height()
|
||||
table.close()
|
||||
table.deleteLater()
|
||||
|
||||
|
||||
def test_run_after_removing_only_entry_clears_field(tmp_path: Path) -> None:
|
||||
from PySide6.QtWidgets import QTableWidgetItem
|
||||
from portprotonqt.dialogs.settings_dialog import ExeSettingsDialog
|
||||
|
||||
owner = cast(Any, QWidget())
|
||||
owner.advanced_table = QTableWidget(2, 3)
|
||||
owner.theme = SimpleNamespace(ACTION_BUTTON_STYLE="", LINE_EDIT_STYLE="")
|
||||
owner._toggle_run_after_entries = lambda target: ExeSettingsDialog._toggle_run_after_entries(cast(ExeSettingsDialog, owner), target)
|
||||
container = QWidget()
|
||||
target = QLineEdit(container)
|
||||
owner.advanced_table.setCellWidget(0, 1, container)
|
||||
owner.advanced_table.setItem(1, 0, QTableWidgetItem("next setting"))
|
||||
exe = tmp_path / "Game.exe"
|
||||
exe.touch()
|
||||
target.setText(str(exe))
|
||||
owner._toggle_run_after_entries(target)
|
||||
ExeSettingsDialog._remove_run_after_entry(cast(ExeSettingsDialog, owner), target, 0)
|
||||
assert target.text() == ""
|
||||
assert owner.advanced_table.rowCount() == 2
|
||||
owner.advanced_table.deleteLater()
|
||||
Reference in new issue
Block a user