Compare commits
7
Commits
reshade
...
goggl-fork
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4b80522d1
|
||
|
|
eb3d507f9d
|
||
|
|
d501dfcecf
|
||
|
|
e21bb09081
|
||
|
|
fa8e8478e5
|
||
|
|
51b78b506c
|
||
|
|
1776dd9e94
|
No files matched your search
@@ -9,9 +9,14 @@
|
||||
|
||||
### Changed
|
||||
|
||||
- Загрузка игр GOG переведена на форк `linux-gaming-ru/gogdl` с выбором CDN по скорости, исправленной статистикой загрузки и ограничением повторных запросов; ранее установленный gogdl автоматически заменяется форком
|
||||
### Fixed
|
||||
|
||||
- Отключена проверка обновлений AppImage при запуске с `--silent`, чтобы не показывать диалог обновления
|
||||
- Исправлена рекурсивная ошибка фильтра событий клавиатуры при закрытии приложения
|
||||
- Исправлена высокая нагрузка на CPU из-за непрерывной перерисовки таблиц в настройках exe
|
||||
- В меню системного трея при запуске игры с `--silent` добавлены отсутствовавшие действия приостановки и возобновления игры
|
||||
- Исправлена гонка при тихом запуске игр
|
||||
|
||||
### Contributors
|
||||
|
||||
|
||||
@@ -16,6 +16,15 @@ 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"
|
||||
|
||||
@@ -38,7 +47,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 \
|
||||
@@ -98,4 +107,4 @@ done
|
||||
rm -rf "$PYSIDE_DIR"/QtAsyncio
|
||||
|
||||
# Turn AppDir into AppImage
|
||||
quick-sharun --make-appimage
|
||||
./quick-sharun --make-appimage
|
||||
+11
-9
@@ -807,13 +807,14 @@ def main():
|
||||
return
|
||||
QMessageBox.warning(window, _("Error"), _("Failed to update AppImage."))
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
class GOGDLUpdateWorker(QThread):
|
||||
"""Update gogdl without blocking the UI thread."""
|
||||
@@ -900,14 +901,15 @@ def main():
|
||||
finally:
|
||||
self.finished.emit()
|
||||
|
||||
if start_sh:
|
||||
# Initial setup can stop Wine and must not race a silent game launch.
|
||||
if start_sh and not silent_game_request:
|
||||
worker = InitialCommandWorker(start_sh)
|
||||
window.initialCommandWorker = worker
|
||||
worker.finished.connect(
|
||||
lambda: setattr(window, "initialCommandWorker", None)
|
||||
)
|
||||
worker.start()
|
||||
else:
|
||||
elif not start_sh:
|
||||
logger.warning("PortProton start command not available, skipping initial command")
|
||||
|
||||
# --- Cleanup ---
|
||||
|
||||
@@ -1646,7 +1646,8 @@ class ExeSettingsDialog(
|
||||
palette.setBrush(QPalette.ColorRole.Text, item.foreground())
|
||||
label.setAutoFillBackground(selected)
|
||||
label.setPalette(palette)
|
||||
label.setForegroundRole(QPalette.ColorRole.Text)
|
||||
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)
|
||||
@@ -1663,7 +1664,11 @@ class ExeSettingsDialog(
|
||||
if label.text() != formatted_text:
|
||||
label.setText(formatted_text)
|
||||
width = max(1, table.columnWidth(0))
|
||||
table.setRowHeight(row, label.heightForWidth(width) + table.fontMetrics().height())
|
||||
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)
|
||||
for row in range(table.rowCount()):
|
||||
widget = table.cellWidget(row, 1)
|
||||
if widget is None:
|
||||
|
||||
+14
-10
@@ -4,7 +4,6 @@ import hashlib
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
import zlib
|
||||
@@ -25,7 +24,8 @@ GOG_LOGIN_URL = (
|
||||
"redirect_uri=https%3A%2F%2Fembed.gog.com%2Fon_login_success%3Forigin%3Dclient&"
|
||||
"response_type=code&layout=galaxy"
|
||||
)
|
||||
GOGDL_RELEASE_URL = "https://api.github.com/repos/Heroic-Games-Launcher/heroic-gogdl/releases/latest"
|
||||
GOGDL_RELEASE_SOURCE = "linux-gaming-ru/gogdl"
|
||||
GOGDL_RELEASE_URL = f"https://api.github.com/repos/{GOGDL_RELEASE_SOURCE}/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 an available gogdl executable."""
|
||||
"""Return the managed gogdl fork executable when installed."""
|
||||
bundled = self.bin_dir / "gogdl"
|
||||
if bundled.is_file() and os.access(bundled, os.X_OK):
|
||||
if bundled.is_file() and os.access(bundled, os.X_OK) and self._get_gogdl_release_tag():
|
||||
return str(bundled)
|
||||
return shutil.which("gogdl")
|
||||
return None
|
||||
|
||||
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 official Heroic gogdl binary."""
|
||||
"""Download and verify the latest linux-gaming-ru 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:
|
||||
"""Update the bundled gogdl binary to the latest official release."""
|
||||
"""Replace other gogdl binaries and update the managed fork."""
|
||||
bundled = self.bin_dir / "gogdl"
|
||||
if bundled.is_file() and os.access(bundled, os.X_OK):
|
||||
if self.get_gogdl_path():
|
||||
try:
|
||||
last_check = self.gogdl_version_path.stat().st_mtime
|
||||
except OSError:
|
||||
@@ -143,13 +143,17 @@ class GOGAPI:
|
||||
|
||||
def _get_gogdl_release_tag(self) -> str:
|
||||
try:
|
||||
return self.gogdl_version_path.read_text(encoding="utf-8").strip()
|
||||
version = 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(release_tag, encoding="utf-8")
|
||||
self.gogdl_version_path.write_text(
|
||||
f"{GOGDL_RELEASE_SOURCE}:{release_tag}", encoding="utf-8"
|
||||
)
|
||||
return path
|
||||
|
||||
def _download_gogdl_asset(self, asset: dict) -> str:
|
||||
|
||||
@@ -93,10 +93,6 @@ 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")
|
||||
@@ -162,7 +158,7 @@ class KeyboardInputMixin(InputMixin):
|
||||
logger.debug(f"Skipping event filter for non-QObject: {type(obj).__name__}")
|
||||
return False
|
||||
|
||||
return QObject.eventFilter(cast(QObject, self), obj, event)
|
||||
return False
|
||||
|
||||
def _qt_event_to_input_key(self, event: QEvent) -> int | None:
|
||||
native_key = self._native_scan_to_input_key(event)
|
||||
|
||||
@@ -152,6 +152,38 @@ 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"),
|
||||
|
||||
+76
-6
@@ -1,5 +1,6 @@
|
||||
import os
|
||||
import zlib
|
||||
from contextlib import nullcontext
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock
|
||||
@@ -7,7 +8,7 @@ from unittest.mock import Mock
|
||||
import orjson
|
||||
import pytest
|
||||
|
||||
from portprotonqt.gog_api import GOGAPI, GOGDL_UPDATE_INTERVAL, GOG_PRODUCT_LOCALES
|
||||
from portprotonqt.gog_api import GOGAPI, GOGDL_RELEASE_SOURCE, GOGDL_UPDATE_INTERVAL, GOG_PRODUCT_LOCALES
|
||||
from portprotonqt.localization import LOCALE_MAP
|
||||
|
||||
|
||||
@@ -392,13 +393,82 @@ 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")
|
||||
monkeypatch.setattr("portprotonqt.gog_api.requests.get", lambda *args, **kwargs: response)
|
||||
request = Mock(return_value=response)
|
||||
monkeypatch.setattr("portprotonqt.gog_api.requests.get", request)
|
||||
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:
|
||||
@@ -410,7 +480,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("v1.2.1", encoding="utf-8")
|
||||
api.gogdl_version_path.write_text(f"{GOGDL_RELEASE_SOURCE}: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()
|
||||
@@ -430,7 +500,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("v1.2.1", encoding="utf-8")
|
||||
api.gogdl_version_path.write_text(f"{GOGDL_RELEASE_SOURCE}: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(
|
||||
@@ -453,7 +523,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("v1.2.1", encoding="utf-8")
|
||||
api.gogdl_version_path.write_text(f"{GOGDL_RELEASE_SOURCE}: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(
|
||||
@@ -492,7 +562,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") == "v1.2.1"
|
||||
assert api.gogdl_version_path.read_text(encoding="utf-8") == f"{GOGDL_RELEASE_SOURCE}:v1.2.1"
|
||||
|
||||
|
||||
def test_authenticate_returns_gogdl_error(monkeypatch) -> None:
|
||||
|
||||
@@ -6,8 +6,9 @@ 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, QWidget
|
||||
from PySide6.QtWidgets import QApplication, QCheckBox, QComboBox, QDialog, QFrame, QHBoxLayout, QLineEdit, QListView, QMenu, QPushButton, QSlider, QStackedWidget, QTableWidget, QTableWidgetItem, QVBoxLayout, 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
|
||||
@@ -35,6 +36,34 @@ 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,8 +1,6 @@
|
||||
"""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
|
||||
@@ -399,7 +397,7 @@ def real_cover_scene(
|
||||
|
||||
|
||||
@pytest.mark.parametrize("animation_type", ["gradient", "glow"])
|
||||
def test_real_cover_loading_and_first_paint_focus_latency(
|
||||
def test_real_cover_loading_yields_and_focuses_first_paint(
|
||||
real_cover_scene: tuple[GameLibraryManager, Path],
|
||||
monkeypatch: pytest.MonkeyPatch, animation_type: str,
|
||||
) -> None:
|
||||
@@ -407,14 +405,14 @@ def test_real_cover_loading_and_first_paint_focus_latency(
|
||||
monkeypatch.setattr(manager.theme, "GAME_CARD_ANIMATION",
|
||||
{**manager.theme.GAME_CARD_ANIMATION, "card_animation_type": animation_type})
|
||||
threads: list[bool] = []
|
||||
paints: list[tuple[float, bool]] = []
|
||||
ticks: list[float] = []
|
||||
paints: list[bool] = []
|
||||
ticks: list[tuple[int, int]] = []
|
||||
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((perf_counter(), card._focused or card._hovered))
|
||||
paints.append(card._focused or card._hovered)
|
||||
original_paint(card, event)
|
||||
|
||||
def record_cover(card: GameCard, pixmap: QPixmap) -> None:
|
||||
@@ -422,7 +420,7 @@ def test_real_cover_loading_and_first_paint_focus_latency(
|
||||
original_cover(card, pixmap)
|
||||
|
||||
def sample() -> None:
|
||||
ticks.append(perf_counter())
|
||||
ticks.append((len(manager.game_card_cache), len(threads)))
|
||||
if len(threads) == len(games) and not manager._incremental_add_queue:
|
||||
loop.quit()
|
||||
|
||||
@@ -434,18 +432,14 @@ def test_real_cover_loading_and_first_paint_focus_latency(
|
||||
timer.setInterval(5)
|
||||
timer.timeout.connect(sample)
|
||||
timer.start()
|
||||
QTimer.singleShot(2000, loop.quit)
|
||||
QTimer.singleShot(5000, loop.quit)
|
||||
manager.set_games(games)
|
||||
loop.exec()
|
||||
timer.stop()
|
||||
assert len(threads) == len(games) and all(threads), threads
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
@pytest.mark.parametrize("scenario", [("grid", 0), ("grid", 1), ("vertical", 0), ("vertical", 1)])
|
||||
|
||||
@@ -631,3 +631,48 @@ 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