forked from Boria138/PortProtonQt
Compare commits
18 Commits
3736bb279e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
e07f3f06bc
|
|||
|
16a3f4e09a
|
|||
|
a448ba29b0
|
|||
|
06e55db54d
|
|||
|
5fce23f261
|
|||
|
|
96ad40d625 | ||
|
|
a30f6f2e74 | ||
|
0231073b19
|
|||
|
dec24429f5
|
|||
|
4a758f3b3c
|
|||
|
0853dd1579
|
|||
|
bbb87c0455
|
|||
|
b32a71a125
|
|||
|
|
bddf9f850a | ||
|
|
a9c3cfa167 | ||
|
7675bc4cdc
|
|||
|
ffa203f019
|
|||
|
3eed25ecee
|
@@ -62,7 +62,7 @@ jobs:
|
|||||||
- name: Install build dependencies
|
- name: Install build dependencies
|
||||||
run: |
|
run: |
|
||||||
dnf install -y git rpmdevtools python3-devel python3-wheel python3-pip \
|
dnf install -y git rpmdevtools python3-devel python3-wheel python3-pip \
|
||||||
python3-build pyproject-rpm-macros python3-setuptools \
|
python3-build pyproject-rpm-macros systemd-rpm-macros python3-setuptools \
|
||||||
redhat-rpm-config nodejs npm
|
redhat-rpm-config nodejs npm
|
||||||
|
|
||||||
- name: Setup rpmbuild environment
|
- name: Setup rpmbuild environment
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ jobs:
|
|||||||
- name: Install build dependencies
|
- name: Install build dependencies
|
||||||
run: |
|
run: |
|
||||||
dnf install -y git rpmdevtools python3-devel python3-wheel python3-pip \
|
dnf install -y git rpmdevtools python3-devel python3-wheel python3-pip \
|
||||||
python3-build pyproject-rpm-macros python3-setuptools \
|
python3-build pyproject-rpm-macros systemd-rpm-macros python3-setuptools \
|
||||||
redhat-rpm-config nodejs npm
|
redhat-rpm-config nodejs npm
|
||||||
|
|
||||||
- name: Setup rpmbuild environment
|
- name: Setup rpmbuild environment
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ repos:
|
|||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
|
|
||||||
- repo: https://github.com/astral-sh/uv-pre-commit
|
- repo: https://github.com/astral-sh/uv-pre-commit
|
||||||
rev: 0.8.22
|
rev: 0.9.5
|
||||||
hooks:
|
hooks:
|
||||||
- id: uv-lock
|
- id: uv-lock
|
||||||
|
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.14.1
|
rev: v0.14.3
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff-check
|
- id: ruff-check
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,12 @@ script:
|
|||||||
- uv pip install --no-cache-dir ../
|
- uv pip install --no-cache-dir ../
|
||||||
- cp -r .venv/lib/python3.10/site-packages/* AppDir/usr/local/lib/python3.10/dist-packages
|
- cp -r .venv/lib/python3.10/site-packages/* AppDir/usr/local/lib/python3.10/dist-packages
|
||||||
- cp -r share AppDir/usr
|
- cp -r share AppDir/usr
|
||||||
|
- cp -r lib AppDir/usr
|
||||||
- rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/qml/
|
- rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/qml/
|
||||||
- rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{assistant,designer,linguist,lrelease,lupdate}
|
- rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{assistant,designer,linguist,lrelease,lupdate}
|
||||||
- rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{Qt3DAnimation*,Qt3DCore*,Qt3DExtras*,Qt3DInput*,Qt3DLogic*,Qt3DRender*,QtBluetooth*,QtCharts*,QtConcurrent*,QtDataVisualization*,QtDesigner*,QtExampleIcons*,QtGraphs*,QtGraphsWidgets*,QtHelp*,QtHttpServer*,QtLocation*,QtMultimedia*,QtMultimediaWidgets*,QtNetwork*,QtNetworkAuth*,QtNfc*,QtOpenGL*,QtOpenGLWidgets*,QtPdf*,QtPdfWidgets*,QtPositioning*,QtPrintSupport*,QtQml*,QtQuick*,QtQuick3D*,QtQuickControls2*,QtQuickTest*,QtQuickWidgets*,QtRemoteObjects*,QtScxml*,QtSensors*,QtSerialBus*,QtSerialPort*,QtSpatialAudio*,QtSql*,QtStateMachine*,QtSvgWidgets*,QtTest*,QtTextToSpeech*,QtUiTools*,QtWebChannel*,QtWebEngineCore*,QtWebEngineQuick*,QtWebEngineWidgets*,QtWebSockets*,QtWebView*,QtXml*}
|
- rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{Qt3DAnimation*,Qt3DCore*,Qt3DExtras*,Qt3DInput*,Qt3DLogic*,Qt3DRender*,QtBluetooth*,QtCharts*,QtConcurrent*,QtDataVisualization*,QtDesigner*,QtExampleIcons*,QtGraphs*,QtGraphsWidgets*,QtHelp*,QtHttpServer*,QtLocation*,QtMultimedia*,QtMultimediaWidgets*,QtNetworkAuth*,QtNfc*,QtOpenGL*,QtOpenGLWidgets*,QtPdf*,QtPdfWidgets*,QtPositioning*,QtPrintSupport*,QtQml*,QtQuick*,QtQuick3D*,QtQuickControls2*,QtQuickTest*,QtQuickWidgets*,QtRemoteObjects*,QtScxml*,QtSensors*,QtSerialBus*,QtSerialPort*,QtSpatialAudio*,QtSql*,QtStateMachine*,QtSvgWidgets*,QtTest*,QtTextToSpeech*,QtUiTools*,QtWebChannel*,QtWebEngineCore*,QtWebEngineQuick*,QtWebEngineWidgets*,QtWebSockets*,QtWebView*,QtXml*}
|
||||||
- shopt -s extglob
|
- shopt -s extglob
|
||||||
- rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/lib/!(libQt6Core*|libQt6DBus*|libQt6Egl*|libQt6Gui*|libQt6Svg*|libQt6Wayland*|libQt6Widgets*|libQt6XcbQpa*|libicudata*|libicui18n*|libicuuc*)
|
- rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/lib/!(libQt6Core*|libQt6DBus*|libQt6Egl*|libQt6Gui*|libQt6Network*|libQt6Svg*|libQt6Wayland*|libQt6Widgets*|libQt6XcbQpa*|libicudata*|libicui18n*|libicuuc*)
|
||||||
AppDir:
|
AppDir:
|
||||||
path: ./AppDir
|
path: ./AppDir
|
||||||
after_bundle:
|
after_bundle:
|
||||||
|
|||||||
@@ -20,4 +20,5 @@ package() {
|
|||||||
cd "$srcdir/PortProtonQt"
|
cd "$srcdir/PortProtonQt"
|
||||||
python -m installer --destdir="$pkgdir" dist/*.whl
|
python -m installer --destdir="$pkgdir" dist/*.whl
|
||||||
cp -r build-aux/share "$pkgdir/usr/"
|
cp -r build-aux/share "$pkgdir/usr/"
|
||||||
|
cp -r build-aux/lib "$pkgdir/usr/"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,4 +25,5 @@ package() {
|
|||||||
cd "$srcdir/PortProtonQt"
|
cd "$srcdir/PortProtonQt"
|
||||||
python -m installer --destdir="$pkgdir" dist/*.whl
|
python -m installer --destdir="$pkgdir" dist/*.whl
|
||||||
cp -r build-aux/share "$pkgdir/usr/"
|
cp -r build-aux/share "$pkgdir/usr/"
|
||||||
|
cp -r build-aux/lib "$pkgdir/usr/"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ BuildRequires: python3-build
|
|||||||
BuildRequires: pyproject-rpm-macros
|
BuildRequires: pyproject-rpm-macros
|
||||||
BuildRequires: python3dist(setuptools)
|
BuildRequires: python3dist(setuptools)
|
||||||
BuildRequires: git
|
BuildRequires: git
|
||||||
|
BuildRequires: systemd-rpm-macros
|
||||||
|
|
||||||
%description
|
%description
|
||||||
%{summary}
|
%{summary}
|
||||||
@@ -69,11 +70,13 @@ cd %{oname}
|
|||||||
%pyproject_install
|
%pyproject_install
|
||||||
%pyproject_save_files %{pypi_name}
|
%pyproject_save_files %{pypi_name}
|
||||||
cp -r build-aux/share %{buildroot}/usr/
|
cp -r build-aux/share %{buildroot}/usr/
|
||||||
|
cp -r build-aux/lib %{buildroot}/usr/
|
||||||
|
|
||||||
%files -n python3-%{pypi_name}-git -f %{pyproject_files}
|
%files -n python3-%{pypi_name}-git -f %{pyproject_files}
|
||||||
%{_bindir}/%{pypi_name}
|
%{_bindir}/%{pypi_name}
|
||||||
%{_datadir}/icons/hicolor/scalable/apps/ru.linux_gaming.PortProtonQt.svg
|
%{_datadir}/icons/hicolor/scalable/apps/ru.linux_gaming.PortProtonQt.svg
|
||||||
%{_metainfodir}/ru.linux_gaming.PortProtonQt.metainfo.xml
|
%{_metainfodir}/ru.linux_gaming.PortProtonQt.metainfo.xml
|
||||||
|
%{_udevrulesdir}/60-portprotonqt.rules
|
||||||
%{_datadir}/applications/ru.linux_gaming.PortProtonQt.desktop
|
%{_datadir}/applications/ru.linux_gaming.PortProtonQt.desktop
|
||||||
%{bash_completions_dir}/portprotonqt
|
%{bash_completions_dir}/portprotonqt
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ BuildRequires: python3-build
|
|||||||
BuildRequires: pyproject-rpm-macros
|
BuildRequires: pyproject-rpm-macros
|
||||||
BuildRequires: python3dist(setuptools)
|
BuildRequires: python3dist(setuptools)
|
||||||
BuildRequires: git
|
BuildRequires: git
|
||||||
|
BuildRequires: systemd-rpm-macros
|
||||||
|
|
||||||
%description
|
%description
|
||||||
%{summary}
|
%{summary}
|
||||||
@@ -68,11 +69,13 @@ cd %{oname}
|
|||||||
%pyproject_install
|
%pyproject_install
|
||||||
%pyproject_save_files %{pypi_name}
|
%pyproject_save_files %{pypi_name}
|
||||||
cp -r build-aux/share %{buildroot}/usr/
|
cp -r build-aux/share %{buildroot}/usr/
|
||||||
|
cp -r build-aux/lib %{buildroot}/usr/
|
||||||
|
|
||||||
%files -n python3-%{pypi_name} -f %{pyproject_files}
|
%files -n python3-%{pypi_name} -f %{pyproject_files}
|
||||||
%{_bindir}/%{pypi_name}
|
%{_bindir}/%{pypi_name}
|
||||||
%{_datadir}/icons/hicolor/scalable/apps/ru.linux_gaming.PortProtonQt.svg
|
%{_datadir}/icons/hicolor/scalable/apps/ru.linux_gaming.PortProtonQt.svg
|
||||||
%{_metainfodir}/ru.linux_gaming.PortProtonQt.metainfo.xml
|
%{_metainfodir}/ru.linux_gaming.PortProtonQt.metainfo.xml
|
||||||
|
%{_udevrulesdir}/60-portprotonqt.rules
|
||||||
%{_datadir}/applications/ru.linux_gaming.PortProtonQt.desktop
|
%{_datadir}/applications/ru.linux_gaming.PortProtonQt.desktop
|
||||||
%{bash_completions_dir}/portprotonqt
|
%{bash_completions_dir}/portprotonqt
|
||||||
|
|
||||||
|
|||||||
1
build-aux/lib/udev/rules.d/60-portprotonqt.rules
Normal file
1
build-aux/lib/udev/rules.d/60-portprotonqt.rules
Normal file
@@ -0,0 +1 @@
|
|||||||
|
KERNEL=="uinput", SUBSYSTEM=="misc", OPTIONS+="static_node=uinput", TAG+="uaccess"
|
||||||
@@ -1021,7 +1021,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"normalized_name": "farlight 84",
|
"normalized_name": "farlight 84",
|
||||||
"status": "Supported"
|
"status": "Denied"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"normalized_name": "riders republic",
|
"normalized_name": "riders republic",
|
||||||
@@ -1436,8 +1436,8 @@
|
|||||||
"status": "Broken"
|
"status": "Broken"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"normalized_name": "blue protocol",
|
"normalized_name": "blue protocol star resonance",
|
||||||
"status": "Broken"
|
"status": "Running"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"normalized_name": "dark and darker",
|
"normalized_name": "dark and darker",
|
||||||
|
|||||||
Binary file not shown.
12956
data/games_appid.json
12956
data/games_appid.json
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -1,4 +1,108 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"normalized_title": "split/second",
|
||||||
|
"slug": "split-second"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "warzone 2100",
|
||||||
|
"slug": "warzone-2100"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "foundation",
|
||||||
|
"slug": "foundation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "земский собор [демо]",
|
||||||
|
"slug": "zemskij-sobor-demo"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "crusader kings 3",
|
||||||
|
"slug": "crusader-kings-3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "nadir a grimdark deck builder",
|
||||||
|
"slug": "nadir-a-grimdark-deck-builder"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "oriental empires",
|
||||||
|
"slug": "oriental-empires"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "vampire the masquerade bloodlines 2",
|
||||||
|
"slug": "vampire-the-masquerade-bloodlines-2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "escape from duckov",
|
||||||
|
"slug": "escape-from-duckov"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "xiii",
|
||||||
|
"slug": "xiii"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "saints row 2",
|
||||||
|
"slug": "saints-row-2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "frozenheim",
|
||||||
|
"slug": "frozenheim"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "saints row (2022)",
|
||||||
|
"slug": "saints-row-2022"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "iron harvest",
|
||||||
|
"slug": "iron-harvest"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "tom clancy's splinter cell blacklist",
|
||||||
|
"slug": "tom-clancys-splinter-cell-blacklist"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "painkiller overdose",
|
||||||
|
"slug": "painkiller-overdose"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "ancestors legacy",
|
||||||
|
"slug": "ancestors-legacy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "bye sweet carole",
|
||||||
|
"slug": "bye-sweet-carole"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "painkiller black",
|
||||||
|
"slug": "painkiller-black-edition"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "hogwarts legacy",
|
||||||
|
"slug": "hogwarts-legacy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "active matter",
|
||||||
|
"slug": "active-matter"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "tom clancy's splinter cell",
|
||||||
|
"slug": "tom-clancys-splinter-cell"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "sniper ghost warrior",
|
||||||
|
"slug": "sniper-ghost-warrior"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "fate undiscovered realms",
|
||||||
|
"slug": "fate-undiscovered-realms"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "dying light the beast deluxe",
|
||||||
|
"slug": "dying-light-the-beast-deluxe-edition"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "spellforce platinum",
|
||||||
|
"slug": "spellforce-platinum-edition"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"normalized_title": "dirt rally 2.0 game of the year",
|
"normalized_title": "dirt rally 2.0 game of the year",
|
||||||
"slug": "dirt-rally-2-0-game-of-the-year-edition"
|
"slug": "dirt-rally-2-0-game-of-the-year-edition"
|
||||||
@@ -271,10 +375,6 @@
|
|||||||
"normalized_title": "steins;gate the distant valhalla",
|
"normalized_title": "steins;gate the distant valhalla",
|
||||||
"slug": "steins-gate-the-distant-valhalla"
|
"slug": "steins-gate-the-distant-valhalla"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"normalized_title": "hogwarts legacy",
|
|
||||||
"slug": "hogwarts-legacy"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"normalized_title": "osu!",
|
"normalized_title": "osu!",
|
||||||
"slug": "osu"
|
"slug": "osu"
|
||||||
|
|||||||
Binary file not shown.
@@ -17,17 +17,31 @@ import json
|
|||||||
|
|
||||||
|
|
||||||
class PySide6DependencyAnalyzer:
|
class PySide6DependencyAnalyzer:
|
||||||
def __init__(self):
|
def __init__(self, project_root: Path = None):
|
||||||
# Системные библиотеки, которые нужно всегда оставлять
|
# Системные библиотеки, которые нужно всегда оставлять
|
||||||
self.system_libs = {
|
self.system_libs = {
|
||||||
'libQt6XcbQpa', 'libQt6Wayland', 'libQt6Egl',
|
'libQt6XcbQpa', 'libQt6Wayland', 'libQt6Egl',
|
||||||
'libicudata', 'libicuuc', 'libicui18n', 'libQt6DBus'
|
'libicudata', 'libicuuc', 'libicui18n', 'libQt6DBus',
|
||||||
|
'libQt6Svg'
|
||||||
|
}
|
||||||
|
|
||||||
|
self.critical_modules = {
|
||||||
|
'QtSvg',
|
||||||
}
|
}
|
||||||
|
|
||||||
self.real_dependencies = {}
|
self.real_dependencies = {}
|
||||||
self.used_modules_code = set()
|
self.used_modules_code = set()
|
||||||
self.used_modules_ldd = set()
|
self.used_modules_ldd = set()
|
||||||
self.all_required_modules = set()
|
self.all_required_modules = set()
|
||||||
|
# Определяем корень проекта
|
||||||
|
if project_root is None:
|
||||||
|
# Корень проекта - две директории выше от скрипта
|
||||||
|
self.project_root = Path(__file__).parent.parent
|
||||||
|
else:
|
||||||
|
self.project_root = project_root
|
||||||
|
|
||||||
|
self.venv_path = self.project_root / ".venv"
|
||||||
|
self.build_path = self.project_root / "build-aux"
|
||||||
|
|
||||||
def find_python_files(self, directory: Path) -> List[Path]:
|
def find_python_files(self, directory: Path) -> List[Path]:
|
||||||
"""Находит все Python файлы в директории"""
|
"""Находит все Python файлы в директории"""
|
||||||
@@ -44,19 +58,56 @@ class PySide6DependencyAnalyzer:
|
|||||||
"""Находит все PySide6 библиотеки (.so файлы)"""
|
"""Находит все PySide6 библиотеки (.so файлы)"""
|
||||||
libs = {}
|
libs = {}
|
||||||
|
|
||||||
# Поиск в единственной локации
|
# Ищем venv в корне проекта
|
||||||
search_path = Path("../.venv/lib/python3.10/site-packages/PySide6")
|
venv_candidates = [
|
||||||
print(f"Поиск PySide6 библиотек в: {search_path}")
|
self.venv_path, # .venv
|
||||||
|
self.project_root / "venv",
|
||||||
|
self.project_root / ".virtualenv",
|
||||||
|
]
|
||||||
|
|
||||||
|
pyside6_path = None
|
||||||
|
|
||||||
|
# Пробуем найти PySide6 в venv
|
||||||
|
for venv in venv_candidates:
|
||||||
|
if venv.exists():
|
||||||
|
# Ищем Python версию
|
||||||
|
lib_path = venv / "lib"
|
||||||
|
if lib_path.exists():
|
||||||
|
for python_dir in lib_path.iterdir():
|
||||||
|
if python_dir.name.startswith('python'):
|
||||||
|
candidate = python_dir / "site-packages" / "PySide6"
|
||||||
|
if candidate.exists():
|
||||||
|
pyside6_path = candidate
|
||||||
|
print(f"Найден PySide6 в: {candidate}")
|
||||||
|
break
|
||||||
|
if pyside6_path:
|
||||||
|
break
|
||||||
|
|
||||||
|
if not pyside6_path:
|
||||||
|
print(f"Предупреждение: PySide6 не найден в venv, проверяем AppDir...")
|
||||||
|
# Если не нашли в venv, пробуем в AppDir
|
||||||
|
if base_path:
|
||||||
|
appdir_candidate = base_path / "AppDir/usr/local/lib"
|
||||||
|
if appdir_candidate.exists():
|
||||||
|
for python_dir in appdir_candidate.iterdir():
|
||||||
|
if python_dir.name.startswith('python'):
|
||||||
|
candidate = python_dir / "dist-packages" / "PySide6"
|
||||||
|
if candidate.exists():
|
||||||
|
pyside6_path = candidate
|
||||||
|
print(f"Найден PySide6 в AppDir: {candidate}")
|
||||||
|
break
|
||||||
|
|
||||||
|
if not pyside6_path:
|
||||||
|
return libs
|
||||||
|
|
||||||
if search_path.exists():
|
|
||||||
# Ищем .so файлы модулей
|
# Ищем .so файлы модулей
|
||||||
for so_file in search_path.glob("Qt*.*.so"):
|
for so_file in pyside6_path.glob("Qt*.*.so"):
|
||||||
module_name = so_file.stem.split('.')[0] # QtCore.abi3.so -> QtCore
|
module_name = so_file.stem.split('.')[0] # QtCore.abi3.so -> QtCore
|
||||||
if module_name.startswith('Qt'):
|
if module_name.startswith('Qt'):
|
||||||
libs[module_name] = so_file
|
libs[module_name] = so_file
|
||||||
|
|
||||||
# Также ищем в подпапках
|
# Также ищем в подпапках
|
||||||
for subdir in search_path.iterdir():
|
for subdir in pyside6_path.iterdir():
|
||||||
if subdir.is_dir() and subdir.name.startswith('Qt'):
|
if subdir.is_dir() and subdir.name.startswith('Qt'):
|
||||||
for so_file in subdir.glob("*.so*"):
|
for so_file in subdir.glob("*.so*"):
|
||||||
if 'Qt' in so_file.name:
|
if 'Qt' in so_file.name:
|
||||||
@@ -257,7 +308,9 @@ class PySide6DependencyAnalyzer:
|
|||||||
|
|
||||||
# Модули для удаления
|
# Модули для удаления
|
||||||
if removable_modules:
|
if removable_modules:
|
||||||
modules_list = ','.join([f"{mod}*" for mod in sorted(removable_modules)])
|
removable_filtered = [m for m in removable_modules if m not in self.critical_modules]
|
||||||
|
if removable_filtered:
|
||||||
|
modules_list = ','.join([f"{mod}*" for mod in sorted(removable_filtered)])
|
||||||
cleanup_lines.append(f" - rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{{{modules_list}}}")
|
cleanup_lines.append(f" - rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{{{modules_list}}}")
|
||||||
|
|
||||||
# Генерируем команду для удаления нативных библиотек с сохранением нужных
|
# Генерируем команду для удаления нативных библиотек с сохранением нужных
|
||||||
@@ -276,39 +329,82 @@ class PySide6DependencyAnalyzer:
|
|||||||
f" - rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/lib/!({keep_pattern})"
|
f" - rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/lib/!({keep_pattern})"
|
||||||
])
|
])
|
||||||
|
|
||||||
# Заменяем блок очистки в рецепте
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
# Ищем блок "# 5) чистим от ненужных модулей и бинарников" до следующего комментария или до AppDir:
|
# Ищем весь блок команд очистки PySide6 (от первой rm до AppDir:)
|
||||||
pattern = r'( # 5\) чистим от ненужных модулей и бинарников\n).*?(?=\nAppDir:|\n # [0-9]+\)|$)'
|
# Паттерн: после " - cp -r lib AppDir/usr\n" идут команды rm, а затем "AppDir:"
|
||||||
|
pattern = r'( - cp -r lib AppDir/usr\n)((?: - (?:rm|shopt).*\n)*?)(?=AppDir:)'
|
||||||
|
|
||||||
new_cleanup_block = " # 5) чистим от ненужных модулей и бинарников\n" + '\n'.join(cleanup_lines)
|
match = re.search(pattern, recipe_content)
|
||||||
|
|
||||||
updated_recipe = re.sub(pattern, new_cleanup_block, recipe_content, flags=re.DOTALL)
|
if not match:
|
||||||
|
print("ПРЕДУПРЕЖДЕНИЕ: Не удалось найти блок очистки в рецепте")
|
||||||
|
print("Добавляем команды очистки перед блоком AppDir:")
|
||||||
|
|
||||||
|
# Просто вставим команды перед AppDir:
|
||||||
|
appdir_pos = recipe_content.find('AppDir:')
|
||||||
|
if appdir_pos != -1:
|
||||||
|
new_content = (
|
||||||
|
recipe_content[:appdir_pos] +
|
||||||
|
'\n'.join(cleanup_lines) + '\n' +
|
||||||
|
recipe_content[appdir_pos:]
|
||||||
|
)
|
||||||
|
return new_content
|
||||||
|
else:
|
||||||
|
print("ОШИБКА: Не найден блок AppDir: в рецепте")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# Создаем замену - группа 1 (cp -r lib) + новые команды очистки
|
||||||
|
replacement = r'\1' + '\n'.join(cleanup_lines) + '\n'
|
||||||
|
|
||||||
|
updated_recipe = re.sub(pattern, replacement, recipe_content, count=1)
|
||||||
|
|
||||||
return updated_recipe
|
return updated_recipe
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description='Анализ зависимостей PySide6 модулей с использованием ldd')
|
parser = argparse.ArgumentParser(description='Анализ зависимостей PySide6 модулей с использованием ldd')
|
||||||
parser.add_argument('project_path', help='Путь к проекту для анализа')
|
parser.add_argument('project_path', nargs='?', default='.',
|
||||||
|
help='Путь к проекту для анализа (по умолчанию: текущая директория)')
|
||||||
parser.add_argument('--appdir', help='Путь к AppDir для поиска PySide6 библиотек')
|
parser.add_argument('--appdir', help='Путь к AppDir для поиска PySide6 библиотек')
|
||||||
parser.add_argument('--output', '-o', help='Путь для сохранения результатов (JSON)')
|
parser.add_argument('--output', '-o', help='Путь для сохранения результатов (JSON)')
|
||||||
parser.add_argument('--verbose', '-v', action='store_true', help='Подробный вывод')
|
parser.add_argument('--verbose', '-v', action='store_true', help='Подробный вывод')
|
||||||
|
parser.add_argument('--venv', help='Путь к виртуальному окружению (по умолчанию: .venv в корне проекта)')
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
project_path = Path(args.project_path)
|
project_path = Path(args.project_path).resolve()
|
||||||
if not project_path.exists():
|
if not project_path.exists():
|
||||||
print(f"Ошибка: путь {project_path} не существует")
|
print(f"Ошибка: путь {project_path} не существует")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
appdir_path = Path(args.appdir) if args.appdir else None
|
appdir_path = Path(args.appdir).resolve() if args.appdir else None
|
||||||
if appdir_path and not appdir_path.exists():
|
if appdir_path and not appdir_path.exists():
|
||||||
print(f"Предупреждение: AppDir путь {appdir_path} не существует")
|
print(f"Предупреждение: AppDir путь {appdir_path} не существует")
|
||||||
appdir_path = None
|
appdir_path = None
|
||||||
|
|
||||||
analyzer = PySide6DependencyAnalyzer()
|
# Определяем корень проекта
|
||||||
|
# Если запущен из подпапки проекта, ищем корень
|
||||||
|
project_root = project_path
|
||||||
|
if (project_path / ".git").exists() or (project_path / "pyproject.toml").exists():
|
||||||
|
project_root = project_path
|
||||||
|
else:
|
||||||
|
# Пытаемся найти корень проекта
|
||||||
|
current = project_path
|
||||||
|
while current != current.parent:
|
||||||
|
if (current / ".git").exists() or (current / "pyproject.toml").exists():
|
||||||
|
project_root = current
|
||||||
|
break
|
||||||
|
current = current.parent
|
||||||
|
|
||||||
|
print(f"Корень проекта: {project_root}")
|
||||||
|
|
||||||
|
analyzer = PySide6DependencyAnalyzer(project_root=project_root)
|
||||||
|
|
||||||
|
# Если указан custom venv путь
|
||||||
|
if args.venv:
|
||||||
|
analyzer.venv_path = Path(args.venv).resolve()
|
||||||
|
print(f"Использую указанный venv: {analyzer.venv_path}")
|
||||||
|
|
||||||
results = analyzer.analyze_project(project_path, appdir_path)
|
results = analyzer.analyze_project(project_path, appdir_path)
|
||||||
|
|
||||||
# Сохраняем в анализатор для генерации команд
|
# Сохраняем в анализатор для генерации команд
|
||||||
@@ -347,13 +443,13 @@ def main():
|
|||||||
print(f"\nПотенциальная экономия места: {results['analysis_summary']['space_saving_potential']}")
|
print(f"\nПотенциальная экономия места: {results['analysis_summary']['space_saving_potential']}")
|
||||||
|
|
||||||
if args.verbose and results['real_dependencies']:
|
if args.verbose and results['real_dependencies']:
|
||||||
Devlin(f"\nРеальные зависимости (ldd):")
|
print(f"\nРеальные зависимости (ldd):")
|
||||||
for module, deps in results['real_dependencies'].items():
|
for module, deps in results['real_dependencies'].items():
|
||||||
if deps:
|
if deps:
|
||||||
print(f" {module} → {', '.join(deps)}")
|
print(f" {module} → {', '.join(deps)}")
|
||||||
|
|
||||||
# Обновляем AppImage рецепт
|
# Обновляем AppImage рецепт
|
||||||
recipe_path = Path("../build-aux/AppImageBuilder.yml")
|
recipe_path = analyzer.build_path / "AppImageBuilder.yml"
|
||||||
if recipe_path.exists():
|
if recipe_path.exists():
|
||||||
updated_recipe = analyzer.generate_appimage_recipe(results['removable'], recipe_path)
|
updated_recipe = analyzer.generate_appimage_recipe(results['removable'], recipe_path)
|
||||||
if updated_recipe:
|
if updated_recipe:
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
from PySide6.QtCore import QLocale, QTranslator, QLibraryInfo
|
from PySide6.QtCore import QLocale, QTranslator, QLibraryInfo, QTimer, Qt
|
||||||
from PySide6.QtWidgets import QApplication
|
from PySide6.QtWidgets import QApplication
|
||||||
from PySide6.QtGui import QIcon
|
from PySide6.QtGui import QIcon
|
||||||
|
from PySide6.QtNetwork import QLocalServer, QLocalSocket
|
||||||
|
|
||||||
from portprotonqt.main_window import MainWindow
|
from portprotonqt.main_window import MainWindow
|
||||||
from portprotonqt.config_utils import save_fullscreen_config, get_portproton_location
|
from portprotonqt.config_utils import (
|
||||||
|
save_fullscreen_config,
|
||||||
|
read_fullscreen_config,
|
||||||
|
get_portproton_start_command
|
||||||
|
)
|
||||||
from portprotonqt.logger import get_logger, setup_logger
|
from portprotonqt.logger import get_logger, setup_logger
|
||||||
from portprotonqt.cli import parse_args
|
from portprotonqt.cli import parse_args
|
||||||
|
|
||||||
@@ -16,25 +22,24 @@ __app_version__ = "0.1.8"
|
|||||||
def get_version():
|
def get_version():
|
||||||
try:
|
try:
|
||||||
commit = subprocess.check_output(
|
commit = subprocess.check_output(
|
||||||
['git', 'rev-parse', '--short', 'HEAD'],
|
["git", "rev-parse", "--short", "HEAD"],
|
||||||
stderr=subprocess.DEVNULL
|
stderr=subprocess.DEVNULL,
|
||||||
).decode('utf-8').strip()
|
).decode("utf-8").strip()
|
||||||
return f"{__app_version__} ({commit})"
|
return f"{__app_version__} ({commit})"
|
||||||
except (subprocess.CalledProcessError, FileNotFoundError, OSError):
|
except (subprocess.CalledProcessError, FileNotFoundError, OSError):
|
||||||
return __app_version__
|
return __app_version__
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
os.environ['PW_CLI'] = '1'
|
os.environ["PW_CLI"] = "1"
|
||||||
os.environ['PROCESS_LOG'] = '1'
|
os.environ["PROCESS_LOG"] = "1"
|
||||||
os.environ['START_FROM_STEAM'] = '1'
|
os.environ["START_FROM_STEAM"] = "1"
|
||||||
|
|
||||||
portproton_path = get_portproton_location()
|
start_sh = get_portproton_start_command()
|
||||||
|
|
||||||
if portproton_path is None:
|
if start_sh is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
script_path = os.path.join(portproton_path, 'data', 'scripts', 'start.sh')
|
subprocess.run(start_sh + ["cli", "--initial"])
|
||||||
subprocess.run([script_path, 'cli', '--initial'])
|
|
||||||
|
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
app.setWindowIcon(QIcon.fromTheme(__app_id__))
|
app.setWindowIcon(QIcon.fromTheme(__app_id__))
|
||||||
@@ -43,41 +48,116 @@ def main():
|
|||||||
app.setApplicationVersion(__app_version__)
|
app.setApplicationVersion(__app_version__)
|
||||||
|
|
||||||
args = parse_args()
|
args = parse_args()
|
||||||
|
|
||||||
# Setup logger with specified debug level
|
|
||||||
setup_logger(args.debug_level)
|
setup_logger(args.debug_level)
|
||||||
|
|
||||||
# Reinitialize logger after setup to ensure it uses the new configuration
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
# --- Single-instance logic ---
|
||||||
|
server_name = __app_id__
|
||||||
|
socket = QLocalSocket()
|
||||||
|
socket.connectToServer(server_name)
|
||||||
|
|
||||||
|
if socket.waitForConnected(200):
|
||||||
|
# Второй экземпляр — передаём команду первому
|
||||||
|
fullscreen = args.fullscreen or read_fullscreen_config()
|
||||||
|
msg = b"show:fullscreen" if fullscreen else b"show"
|
||||||
|
socket.write(msg)
|
||||||
|
socket.flush()
|
||||||
|
socket.waitForBytesWritten(500)
|
||||||
|
socket.disconnectFromServer()
|
||||||
|
logger.info("Restored existing instance from tray")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Если старый сокет остался — удалить
|
||||||
|
QLocalServer.removeServer(server_name)
|
||||||
|
|
||||||
|
local_server = QLocalServer()
|
||||||
|
if not local_server.listen(server_name):
|
||||||
|
logger.warning(f"Failed to start local server: {local_server.errorString()}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# --- Qt translations ---
|
||||||
system_locale = QLocale.system()
|
system_locale = QLocale.system()
|
||||||
qt_translator = QTranslator()
|
qt_translator = QTranslator()
|
||||||
translations_path = QLibraryInfo.path(QLibraryInfo.LibraryPath.TranslationsPath)
|
translations_path = QLibraryInfo.path(QLibraryInfo.LibraryPath.TranslationsPath)
|
||||||
if qt_translator.load(system_locale, "qtbase", "_", translations_path):
|
if qt_translator.load(system_locale, "qtbase", "_", translations_path):
|
||||||
app.installTranslator(qt_translator)
|
app.installTranslator(qt_translator)
|
||||||
else:
|
else:
|
||||||
logger.warning(f"Qt translations for {system_locale.name()} not found in {translations_path}, using english language")
|
logger.warning(
|
||||||
|
f"Qt translations for {system_locale.name()} not found in {translations_path}, using English"
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- Main Window ---
|
||||||
version = get_version()
|
version = get_version()
|
||||||
window = MainWindow(app_name=__app_name__, version=version)
|
window = MainWindow(app_name=__app_name__, version=version)
|
||||||
|
|
||||||
if args.fullscreen:
|
# --- Handle incoming connections ---
|
||||||
logger.info("Launching in fullscreen mode due to --fullscreen flag")
|
def handle_new_connection():
|
||||||
|
conn = local_server.nextPendingConnection()
|
||||||
|
if not conn:
|
||||||
|
return
|
||||||
|
|
||||||
|
if conn.waitForReadyRead(1000):
|
||||||
|
data = conn.readAll().data()
|
||||||
|
msg = bytes(data).decode("utf-8", errors="ignore")
|
||||||
|
logger.info(f"IPC message received: {msg}")
|
||||||
|
|
||||||
|
def restore_window():
|
||||||
|
try:
|
||||||
|
if msg.startswith("show"):
|
||||||
|
if hasattr(window, "restore_from_tray"):
|
||||||
|
window.restore_from_tray() # type: ignore[attr-defined]
|
||||||
|
else:
|
||||||
|
window.showNormal()
|
||||||
|
window.raise_()
|
||||||
|
window.activateWindow()
|
||||||
|
window.setWindowState(
|
||||||
|
window.windowState() & ~Qt.WindowState.WindowMinimized | Qt.WindowState.WindowActive
|
||||||
|
)
|
||||||
|
|
||||||
|
if ":fullscreen" in msg:
|
||||||
|
logger.info("Switching to fullscreen via IPC")
|
||||||
save_fullscreen_config(True)
|
save_fullscreen_config(True)
|
||||||
window.showFullScreen()
|
window.showFullScreen()
|
||||||
|
else:
|
||||||
|
logger.info("Switching to normal window via IPC")
|
||||||
|
save_fullscreen_config(False)
|
||||||
|
window.showNormal()
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to restore window: {e}")
|
||||||
|
|
||||||
|
# Выполняем в основном потоке
|
||||||
|
QTimer.singleShot(0, restore_window)
|
||||||
|
|
||||||
|
conn.disconnectFromServer()
|
||||||
|
|
||||||
|
local_server.newConnection.connect(handle_new_connection)
|
||||||
|
|
||||||
|
# --- Initial fullscreen state ---
|
||||||
|
launch_fullscreen = args.fullscreen or read_fullscreen_config()
|
||||||
|
if launch_fullscreen:
|
||||||
|
logger.info(
|
||||||
|
f"Launching in fullscreen mode ({'--fullscreen' if args.fullscreen else 'config'})"
|
||||||
|
)
|
||||||
|
save_fullscreen_config(True)
|
||||||
|
window.showFullScreen()
|
||||||
|
else:
|
||||||
|
logger.info("Launching in normal mode")
|
||||||
|
save_fullscreen_config(False)
|
||||||
|
window.showNormal()
|
||||||
|
|
||||||
|
# --- Cleanup ---
|
||||||
def cleanup_on_exit():
|
def cleanup_on_exit():
|
||||||
nonlocal window
|
try:
|
||||||
app.aboutToQuit.disconnect()
|
local_server.close()
|
||||||
|
QLocalServer.removeServer(server_name)
|
||||||
if window:
|
if window:
|
||||||
window.close()
|
window.close()
|
||||||
app.quit()
|
except Exception as e:
|
||||||
|
logger.warning(f"Cleanup error: {e}")
|
||||||
|
|
||||||
app.aboutToQuit.connect(cleanup_on_exit)
|
app.aboutToQuit.connect(cleanup_on_exit)
|
||||||
|
|
||||||
window.show()
|
|
||||||
|
|
||||||
sys.exit(app.exec())
|
sys.exit(app.exec())
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import os
|
import os
|
||||||
import configparser
|
import configparser
|
||||||
import shutil
|
import shutil
|
||||||
|
import subprocess
|
||||||
from portprotonqt.logger import get_logger
|
from portprotonqt.logger import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
_portproton_location = None
|
_portproton_location = None
|
||||||
|
_portproton_start_sh = None
|
||||||
|
|
||||||
# Paths to configuration files
|
# Paths to configuration files
|
||||||
CONFIG_FILE = os.path.join(
|
CONFIG_FILE = os.path.join(
|
||||||
@@ -101,14 +103,14 @@ def read_file_content(file_path):
|
|||||||
return f.read().strip()
|
return f.read().strip()
|
||||||
|
|
||||||
def get_portproton_location():
|
def get_portproton_location():
|
||||||
"""Returns the path to the PortProton directory.
|
"""Возвращает путь к PortProton каталогу (строку) или None."""
|
||||||
Checks the cached path first. If not set, reads from PORTPROTON_CONFIG_FILE.
|
|
||||||
If the path is invalid, uses the default directory.
|
|
||||||
"""
|
|
||||||
global _portproton_location
|
global _portproton_location
|
||||||
|
|
||||||
if _portproton_location is not None:
|
if _portproton_location is not None:
|
||||||
return _portproton_location
|
return _portproton_location
|
||||||
|
|
||||||
|
location = None
|
||||||
|
|
||||||
if os.path.isfile(PORTPROTON_CONFIG_FILE):
|
if os.path.isfile(PORTPROTON_CONFIG_FILE):
|
||||||
try:
|
try:
|
||||||
location = read_file_content(PORTPROTON_CONFIG_FILE).strip()
|
location = read_file_content(PORTPROTON_CONFIG_FILE).strip()
|
||||||
@@ -116,19 +118,46 @@ def get_portproton_location():
|
|||||||
_portproton_location = location
|
_portproton_location = location
|
||||||
logger.info(f"PortProton path from configuration: {location}")
|
logger.info(f"PortProton path from configuration: {location}")
|
||||||
return _portproton_location
|
return _portproton_location
|
||||||
logger.warning(f"Invalid PortProton path in configuration: {location}, using default path")
|
logger.warning(f"Invalid PortProton path in configuration: {location}, using defaults")
|
||||||
except (OSError, PermissionError) as e:
|
except (OSError, PermissionError) as e:
|
||||||
logger.warning(f"Failed to read PortProton configuration file: {e}, using default path")
|
logger.warning(f"Failed to read PortProton configuration file: {e}")
|
||||||
|
|
||||||
default_dir = os.path.join(os.path.expanduser("~"), ".var", "app", "ru.linux_gaming.PortProton")
|
default_flatpak_dir = os.path.join(os.path.expanduser("~"), ".var", "app", "ru.linux_gaming.PortProton")
|
||||||
if os.path.isdir(default_dir):
|
if os.path.isdir(default_flatpak_dir):
|
||||||
_portproton_location = default_dir
|
_portproton_location = default_flatpak_dir
|
||||||
logger.info(f"Using flatpak PortProton directory: {default_dir}")
|
logger.info(f"Using Flatpak PortProton directory: {default_flatpak_dir}")
|
||||||
return _portproton_location
|
return _portproton_location
|
||||||
|
|
||||||
logger.warning("PortProton configuration and flatpak directory not found")
|
logger.warning("PortProton configuration and Flatpak directory not found")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def get_portproton_start_command():
|
||||||
|
"""Возвращает список команд для запуска PortProton (start.sh или flatpak run)."""
|
||||||
|
portproton_path = get_portproton_location()
|
||||||
|
if not portproton_path:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["flatpak", "list"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=False
|
||||||
|
)
|
||||||
|
if "ru.linux_gaming.PortProton" in result.stdout:
|
||||||
|
logger.info("Detected Flatpak installation")
|
||||||
|
return ["flatpak", "run", "ru.linux_gaming.PortProton"]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
start_sh_path = os.path.join(portproton_path, "data", "scripts", "start.sh")
|
||||||
|
if os.path.exists(start_sh_path):
|
||||||
|
return [start_sh_path]
|
||||||
|
|
||||||
|
logger.warning("Neither flatpak nor start.sh found for PortProton")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def parse_desktop_entry(file_path):
|
def parse_desktop_entry(file_path):
|
||||||
"""Reads and parses a .desktop file using configparser.
|
"""Reads and parses a .desktop file using configparser.
|
||||||
Returns None if the [Desktop Entry] section is missing.
|
Returns None if the [Desktop Entry] section is missing.
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from PySide6.QtWidgets import QMessageBox, QDialog, QMenu, QLineEdit, QApplicati
|
|||||||
from PySide6.QtCore import QUrl, QPoint, QObject, Signal, Qt
|
from PySide6.QtCore import QUrl, QPoint, QObject, Signal, Qt
|
||||||
from PySide6.QtGui import QDesktopServices, QIcon, QKeySequence
|
from PySide6.QtGui import QDesktopServices, QIcon, QKeySequence
|
||||||
from portprotonqt.localization import _
|
from portprotonqt.localization import _
|
||||||
from portprotonqt.config_utils import parse_desktop_entry, read_favorites, save_favorites, read_favorite_folders, save_favorite_folders
|
from portprotonqt.config_utils import parse_desktop_entry, read_favorites, save_favorites, read_favorite_folders, save_favorite_folders, get_portproton_start_command
|
||||||
from portprotonqt.steam_api import is_game_in_steam, add_to_steam, remove_from_steam
|
from portprotonqt.steam_api import is_game_in_steam, add_to_steam, remove_from_steam
|
||||||
from portprotonqt.egs_api import add_egs_to_steam, get_egs_executable, remove_egs_from_steam
|
from portprotonqt.egs_api import add_egs_to_steam, get_egs_executable, remove_egs_from_steam
|
||||||
from portprotonqt.dialogs import AddGameDialog, FileExplorer, generate_thumbnail
|
from portprotonqt.dialogs import AddGameDialog, FileExplorer, generate_thumbnail
|
||||||
@@ -406,16 +406,7 @@ class ContextMenuManager:
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
# Construct EGS launch command
|
# Construct EGS launch command
|
||||||
wrapper = "flatpak run ru.linux_gaming.PortProton"
|
wrapper = get_portproton_start_command()
|
||||||
start_sh_path = os.path.join(self.portproton_location, "data", "scripts", "start.sh")
|
|
||||||
if self.portproton_location and ".var" not in self.portproton_location:
|
|
||||||
wrapper = start_sh_path
|
|
||||||
if not os.path.exists(start_sh_path):
|
|
||||||
self.signals.show_warning_dialog.emit(
|
|
||||||
_("Error"),
|
|
||||||
_("start.sh not found at {path}").format(path=start_sh_path)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
exec_line = f'"{self.legendary_path}" launch {game_card.appid} --no-wine --wrapper "env START_FROM_STEAM=1 {wrapper}"'
|
exec_line = f'"{self.legendary_path}" launch {game_card.appid} --no-wine --wrapper "env START_FROM_STEAM=1 {wrapper}"'
|
||||||
else:
|
else:
|
||||||
exec_line = self._get_exec_line(game_card.name, game_card.exec_line)
|
exec_line = self._get_exec_line(game_card.name, game_card.exec_line)
|
||||||
|
|||||||
@@ -2,12 +2,11 @@ import os
|
|||||||
import tempfile
|
import tempfile
|
||||||
import re
|
import re
|
||||||
from typing import cast, TYPE_CHECKING
|
from typing import cast, TYPE_CHECKING
|
||||||
from PySide6.QtGui import QPixmap, QIcon, QTextCursor
|
from PySide6.QtGui import QPixmap, QIcon, QTextCursor, QColor
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QDialog, QFormLayout, QHBoxLayout, QLabel, QVBoxLayout, QListWidget, QScrollArea, QWidget, QListWidgetItem, QSizePolicy, QApplication, QProgressBar, QScroller,
|
QDialog, QFormLayout, QHBoxLayout, QLabel, QVBoxLayout, QListWidget, QScrollArea, QWidget, QListWidgetItem, QSizePolicy, QApplication, QProgressBar, QScroller,
|
||||||
QTabWidget, QTableWidget, QHeaderView, QMessageBox, QTableWidgetItem, QTextEdit, QAbstractItemView, QStackedWidget
|
QTabWidget, QTableWidget, QHeaderView, QMessageBox, QTableWidgetItem, QTextEdit, QAbstractItemView, QStackedWidget, QComboBox, QLineEdit
|
||||||
)
|
)
|
||||||
|
|
||||||
from PySide6.QtCore import Qt, QObject, Signal, QMimeDatabase, QTimer, QThreadPool, QRunnable, Slot, QProcess, QProcessEnvironment
|
from PySide6.QtCore import Qt, QObject, Signal, QMimeDatabase, QTimer, QThreadPool, QRunnable, Slot, QProcess, QProcessEnvironment
|
||||||
from icoextract import IconExtractor, IconExtractorError
|
from icoextract import IconExtractor, IconExtractorError
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
@@ -1674,3 +1673,563 @@ class WinetricksDialog(QDialog):
|
|||||||
if self.input_manager:
|
if self.input_manager:
|
||||||
self.input_manager.disable_winetricks_mode()
|
self.input_manager.disable_winetricks_mode()
|
||||||
super().reject()
|
super().reject()
|
||||||
|
|
||||||
|
class ExeSettingsDialog(QDialog):
|
||||||
|
def __init__(self, parent=None, theme=None, exe_path=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.theme = theme if theme else theme_manager.apply_theme(read_theme_from_config())
|
||||||
|
self.exe_path = exe_path
|
||||||
|
if not self.exe_path:
|
||||||
|
return
|
||||||
|
self.portproton_path = get_portproton_location()
|
||||||
|
if self.portproton_path is None:
|
||||||
|
logger.error("PortProton location not found")
|
||||||
|
return
|
||||||
|
base_path = os.path.join(self.portproton_path, "data")
|
||||||
|
self.start_sh = [os.path.join(base_path, "scripts", "start.sh")]
|
||||||
|
|
||||||
|
self.current_settings = {}
|
||||||
|
self.value_widgets = {}
|
||||||
|
self.original_values = {}
|
||||||
|
self.advanced_widgets = {}
|
||||||
|
self.original_display_values = {}
|
||||||
|
self.available_keys = set()
|
||||||
|
self.blocked_keys = set()
|
||||||
|
self.numa_nodes = {}
|
||||||
|
self.is_amd = False
|
||||||
|
self.locale_options = []
|
||||||
|
self.logical_core_options = []
|
||||||
|
self.amd_vulkan_drivers = []
|
||||||
|
|
||||||
|
self.setWindowTitle(_("Exe Settings"))
|
||||||
|
self.setModal(True)
|
||||||
|
self.resize(900, 600)
|
||||||
|
self.setStyleSheet(self.theme.MAIN_WINDOW_STYLE + self.theme.MESSAGE_BOX_STYLE)
|
||||||
|
|
||||||
|
self.init_toggle_settings()
|
||||||
|
self.setup_ui()
|
||||||
|
|
||||||
|
# Find input_manager and main_window
|
||||||
|
self.input_manager = None
|
||||||
|
self.main_window = None
|
||||||
|
parent = self.parent()
|
||||||
|
while parent:
|
||||||
|
if hasattr(parent, 'input_manager'):
|
||||||
|
self.input_manager = cast("MainWindow", parent).input_manager
|
||||||
|
self.main_window = parent
|
||||||
|
parent = parent.parent()
|
||||||
|
|
||||||
|
self.current_theme_name = read_theme_from_config()
|
||||||
|
|
||||||
|
# Load current settings (includes list-db)
|
||||||
|
self.load_current_settings()
|
||||||
|
|
||||||
|
def _get_process_args(self, subcommand_args):
|
||||||
|
"""Get the full arguments for QProcess.start, handling flatpak separator."""
|
||||||
|
if self.start_sh[0] == "flatpak":
|
||||||
|
return self.start_sh[1:] + ["--"] + subcommand_args
|
||||||
|
else:
|
||||||
|
return self.start_sh + subcommand_args
|
||||||
|
|
||||||
|
def init_toggle_settings(self):
|
||||||
|
"""Initialize predefined toggle settings with descriptions."""
|
||||||
|
self.toggle_settings = {
|
||||||
|
'PW_MANGOHUD': _("Using FPS and system load monitoring (Turns on and off by the key combination - right Shift + F12)"),
|
||||||
|
'PW_MANGOHUD_USER_CONF': _("Forced use of MANGOHUD system settings (GOverlay, etc.)"),
|
||||||
|
'PW_VKBASALT': _("Enable vkBasalt by default to improve graphics in games running on Vulkan. (The HOME hotkey disables vkbasalt)"),
|
||||||
|
'PW_VKBASALT_USER_CONF': _("Forced use of VKBASALT system settings (GOverlay, etc.)"),
|
||||||
|
'PW_DGVOODOO2': _("Enable dgVoodoo2. Forced use all dgVoodoo2 libs (Glide 2.11-3.1, DirectDraw 1-7, Direct3D 2-9) on all 3D API."),
|
||||||
|
'PW_GAMESCOPE': _("Super + F : Toggle fullscreen\nSuper + N : Toggle nearest neighbour filtering\nSuper + U : Toggle FSR upscaling\nSuper + Y : Toggle NIS upscaling\nSuper + I : Increase FSR sharpness by 1\nSuper + O : Decrease FSR sharpness by 1\nSuper + S : Take screenshot (currently goes to /tmp/gamescope_DATE.png)\nSuper + G : Toggle keyboard grab\nSuper + C : Update clipboard"),
|
||||||
|
'PW_USE_ESYNC': _("Enable in-process synchronization primitives based on eventfd."),
|
||||||
|
'PW_USE_FSYNC': _("Enable futex-based in-process synchronization primitives."),
|
||||||
|
'PW_USE_NTSYNC': _("Enable in-process synchronization via the Linux ntsync driver."),
|
||||||
|
'PW_USE_RAY_TRACING': _("Enable vkd3d support - Ray Tracing"),
|
||||||
|
'PW_USE_NVAPI_AND_DLSS': _("Enable DLSS on supported NVIDIA graphics cards"),
|
||||||
|
'PW_USE_OPTISCALER': _("Enable OptiScaler (replacement upscaler / frame generator)"),
|
||||||
|
'PW_USE_LS_FRAME_GEN': _("Enable Lossless Scaling frame generation (experimental)"),
|
||||||
|
'PW_WINE_FULLSCREEN_FSR': _("FSR upscaling in fullscreen with ProtonGE below native resolution"),
|
||||||
|
'PW_HIDE_NVIDIA_GPU': _("Disguise all NVIDIA GPU features"),
|
||||||
|
'PW_VIRTUAL_DESKTOP': _("Run the application in WINE virtual desktop"),
|
||||||
|
'PW_USE_TERMINAL': _("Run the application in a terminal"),
|
||||||
|
'PW_GUI_DISABLED_CS': _("Disable startup mode and WINE version selector window"),
|
||||||
|
'PW_USE_GAMEMODE': _("Use system GameMode for performance optimization"),
|
||||||
|
'PW_USE_D3D_EXTRAS': _("Enable forced use of third-party DirectX libraries"),
|
||||||
|
'PW_FIX_VIDEO_IN_GAME': _("Fix pink-tinted video playback in some games"),
|
||||||
|
'PW_REDUCE_PULSE_LATENCY': _("Reduce PulseAudio latency to fix intermittent sound"),
|
||||||
|
'PW_USE_US_LAYOUT': _("Force US keyboard layout"),
|
||||||
|
'PW_USE_GSTREAMER': _("Use GStreamer for in-game clips (WMF support)"),
|
||||||
|
'PW_USE_SHADER_CACHE': _("Use WINE shader caching"),
|
||||||
|
'PW_USE_WINE_DXGI': _("Force use of built-in DXGI library"),
|
||||||
|
'PW_USE_EAC_AND_BE': _("Enable Easy Anti-Cheat and BattlEye runtimes"),
|
||||||
|
'PW_USE_SYSTEM_VK_LAYERS': _("Use system Vulkan layers (MangoHud, vkBasalt, OBS, etc.)"),
|
||||||
|
'PW_USE_OBS_VKCAPTURE': _("Enable OBS Studio capture via obs-vkcapture"),
|
||||||
|
'PW_DISABLE_COMPOSITING': _("Disable desktop compositing for performance"),
|
||||||
|
'PW_USE_RUNTIME': _("Use container launch mode (recommended default)"),
|
||||||
|
'PW_DINPUT_PROTOCOL': _("Force DirectInput protocol instead of XInput"),
|
||||||
|
'PW_USE_NATIVE_WAYLAND': _("Enable experimental native Wayland support"),
|
||||||
|
'PW_USE_DXVK_HDR': _("Enable HDR settings under native Wayland"),
|
||||||
|
'PW_USE_GALLIUM_ZINK': _("Use Gallium Zink (OpenGL via Vulkan)"),
|
||||||
|
'PW_USE_GALLIUM_NINE': _("Use Gallium Nine (native DirectX 9 for Mesa)"),
|
||||||
|
'PW_USE_WINED3D_VULKAN': _("Use WineD3D Vulkan backend (Damavand)"),
|
||||||
|
'PW_USE_SUPPLIED_DXVK_VKD3D': _("Use bundled dxvk/vkd3d from Wine/Proton"),
|
||||||
|
'PW_USE_SAREK_ASYNC': _("Use async dxvk-sarek (experimental)")
|
||||||
|
}
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
"""Set up the user interface."""
|
||||||
|
self.main_layout = QVBoxLayout(self)
|
||||||
|
self.main_layout.setContentsMargins(10, 10, 10, 10)
|
||||||
|
self.main_layout.setSpacing(10)
|
||||||
|
|
||||||
|
# Tab widget
|
||||||
|
self.tab_widget = QTabWidget()
|
||||||
|
self.main_tab = QWidget()
|
||||||
|
self.main_tab_layout = QVBoxLayout(self.main_tab)
|
||||||
|
self.advanced_tab = QWidget()
|
||||||
|
self.advanced_tab_layout = QVBoxLayout(self.advanced_tab)
|
||||||
|
|
||||||
|
self.tab_widget.addTab(self.main_tab, _("Main"))
|
||||||
|
self.tab_widget.addTab(self.advanced_tab, _("Advanced"))
|
||||||
|
|
||||||
|
# Таблица настроек
|
||||||
|
self.settings_table = QTableWidget()
|
||||||
|
self.settings_table.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||||
|
self.settings_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||||
|
self.settings_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
||||||
|
self.settings_table.setColumnCount(3)
|
||||||
|
self.settings_table.setHorizontalHeaderLabels([_("Setting"), _("Value"), _("Description")])
|
||||||
|
self.settings_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
|
||||||
|
self.settings_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
|
||||||
|
self.settings_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
|
||||||
|
self.settings_table.horizontalHeader().resizeSection(1, 100)
|
||||||
|
self.settings_table.setWordWrap(True)
|
||||||
|
self.settings_table.verticalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
|
||||||
|
self.settings_table.setTextElideMode(Qt.TextElideMode.ElideNone)
|
||||||
|
self.settings_table.setStyleSheet(self.theme.WINETRICKS_TABBLE_STYLE)
|
||||||
|
self.main_tab_layout.addWidget(self.settings_table)
|
||||||
|
|
||||||
|
# Таблица Advanced
|
||||||
|
self.advanced_table = QTableWidget()
|
||||||
|
self.advanced_table.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||||
|
self.advanced_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||||
|
self.advanced_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
||||||
|
self.advanced_table.setColumnCount(3)
|
||||||
|
self.advanced_table.setHorizontalHeaderLabels([_("Setting"), _("Value"), _("Description")])
|
||||||
|
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, 200)
|
||||||
|
self.advanced_table.setWordWrap(True)
|
||||||
|
self.advanced_table.verticalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
|
||||||
|
self.advanced_table.setTextElideMode(Qt.TextElideMode.ElideNone)
|
||||||
|
self.advanced_table.setStyleSheet(self.theme.WINETRICKS_TABBLE_STYLE)
|
||||||
|
self.advanced_tab_layout.addWidget(self.advanced_table)
|
||||||
|
|
||||||
|
self.main_layout.addWidget(self.tab_widget)
|
||||||
|
|
||||||
|
# Кнопки
|
||||||
|
button_layout = QHBoxLayout()
|
||||||
|
self.apply_button = AutoSizeButton(_("Apply"), icon=ThemeManager().get_icon("apply"))
|
||||||
|
self.cancel_button = AutoSizeButton(_("Cancel"), icon=ThemeManager().get_icon("cancel"))
|
||||||
|
self.apply_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||||
|
self.cancel_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||||
|
button_layout.addWidget(self.apply_button)
|
||||||
|
button_layout.addWidget(self.cancel_button)
|
||||||
|
self.main_layout.addLayout(button_layout)
|
||||||
|
|
||||||
|
self.apply_button.clicked.connect(self.apply_changes)
|
||||||
|
self.cancel_button.clicked.connect(self.reject)
|
||||||
|
|
||||||
|
|
||||||
|
def load_current_settings(self):
|
||||||
|
"""Load available toggles first, then current settings."""
|
||||||
|
process = QProcess(self)
|
||||||
|
process.finished.connect(self.on_list_db_finished)
|
||||||
|
process.start(self.start_sh[0], ["cli", "--list-db"])
|
||||||
|
|
||||||
|
|
||||||
|
def on_list_db_finished(self, exit_code, exit_status):
|
||||||
|
"""Handle --list-db output and extract available keys and system info."""
|
||||||
|
process = cast(QProcess, self.sender())
|
||||||
|
self.available_keys = set()
|
||||||
|
self.blocked_keys = set()
|
||||||
|
if exit_code == 0 and exit_status == QProcess.ExitStatus.NormalExit:
|
||||||
|
output = bytes(process.readAllStandardOutput().data()).decode('utf-8', 'ignore')
|
||||||
|
lines = output.splitlines()
|
||||||
|
self.numa_nodes = {}
|
||||||
|
self.is_amd = False
|
||||||
|
self.logical_core_options = []
|
||||||
|
self.locale_options = []
|
||||||
|
self.amd_vulkan_drivers = []
|
||||||
|
for line in lines:
|
||||||
|
line_stripped = line.strip()
|
||||||
|
if not line_stripped:
|
||||||
|
continue
|
||||||
|
if re.match(r'^[A-Z_0-9]+=[^=]+$', line_stripped) and not line_stripped.startswith('PW_'):
|
||||||
|
# System info
|
||||||
|
k, v = line_stripped.split('=', 1)
|
||||||
|
if k.startswith('NUMA_NODE_'):
|
||||||
|
node_id = k[10:]
|
||||||
|
self.numa_nodes[node_id] = v
|
||||||
|
elif k == 'IS_AMD':
|
||||||
|
self.is_amd = v.lower() == 'true'
|
||||||
|
elif k == 'LOGICAL_CORE_OPTIONS':
|
||||||
|
self.logical_core_options = v.split('!') if v else []
|
||||||
|
elif k == 'LOCALE_LIST':
|
||||||
|
self.locale_options = v.split('!') if v else []
|
||||||
|
elif k == 'AMD_VULKAN_DRIVER_LIST':
|
||||||
|
self.amd_vulkan_drivers = v.split('!') if v else []
|
||||||
|
continue
|
||||||
|
if line_stripped.startswith('PW_'):
|
||||||
|
parts = line_stripped.split(maxsplit=1)
|
||||||
|
key = parts[0]
|
||||||
|
self.available_keys.add(key)
|
||||||
|
if len(parts) > 1 and 'blocked' in parts[1]:
|
||||||
|
self.blocked_keys.add(key)
|
||||||
|
|
||||||
|
# Показываем только пересечение
|
||||||
|
self.available_keys &= set(self.toggle_settings.keys())
|
||||||
|
|
||||||
|
# Загружаем текущие настройки
|
||||||
|
process = QProcess(self)
|
||||||
|
process.finished.connect(self.on_show_ppdb_finished)
|
||||||
|
process.start(self.start_sh[0], ["cli", "--show-ppdb", f"{self.exe_path}.ppdb"])
|
||||||
|
|
||||||
|
def on_show_ppdb_finished(self, exit_code, exit_status):
|
||||||
|
"""Handle --show-ppdb output."""
|
||||||
|
process = cast(QProcess, self.sender())
|
||||||
|
if exit_code != 0 or exit_status != QProcess.ExitStatus.NormalExit:
|
||||||
|
# Fallback to defaults if load fails
|
||||||
|
for key in self.toggle_settings:
|
||||||
|
self.current_settings[key] = '0'
|
||||||
|
for adv_key in ['PW_WINDOWS_VER', 'WINEDLLOVERRIDES', 'LAUNCH_PARAMETERS',
|
||||||
|
'PW_WINE_CPU_TOPOLOGY', 'PW_MESA_GL_VERSION_OVERRIDE',
|
||||||
|
'PW_VKD3D_FEATURE_LEVEL', 'PW_LOCALE_SELECT',
|
||||||
|
'PW_MESA_VK_WSI_PRESENT_MODE', 'PW_AMD_VULKAN_USE',
|
||||||
|
'PW_CPU_NUMA_NODE_INDEX']:
|
||||||
|
self.current_settings[adv_key] = 'disabled' if 'TOPOLOGY' in adv_key or 'SELECT' in adv_key or 'MODE' in adv_key or 'LEVEL' in adv_key or 'GL_VERSION' in adv_key or 'NUMA' in adv_key else ''
|
||||||
|
else:
|
||||||
|
output = bytes(process.readAllStandardOutput().data()).decode('utf-8', 'ignore').strip()
|
||||||
|
self.current_settings = {}
|
||||||
|
for line in output.split('\n'):
|
||||||
|
line_stripped = line.strip()
|
||||||
|
if '=' in line_stripped:
|
||||||
|
# Parse all KEY=VALUE lines, not just specific prefixes, to catch more
|
||||||
|
try:
|
||||||
|
key, val = line_stripped.split('=', 1)
|
||||||
|
if key in self.toggle_settings or key in ['PW_WINDOWS_VER', 'WINEDLLOVERRIDES', 'LAUNCH_PARAMETERS',
|
||||||
|
'PW_WINE_CPU_TOPOLOGY', 'PW_MESA_GL_VERSION_OVERRIDE',
|
||||||
|
'PW_VKD3D_FEATURE_LEVEL', 'PW_LOCALE_SELECT',
|
||||||
|
'PW_MESA_VK_WSI_PRESENT_MODE', 'PW_AMD_VULKAN_USE',
|
||||||
|
'PW_CPU_NUMA_NODE_INDEX', 'PW_TASKSET_SLR']:
|
||||||
|
self.current_settings[key] = val
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Force blocked settings to '0'
|
||||||
|
for key in self.blocked_keys:
|
||||||
|
self.current_settings[key] = '0'
|
||||||
|
|
||||||
|
self.original_values = self.current_settings.copy()
|
||||||
|
for key in set(self.toggle_settings.keys()):
|
||||||
|
self.original_values.setdefault(key, '0')
|
||||||
|
|
||||||
|
self.populate_table()
|
||||||
|
self.populate_advanced()
|
||||||
|
|
||||||
|
def populate_table(self):
|
||||||
|
"""Populate the table with settings that are available in both lists."""
|
||||||
|
self.settings_table.setRowCount(0)
|
||||||
|
self.value_widgets.clear()
|
||||||
|
self.settings_table.verticalHeader().setVisible(False)
|
||||||
|
|
||||||
|
visible_keys = sorted(self.available_keys) if self.available_keys else sorted(self.toggle_settings.keys())
|
||||||
|
|
||||||
|
for toggle in visible_keys:
|
||||||
|
description = self.toggle_settings.get(toggle)
|
||||||
|
if not description:
|
||||||
|
continue
|
||||||
|
|
||||||
|
row = self.settings_table.rowCount()
|
||||||
|
self.settings_table.insertRow(row)
|
||||||
|
|
||||||
|
name_item = QTableWidgetItem(toggle)
|
||||||
|
name_item.setFlags(Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled)
|
||||||
|
|
||||||
|
current_val = self.current_settings.get(toggle, '0')
|
||||||
|
is_blocked = toggle in self.blocked_keys
|
||||||
|
checkbox = QTableWidgetItem()
|
||||||
|
checkbox.setFlags(checkbox.flags() | Qt.ItemFlag.ItemIsUserCheckable)
|
||||||
|
check_state = Qt.CheckState.Checked if current_val == '1' and not is_blocked else Qt.CheckState.Unchecked
|
||||||
|
checkbox.setCheckState(check_state)
|
||||||
|
checkbox.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
if is_blocked:
|
||||||
|
checkbox.setFlags(checkbox.flags() & ~Qt.ItemFlag.ItemIsUserCheckable)
|
||||||
|
checkbox.setBackground(QColor(240, 240, 240))
|
||||||
|
name_item.setForeground(QColor(128, 128, 128))
|
||||||
|
self.settings_table.setItem(row, 1, checkbox)
|
||||||
|
|
||||||
|
desc_item = QTableWidgetItem(description)
|
||||||
|
desc_item.setFlags(Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled)
|
||||||
|
desc_item.setToolTip(description)
|
||||||
|
desc_item.setTextAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
|
||||||
|
if is_blocked:
|
||||||
|
desc_item.setForeground(QColor(128, 128, 128))
|
||||||
|
self.settings_table.setItem(row, 2, desc_item)
|
||||||
|
|
||||||
|
self.settings_table.setItem(row, 0, name_item)
|
||||||
|
self.value_widgets[(row, 1)] = checkbox
|
||||||
|
|
||||||
|
self.settings_table.resizeRowsToContents()
|
||||||
|
if self.settings_table.rowCount() > 0:
|
||||||
|
self.settings_table.setCurrentCell(0, 0)
|
||||||
|
self.settings_table.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||||
|
|
||||||
|
def populate_advanced(self):
|
||||||
|
"""Populate the advanced tab with table format."""
|
||||||
|
self.advanced_table.setRowCount(0)
|
||||||
|
self.advanced_widgets.clear()
|
||||||
|
self.original_display_values = {}
|
||||||
|
self.advanced_table.verticalHeader().setVisible(False)
|
||||||
|
|
||||||
|
current = self.current_settings
|
||||||
|
disabled_text = _('disabled')
|
||||||
|
|
||||||
|
# Define advanced settings configuration
|
||||||
|
advanced_settings = []
|
||||||
|
|
||||||
|
# 1. Windows version
|
||||||
|
advanced_settings.append({
|
||||||
|
'key': 'PW_WINDOWS_VER',
|
||||||
|
'name': _("Windows version"),
|
||||||
|
'description': _("Changing the WINDOWS emulation version may be required to run older games. WINDOWS versions below 10 do not support new games with DirectX 12"),
|
||||||
|
'type': 'combo',
|
||||||
|
'options': ['11', '10', '7', 'XP'],
|
||||||
|
'default': '10'
|
||||||
|
})
|
||||||
|
|
||||||
|
# 2. Forced to use/disable libraries
|
||||||
|
advanced_settings.append({
|
||||||
|
'key': 'WINEDLLOVERRIDES',
|
||||||
|
'name': _("DLL Overrides"),
|
||||||
|
'description': _("Forced to use/disable the library only for the given application.\n\nA brief instruction:\n* libraries are written WITHOUT the .dll file extension\n* libraries are separated by semicolons - ;\n* library=n - use the WINDOWS (third-party) library\n* library=b - use WINE (built-in) library\n* library=n,b - use WINDOWS library and then WINE\n* library=b,n - use WINE library and then WINDOWS\n* library= - disable the use of this library\n\nExample: libglesv2=;d3dx9_36,d3dx9_42=n,b;mfc120=b,n"),
|
||||||
|
'type': 'text',
|
||||||
|
'default': ''
|
||||||
|
})
|
||||||
|
|
||||||
|
# 3. Launch arguments
|
||||||
|
advanced_settings.append({
|
||||||
|
'key': 'LAUNCH_PARAMETERS',
|
||||||
|
'name': _("Launch Arguments"),
|
||||||
|
'description': _("Adding an argument after the .exe file, just like you would add an argument in a shortcut on a WINDOWS system.\n\nExample: -dx11 -skipintro 1"),
|
||||||
|
'type': 'text',
|
||||||
|
'default': ''
|
||||||
|
})
|
||||||
|
|
||||||
|
# 4. CPU cores limit
|
||||||
|
advanced_settings.append({
|
||||||
|
'key': 'PW_WINE_CPU_TOPOLOGY',
|
||||||
|
'name': _("CPU Cores Limit"),
|
||||||
|
'description': _("Limiting the number of CPU cores is useful for Unity games (It is recommended to set the value equal to 8)"),
|
||||||
|
'type': 'combo',
|
||||||
|
'options': [disabled_text] + self.logical_core_options,
|
||||||
|
'default': disabled_text
|
||||||
|
})
|
||||||
|
|
||||||
|
# 5. OpenGL version
|
||||||
|
advanced_settings.append({
|
||||||
|
'key': 'PW_MESA_GL_VERSION_OVERRIDE',
|
||||||
|
'name': _("OpenGL Version"),
|
||||||
|
'description': _("You can select the required OpenGL version, some games require a forced Compatibility Profile (COMP)."),
|
||||||
|
'type': 'combo',
|
||||||
|
'options': [disabled_text, '4.6COMPAT', '4.5COMPAT', '4.3COMPAT', '4.1COMPAT', '3.3COMPAT', '3.2COMPAT'],
|
||||||
|
'default': disabled_text
|
||||||
|
})
|
||||||
|
|
||||||
|
# 6. VKD3D feature level
|
||||||
|
advanced_settings.append({
|
||||||
|
'key': 'PW_VKD3D_FEATURE_LEVEL',
|
||||||
|
'name': _("VKD3D Feature Level"),
|
||||||
|
'description': _("You can set a forced feature level VKD3D for games on DirectX12"),
|
||||||
|
'type': 'combo',
|
||||||
|
'options': [disabled_text, '12_2', '12_1', '12_0', '11_1', '11_0'],
|
||||||
|
'default': disabled_text
|
||||||
|
})
|
||||||
|
|
||||||
|
# 7. Locale
|
||||||
|
advanced_settings.append({
|
||||||
|
'key': 'PW_LOCALE_SELECT',
|
||||||
|
'name': _("Locale"),
|
||||||
|
'description': _("Force certain locale for an app. Fixes encoding issues in legacy software"),
|
||||||
|
'type': 'combo',
|
||||||
|
'options': [disabled_text] + self.locale_options,
|
||||||
|
'default': disabled_text
|
||||||
|
})
|
||||||
|
|
||||||
|
# 8. Present mode
|
||||||
|
advanced_settings.append({
|
||||||
|
'key': 'PW_MESA_VK_WSI_PRESENT_MODE',
|
||||||
|
'name': _("Window Mode"),
|
||||||
|
'description': _("Window mode (for Vulkan and OpenGL):\nfifo - First in, first out. Limits the frame rate + no tearing. (VSync)\nimmediate - Unlimited frame rate + tearing.\nmailbox - Triple buffering. Unlimited frame rate + no tearing.\nrelaxed - Same as fifo but allows tearing when below the monitors refresh rate."),
|
||||||
|
'type': 'combo',
|
||||||
|
'options': [disabled_text, 'fifo', 'immediate', 'mailbox', 'relaxed'],
|
||||||
|
'default': disabled_text
|
||||||
|
})
|
||||||
|
|
||||||
|
# 9. AMD Vulkan (always show, block if not applicable)
|
||||||
|
amd_options = [disabled_text] + self.amd_vulkan_drivers if self.is_amd and self.amd_vulkan_drivers else [disabled_text]
|
||||||
|
advanced_settings.append({
|
||||||
|
'key': 'PW_AMD_VULKAN_USE',
|
||||||
|
'name': _("AMD Vulkan Driver"),
|
||||||
|
'description': _("Select needed AMD vulkan implementation. Choosing which implementation of vulkan will be used to run the game"),
|
||||||
|
'type': 'combo',
|
||||||
|
'options': amd_options,
|
||||||
|
'default': disabled_text
|
||||||
|
})
|
||||||
|
|
||||||
|
# 10. NUMA node (always show if numa_nodes exist, block if <=1)
|
||||||
|
numa_ids = sorted(self.numa_nodes.keys())
|
||||||
|
numa_options = [disabled_text] + numa_ids if len(numa_ids) > 1 else [disabled_text]
|
||||||
|
advanced_settings.append({
|
||||||
|
'key': 'PW_CPU_NUMA_NODE_INDEX',
|
||||||
|
'name': _("NUMA Node"),
|
||||||
|
'description': _("NUMA node for CPU affinity. In multi-core systems, CPUs are split into NUMA nodes, each with its own local memory and cores. Binding a game to a single node reduces memory-access latency and limits costly core-to-core switches."),
|
||||||
|
'type': 'combo',
|
||||||
|
'options': numa_options,
|
||||||
|
'default': disabled_text
|
||||||
|
})
|
||||||
|
|
||||||
|
# Populate table
|
||||||
|
for setting in advanced_settings:
|
||||||
|
row = self.advanced_table.rowCount()
|
||||||
|
self.advanced_table.insertRow(row)
|
||||||
|
|
||||||
|
# Name column
|
||||||
|
name_item = QTableWidgetItem(setting['name'])
|
||||||
|
name_item.setFlags(Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled)
|
||||||
|
self.advanced_table.setItem(row, 0, name_item)
|
||||||
|
|
||||||
|
# Value column (widget)
|
||||||
|
if setting['type'] == 'combo':
|
||||||
|
combo = QComboBox()
|
||||||
|
combo.addItems(setting['options'])
|
||||||
|
|
||||||
|
# Get current value
|
||||||
|
current_raw = current.get(setting['key'], setting['default'])
|
||||||
|
if setting['key'] == 'PW_WINE_CPU_TOPOLOGY':
|
||||||
|
current_val = disabled_text if current_raw == 'disabled' else (current_raw.split(':')[0] if isinstance(current_raw, str) and ':' in current_raw else current_raw)
|
||||||
|
elif setting['key'] == 'PW_AMD_VULKAN_USE':
|
||||||
|
current_val = disabled_text if not current_raw or current_raw == '' else current_raw
|
||||||
|
else:
|
||||||
|
current_val = disabled_text if current_raw == 'disabled' else current_raw
|
||||||
|
|
||||||
|
if current_val not in setting['options']:
|
||||||
|
combo.addItem(current_val)
|
||||||
|
combo.setCurrentText(current_val)
|
||||||
|
|
||||||
|
# Block if only disabled option
|
||||||
|
if len(setting['options']) == 1:
|
||||||
|
combo.setEnabled(False)
|
||||||
|
|
||||||
|
self.advanced_table.setCellWidget(row, 1, combo)
|
||||||
|
self.advanced_widgets[setting['key']] = combo
|
||||||
|
self.original_display_values[setting['key']] = current_val
|
||||||
|
|
||||||
|
elif setting['type'] == 'text':
|
||||||
|
line_edit = QLineEdit()
|
||||||
|
current_val = current.get(setting['key'], setting['default'])
|
||||||
|
line_edit.setText(current_val)
|
||||||
|
|
||||||
|
self.advanced_table.setCellWidget(row, 1, line_edit)
|
||||||
|
self.advanced_widgets[setting['key']] = line_edit
|
||||||
|
self.original_display_values[setting['key']] = current_val
|
||||||
|
|
||||||
|
# Description column
|
||||||
|
desc_item = QTableWidgetItem(setting['description'])
|
||||||
|
desc_item.setFlags(Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled)
|
||||||
|
desc_item.setToolTip(setting['description'])
|
||||||
|
desc_item.setTextAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
|
||||||
|
self.advanced_table.setItem(row, 2, desc_item)
|
||||||
|
|
||||||
|
# Make sure QLineEdit and QComboBox look consistent
|
||||||
|
self.advanced_table.setStyleSheet("""
|
||||||
|
QComboBox, QLineEdit {
|
||||||
|
padding: 3px 6px;
|
||||||
|
min-height: 26px;
|
||||||
|
}
|
||||||
|
QComboBox::drop-down {
|
||||||
|
subcontrol-origin: padding;
|
||||||
|
subcontrol-position: top right;
|
||||||
|
width: 18px;
|
||||||
|
}
|
||||||
|
QTextEdit {
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
def apply_changes(self):
|
||||||
|
"""Apply changes by collecting diffs from both main and advanced tabs."""
|
||||||
|
changes = []
|
||||||
|
|
||||||
|
for key, orig_val in self.original_values.items():
|
||||||
|
if key in self.blocked_keys:
|
||||||
|
continue # Skip blocked keys
|
||||||
|
row = -1
|
||||||
|
for r in range(self.settings_table.rowCount()):
|
||||||
|
item0 = self.settings_table.item(r, 0)
|
||||||
|
if item0 and item0.text() == key:
|
||||||
|
row = r
|
||||||
|
break
|
||||||
|
if row == -1:
|
||||||
|
continue
|
||||||
|
|
||||||
|
item = self.settings_table.item(row, 1)
|
||||||
|
if not item:
|
||||||
|
continue
|
||||||
|
|
||||||
|
new_val = '1' if item.checkState() == Qt.CheckState.Checked else '0'
|
||||||
|
if new_val != orig_val:
|
||||||
|
changes.append(f"{key}={new_val}")
|
||||||
|
|
||||||
|
for key, widget in self.advanced_widgets.items():
|
||||||
|
orig_val = self.original_display_values.get(key, '')
|
||||||
|
if isinstance(widget, QComboBox):
|
||||||
|
new_val = widget.currentText()
|
||||||
|
if new_val.lower() == _('disabled').lower():
|
||||||
|
new_val = 'disabled'
|
||||||
|
elif isinstance(widget, QLineEdit):
|
||||||
|
new_val = widget.text().strip()
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if new_val != orig_val:
|
||||||
|
changes.append(f"{key}={new_val}")
|
||||||
|
|
||||||
|
if not changes:
|
||||||
|
QMessageBox.information(self, _("Info"), _("No changes to apply."))
|
||||||
|
return
|
||||||
|
|
||||||
|
process = QProcess(self)
|
||||||
|
process.finished.connect(self.on_edit_db_finished)
|
||||||
|
args = ["cli", "--edit-db", self.exe_path] + changes
|
||||||
|
process.start(self.start_sh[0], args)
|
||||||
|
self.apply_button.setEnabled(False)
|
||||||
|
|
||||||
|
def on_edit_db_finished(self, exit_code, exit_status):
|
||||||
|
"""Handle --edit-db output."""
|
||||||
|
process = cast(QProcess, self.sender())
|
||||||
|
self.apply_button.setEnabled(True)
|
||||||
|
if exit_code != 0 or exit_status != QProcess.ExitStatus.NormalExit:
|
||||||
|
error_output = bytes(process.readAllStandardError().data()).decode('utf-8', 'ignore')
|
||||||
|
QMessageBox.warning(self, _("Error"), _("Failed to apply changes. Check logs."))
|
||||||
|
logger.error(f"Failed to apply changes: {error_output}")
|
||||||
|
else:
|
||||||
|
self.load_current_settings()
|
||||||
|
QMessageBox.information(self, _("Success"), _("Settings updated successfully."))
|
||||||
|
|
||||||
|
def closeEvent(self, event):
|
||||||
|
super().closeEvent(event)
|
||||||
|
|
||||||
|
def reject(self):
|
||||||
|
super().reject()
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from portprotonqt.localization import get_egs_language, _
|
|||||||
from portprotonqt.logger import get_logger
|
from portprotonqt.logger import get_logger
|
||||||
from portprotonqt.image_utils import load_pixmap_async
|
from portprotonqt.image_utils import load_pixmap_async
|
||||||
from portprotonqt.time_utils import parse_playtime_file, format_playtime, get_last_launch, get_last_launch_timestamp
|
from portprotonqt.time_utils import parse_playtime_file, format_playtime, get_last_launch, get_last_launch_timestamp
|
||||||
from portprotonqt.config_utils import get_portproton_location
|
from portprotonqt.config_utils import get_portproton_location, get_portproton_start_command
|
||||||
from portprotonqt.steam_api import (
|
from portprotonqt.steam_api import (
|
||||||
get_weanticheatyet_status_async, get_steam_apps_and_index_async, get_protondb_tier_async,
|
get_weanticheatyet_status_async, get_steam_apps_and_index_async, get_protondb_tier_async,
|
||||||
search_app, get_steam_home, get_last_steam_user, convert_steam_id, generate_thumbnail, call_steam_api
|
search_app, get_steam_home, get_last_steam_user, convert_steam_id, generate_thumbnail, call_steam_api
|
||||||
@@ -254,14 +254,7 @@ def add_egs_to_steam(app_name: str, game_title: str, legendary_path: str, callba
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Determine wrapper
|
# Determine wrapper
|
||||||
wrapper = "flatpak run ru.linux_gaming.PortProton"
|
wrapper = get_portproton_start_command()
|
||||||
start_sh_path = os.path.join(portproton_dir, "data", "scripts", "start.sh")
|
|
||||||
if portproton_dir is not None and ".var" not in portproton_dir:
|
|
||||||
wrapper = start_sh_path
|
|
||||||
if not os.path.exists(start_sh_path):
|
|
||||||
logger.error(f"start.sh not found at {start_sh_path}")
|
|
||||||
callback((False, f"start.sh not found at {start_sh_path}"))
|
|
||||||
return
|
|
||||||
|
|
||||||
# Create launch script
|
# Create launch script
|
||||||
steam_scripts_dir = os.path.join(portproton_dir, "steam_scripts")
|
steam_scripts_dir = os.path.join(portproton_dir, "steam_scripts")
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from PySide6.QtGui import QPainter, QColor, QDesktopServices
|
from PySide6.QtGui import QPainter, QColor, QDesktopServices
|
||||||
from PySide6.QtCore import Signal, Property, Qt, QUrl
|
from PySide6.QtCore import Signal, Property, Qt, QUrl, QTimer
|
||||||
from PySide6.QtWidgets import QFrame, QGraphicsDropShadowEffect, QVBoxLayout, QWidget, QStackedLayout, QLabel
|
from PySide6.QtWidgets import QFrame, QGraphicsDropShadowEffect, QVBoxLayout, QWidget, QStackedLayout, QLabel
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from portprotonqt.image_utils import load_pixmap_async, round_corners
|
from portprotonqt.image_utils import load_pixmap_async, round_corners
|
||||||
@@ -404,6 +404,13 @@ class GameCard(QFrame):
|
|||||||
self.favoriteLabel.setText("☆")
|
self.favoriteLabel.setText("☆")
|
||||||
self.favoriteLabel.setStyleSheet(self.theme.FAVORITE_LABEL_STYLE)
|
self.favoriteLabel.setStyleSheet(self.theme.FAVORITE_LABEL_STYLE)
|
||||||
|
|
||||||
|
parent = self.parent()
|
||||||
|
while parent:
|
||||||
|
if hasattr(parent, 'game_library_manager'):
|
||||||
|
QTimer.singleShot(0, parent.game_library_manager.update_game_grid) # type: ignore[attr-defined]
|
||||||
|
break
|
||||||
|
parent = parent.parent()
|
||||||
|
|
||||||
def toggle_favorite(self):
|
def toggle_favorite(self):
|
||||||
favorites = read_favorites()
|
favorites = read_favorites()
|
||||||
if self.is_favorite:
|
if self.is_favorite:
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import time
|
import time
|
||||||
import threading
|
import threading
|
||||||
import os
|
import os
|
||||||
|
import math
|
||||||
from typing import Protocol, cast
|
from typing import Protocol, cast
|
||||||
from evdev import InputDevice, InputEvent, ecodes, list_devices, ff
|
from evdev import InputDevice, InputEvent, UInput, ecodes, list_devices, ff
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from pyudev import Context, Monitor, Device, Devices
|
from pyudev import Context, Monitor, Device, Devices
|
||||||
from PySide6.QtWidgets import QWidget, QStackedWidget, QApplication, QScrollArea, QLineEdit, QDialog, QMenu, QComboBox, QListView, QMessageBox, QListWidget, QTableWidget, QAbstractItemView, QTableWidgetItem
|
from PySide6.QtWidgets import QWidget, QStackedWidget, QApplication, QScrollArea, QLineEdit, QDialog, QMenu, QComboBox, QListView, QMessageBox, QListWidget, QTableWidget, QAbstractItemView, QTableWidgetItem
|
||||||
@@ -15,6 +16,7 @@ from portprotonqt.game_card import GameCard
|
|||||||
from portprotonqt.config_utils import read_fullscreen_config, read_window_geometry, save_window_geometry, read_auto_fullscreen_gamepad, read_rumble_config, read_gamepad_type
|
from portprotonqt.config_utils import read_fullscreen_config, read_window_geometry, save_window_geometry, read_auto_fullscreen_gamepad, read_rumble_config, read_gamepad_type
|
||||||
from portprotonqt.dialogs import AddGameDialog
|
from portprotonqt.dialogs import AddGameDialog
|
||||||
from portprotonqt.virtual_keyboard import VirtualKeyboard
|
from portprotonqt.virtual_keyboard import VirtualKeyboard
|
||||||
|
import select
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -115,6 +117,31 @@ class InputManager(QObject):
|
|||||||
self.last_trigger_time = 0.0
|
self.last_trigger_time = 0.0
|
||||||
self.trigger_cooldown = 0.2
|
self.trigger_cooldown = 0.2
|
||||||
|
|
||||||
|
# Mouse emulation attributes
|
||||||
|
self.mouse_emulation_enabled = True # Enable by default as crutch for external apps
|
||||||
|
self.ui = None # UInput for virtual mouse
|
||||||
|
self.stick_x_raw = 0
|
||||||
|
self.stick_y_raw = 0
|
||||||
|
self.deadzone = 8000 # Deadzone for sticks
|
||||||
|
self.max_value = 32767 # Max stick value
|
||||||
|
self.sensitivity = 8.0 # Cursor sensitivity
|
||||||
|
self.scroll_accumulator = 0.0
|
||||||
|
self.scroll_sensitivity = 0.15 # Scroll sensitivity
|
||||||
|
self.scroll_threshold = 0.2 # Scroll threshold
|
||||||
|
self.last_update = time.time()
|
||||||
|
self.update_interval = 0.016 # ~60 FPS
|
||||||
|
self.emulation_active = False # Flag for external focus (updated in main thread)
|
||||||
|
|
||||||
|
# Focus check timer for emulation flag (runs in main thread)
|
||||||
|
self.focus_check_timer = QTimer(self)
|
||||||
|
self.focus_check_timer.timeout.connect(self._update_emulation_flag)
|
||||||
|
self.focus_check_timer.start(100) # Check every 100ms
|
||||||
|
|
||||||
|
logger.info("EMUL: Mouse emulation initialized (enabled=%s)", self.mouse_emulation_enabled)
|
||||||
|
|
||||||
|
if self.mouse_emulation_enabled:
|
||||||
|
self.enable_mouse_emulation()
|
||||||
|
|
||||||
# FileExplorer specific attributes
|
# FileExplorer specific attributes
|
||||||
self.file_explorer = None
|
self.file_explorer = None
|
||||||
self.original_button_handler = None
|
self.original_button_handler = None
|
||||||
@@ -151,6 +178,11 @@ class InputManager(QObject):
|
|||||||
# Initialize evdev + hotplug
|
# Initialize evdev + hotplug
|
||||||
self.init_gamepad()
|
self.init_gamepad()
|
||||||
|
|
||||||
|
def _update_emulation_flag(self):
|
||||||
|
"""Update emulation_active flag based on Qt app focus (main thread only)."""
|
||||||
|
active = QApplication.activeWindow()
|
||||||
|
self.emulation_active = (active is None) # True for external windows (e.g., winefile)
|
||||||
|
|
||||||
def _navigate_game_cards(self, container, tab_index: int, code: int, value: int) -> None:
|
def _navigate_game_cards(self, container, tab_index: int, code: int, value: int) -> None:
|
||||||
"""Common navigation logic for game cards in a container."""
|
"""Common navigation logic for game cards in a container."""
|
||||||
if container is None:
|
if container is None:
|
||||||
@@ -637,6 +669,116 @@ class InputManager(QObject):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in navigation repeat: {e}")
|
logger.error(f"Error in navigation repeat: {e}")
|
||||||
|
|
||||||
|
def enable_mouse_emulation(self):
|
||||||
|
"""Enable mouse emulation mode (creates virtual mouse device)."""
|
||||||
|
if self.mouse_emulation_enabled and self.ui is not None:
|
||||||
|
logger.debug("EMUL: Mouse emulation already enabled, skipping")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.info("EMUL: Attempting to create UInput virtual mouse...")
|
||||||
|
if not os.path.exists('/dev/uinput'):
|
||||||
|
logger.error("EMUL: /dev/uinput does not exist")
|
||||||
|
self.mouse_emulation_enabled = False
|
||||||
|
return
|
||||||
|
|
||||||
|
if not os.access('/dev/uinput', os.W_OK):
|
||||||
|
logger.error("EMUL: No write access to /dev/uinput")
|
||||||
|
self.mouse_emulation_enabled = False
|
||||||
|
return
|
||||||
|
|
||||||
|
self.ui = UInput({
|
||||||
|
ecodes.EV_KEY: [ecodes.BTN_LEFT, ecodes.BTN_RIGHT],
|
||||||
|
ecodes.EV_REL: [ecodes.REL_X, ecodes.REL_Y, ecodes.REL_WHEEL],
|
||||||
|
}, name="Virtual DPad Mouse")
|
||||||
|
|
||||||
|
self.mouse_emulation_enabled = True
|
||||||
|
logger.info("EMUL: Virtual mouse created successfully")
|
||||||
|
|
||||||
|
except PermissionError as e:
|
||||||
|
logger.error("EMUL: Permission denied for /dev/uinput: %s", e)
|
||||||
|
self.mouse_emulation_enabled = False
|
||||||
|
except Exception as ex:
|
||||||
|
logger.error(f"EMUL: Error creating virtual mouse: {ex}", exc_info=True)
|
||||||
|
self.mouse_emulation_enabled = False
|
||||||
|
|
||||||
|
def disable_mouse_emulation(self):
|
||||||
|
"""Disable mouse emulation mode (closes virtual mouse device)."""
|
||||||
|
logger.info("EMUL: Disabling mouse emulation...")
|
||||||
|
if self.ui:
|
||||||
|
try:
|
||||||
|
self.ui.close()
|
||||||
|
logger.info("EMUL: Virtual mouse closed")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("EMUL: Error closing virtual mouse: %s", e)
|
||||||
|
self.ui = None
|
||||||
|
self.mouse_emulation_enabled = False
|
||||||
|
self.stick_x_raw = 0
|
||||||
|
self.stick_y_raw = 0
|
||||||
|
self.scroll_accumulator = 0.0
|
||||||
|
|
||||||
|
def handle_scroll(self, raw_value):
|
||||||
|
"""Обработка прокрутки с правого стика Y"""
|
||||||
|
if not self.mouse_emulation_enabled or not self.emulation_active or not self.ui:
|
||||||
|
return
|
||||||
|
if abs(raw_value) < self.deadzone:
|
||||||
|
self.scroll_accumulator = 0.0
|
||||||
|
return
|
||||||
|
normalized = raw_value / self.max_value
|
||||||
|
self.scroll_accumulator += normalized * self.scroll_sensitivity
|
||||||
|
while abs(self.scroll_accumulator) >= self.scroll_threshold:
|
||||||
|
scroll_step = 1 if self.scroll_accumulator > 0 else -1
|
||||||
|
self.scroll_wheel(-scroll_step)
|
||||||
|
self.scroll_accumulator -= scroll_step * self.scroll_threshold
|
||||||
|
|
||||||
|
def update_mouse_position(self):
|
||||||
|
"""Постоянное обновление позиции мыши на основе состояния стика"""
|
||||||
|
if not self.ui or not self.emulation_active:
|
||||||
|
return
|
||||||
|
x = self.stick_x_raw
|
||||||
|
y = self.stick_y_raw
|
||||||
|
magnitude = math.sqrt(x * x + y * y)
|
||||||
|
if magnitude < self.deadzone:
|
||||||
|
return
|
||||||
|
norm_x = x / magnitude
|
||||||
|
norm_y = y / magnitude
|
||||||
|
adjusted_magnitude = max(0.0, min(1.0, (magnitude - self.deadzone) / (self.max_value - self.deadzone)))
|
||||||
|
adjusted_magnitude = math.pow(adjusted_magnitude, 1.5)
|
||||||
|
speed = adjusted_magnitude * self.sensitivity
|
||||||
|
dx = int(norm_x * speed)
|
||||||
|
dy = int(norm_y * speed)
|
||||||
|
if dx != 0 or dy != 0:
|
||||||
|
self.move_mouse(dx, dy)
|
||||||
|
|
||||||
|
def move_mouse(self, dx, dy):
|
||||||
|
"""Сдвиг системного курсора"""
|
||||||
|
if self.ui:
|
||||||
|
self.ui.write(ecodes.EV_REL, ecodes.REL_X, dx)
|
||||||
|
self.ui.write(ecodes.EV_REL, ecodes.REL_Y, dy)
|
||||||
|
self.ui.syn()
|
||||||
|
|
||||||
|
def scroll_wheel(self, steps):
|
||||||
|
"""Прокрутка колеса мыши"""
|
||||||
|
if self.ui:
|
||||||
|
self.ui.write(ecodes.EV_REL, ecodes.REL_WHEEL, steps)
|
||||||
|
self.ui.syn()
|
||||||
|
|
||||||
|
def click_left(self):
|
||||||
|
"""Клик левой кнопкой мыши"""
|
||||||
|
if self.ui:
|
||||||
|
self.ui.write(ecodes.EV_KEY, ecodes.BTN_LEFT, 1)
|
||||||
|
self.ui.syn()
|
||||||
|
self.ui.write(ecodes.EV_KEY, ecodes.BTN_LEFT, 0)
|
||||||
|
self.ui.syn()
|
||||||
|
|
||||||
|
def click_right(self):
|
||||||
|
"""Клик правой кнопкой мыши"""
|
||||||
|
if self.ui:
|
||||||
|
self.ui.write(ecodes.EV_KEY, ecodes.BTN_RIGHT, 1)
|
||||||
|
self.ui.syn()
|
||||||
|
self.ui.write(ecodes.EV_KEY, ecodes.BTN_RIGHT, 0)
|
||||||
|
self.ui.syn()
|
||||||
|
|
||||||
@Slot(bool)
|
@Slot(bool)
|
||||||
def handle_fullscreen_slot(self, enable: bool) -> None:
|
def handle_fullscreen_slot(self, enable: bool) -> None:
|
||||||
try:
|
try:
|
||||||
@@ -1473,7 +1615,6 @@ class InputManager(QObject):
|
|||||||
logger.error(f"Failed to start udev monitor: {e}")
|
logger.error(f"Failed to start udev monitor: {e}")
|
||||||
return
|
return
|
||||||
|
|
||||||
import select
|
|
||||||
fd = monitor.fileno()
|
fd = monitor.fileno()
|
||||||
poller = select.poll()
|
poller = select.poll()
|
||||||
poller.register(fd, select.POLLIN)
|
poller.register(fd, select.POLLIN)
|
||||||
@@ -1692,60 +1833,103 @@ class InputManager(QObject):
|
|||||||
logger.error(f"Error finding gamepad: {e}", exc_info=True)
|
logger.error(f"Error finding gamepad: {e}", exc_info=True)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def monitor_gamepad(self) -> None:
|
def monitor_gamepad(self) -> None:
|
||||||
try:
|
try:
|
||||||
if not self.gamepad:
|
while self.running:
|
||||||
return
|
current_time = time.time()
|
||||||
for event in self.gamepad.read_loop():
|
|
||||||
|
if self.gamepad:
|
||||||
|
try:
|
||||||
|
# Non-blocking read with short timeout
|
||||||
|
events = []
|
||||||
|
r, w, x = select.select([self.gamepad.fd], [], [], 0.001)
|
||||||
|
if r:
|
||||||
|
events = list(self.gamepad.read())
|
||||||
|
|
||||||
|
# Process events
|
||||||
|
for event in events:
|
||||||
if not self.running:
|
if not self.running:
|
||||||
break
|
break
|
||||||
if event.type not in (ecodes.EV_KEY, ecodes.EV_ABS):
|
|
||||||
continue
|
|
||||||
now = time.time()
|
|
||||||
|
|
||||||
# Проверка фокуса: игнорируем события, если окно не в фокусе
|
|
||||||
app = QApplication.instance()
|
|
||||||
active = QApplication.activeWindow()
|
|
||||||
if not app or not active:
|
|
||||||
continue
|
|
||||||
|
|
||||||
|
# UI signal handling (always, for internal app)
|
||||||
if event.type == ecodes.EV_KEY:
|
if event.type == ecodes.EV_KEY:
|
||||||
# Emit on both press (1) and release (0)
|
|
||||||
self.button_event.emit(event.code, event.value)
|
self.button_event.emit(event.code, event.value)
|
||||||
# Special handling for menu on press only
|
# Special handling for menu on press only
|
||||||
if event.value == 1 and event.code in BUTTONS['menu'] and not self._is_gamescope_session:
|
if event.value == 1 and event.code in BUTTONS['menu'] and not self._is_gamescope_session:
|
||||||
self.toggle_fullscreen.emit(not self._is_fullscreen)
|
self.toggle_fullscreen.emit(not self._is_fullscreen)
|
||||||
elif event.type == ecodes.EV_ABS:
|
elif event.type == ecodes.EV_ABS:
|
||||||
if event.code in {ecodes.ABS_Z, ecodes.ABS_RZ}:
|
if event.code in {ecodes.ABS_Z, ecodes.ABS_RZ}:
|
||||||
# Проверяем, достаточно ли времени прошло с последнего срабатывания
|
# Trigger handling for UI
|
||||||
if now - self.last_trigger_time < self.trigger_cooldown:
|
if current_time - self.last_trigger_time < self.trigger_cooldown:
|
||||||
continue
|
continue
|
||||||
if event.code == ecodes.ABS_Z: # LT/L2
|
if event.code == ecodes.ABS_Z: # LT/L2
|
||||||
if event.value > 128 and not self.lt_pressed:
|
if event.value > 128 and not self.lt_pressed:
|
||||||
self.lt_pressed = True
|
self.lt_pressed = True
|
||||||
self.button_event.emit(event.code, 1) # Emit as press
|
self.button_event.emit(event.code, 1)
|
||||||
self.last_trigger_time = now
|
self.last_trigger_time = current_time
|
||||||
elif event.value <= 128 and self.lt_pressed:
|
elif event.value <= 128 and self.lt_pressed:
|
||||||
self.lt_pressed = False
|
self.lt_pressed = False
|
||||||
self.button_event.emit(event.code, 0) # Emit as release
|
self.button_event.emit(event.code, 0)
|
||||||
elif event.code == ecodes.ABS_RZ: # RT/R2
|
elif event.code == ecodes.ABS_RZ: # RT/R2
|
||||||
if event.value > 128 and not self.rt_pressed:
|
if event.value > 128 and not self.rt_pressed:
|
||||||
self.rt_pressed = True
|
self.rt_pressed = True
|
||||||
self.button_event.emit(event.code, 1) # Emit as press
|
self.button_event.emit(event.code, 1)
|
||||||
self.last_trigger_time = now
|
self.last_trigger_time = current_time
|
||||||
elif event.value <= 128 and self.rt_pressed:
|
elif event.value <= 128 and self.rt_pressed:
|
||||||
self.rt_pressed = False
|
self.rt_pressed = False
|
||||||
self.button_event.emit(event.code, 0) # Emit as release
|
self.button_event.emit(event.code, 0)
|
||||||
else:
|
else:
|
||||||
self.dpad_moved.emit(event.code, event.value, now)
|
self.dpad_moved.emit(event.code, event.value, current_time)
|
||||||
|
|
||||||
|
# Mouse emulation (only for external windows)
|
||||||
|
if self.mouse_emulation_enabled and self.emulation_active:
|
||||||
|
if event.type == ecodes.EV_ABS:
|
||||||
|
if event.code == ecodes.ABS_HAT0X:
|
||||||
|
if event.value == -1:
|
||||||
|
self.move_mouse(-10, 0)
|
||||||
|
elif event.value == 1:
|
||||||
|
self.move_mouse(10, 0)
|
||||||
|
elif event.code == ecodes.ABS_HAT0Y:
|
||||||
|
if event.value == -1:
|
||||||
|
self.move_mouse(0, -10)
|
||||||
|
elif event.value == 1:
|
||||||
|
self.move_mouse(0, 10)
|
||||||
|
elif event.code == ecodes.ABS_X:
|
||||||
|
self.stick_x_raw = event.value
|
||||||
|
elif event.code == ecodes.ABS_Y:
|
||||||
|
self.stick_y_raw = event.value
|
||||||
|
elif event.code == ecodes.ABS_RY:
|
||||||
|
self.handle_scroll(event.value)
|
||||||
|
elif event.type == ecodes.EV_KEY:
|
||||||
|
if event.code in (ecodes.BTN_SOUTH, ecodes.BTN_A) and event.value == 1:
|
||||||
|
self.click_left()
|
||||||
|
elif event.code in (ecodes.BTN_EAST, ecodes.BTN_B) and event.value == 1:
|
||||||
|
self.click_right()
|
||||||
|
|
||||||
|
# Periodic mouse position update
|
||||||
|
if current_time - self.last_update >= self.update_interval:
|
||||||
|
self.update_mouse_position()
|
||||||
|
self.last_update = current_time
|
||||||
|
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
if e.errno == 19: # ENODEV: No such device
|
if e.errno == 19: # ENODEV
|
||||||
logger.info("Gamepad disconnected during event loop")
|
logger.info("Gamepad disconnected during monitoring")
|
||||||
else:
|
else:
|
||||||
logger.error(f"OSError in gamepad monitoring: {e}", exc_info=True)
|
logger.error(f"IOError in gamepad monitoring: {e}")
|
||||||
|
self.gamepad = None
|
||||||
|
self.stick_x_raw = 0
|
||||||
|
self.stick_y_raw = 0
|
||||||
|
self.scroll_accumulator = 0.0
|
||||||
|
break
|
||||||
|
except Exception as ex:
|
||||||
|
logger.error(f"Unexpected error in gamepad monitoring: {ex}")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
time.sleep(0.1)
|
||||||
|
if not self.running:
|
||||||
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in gamepad monitoring: {e}", exc_info=True)
|
logger.error(f"Error in gamepad monitoring thread: {e}", exc_info=True)
|
||||||
finally:
|
finally:
|
||||||
if self.gamepad:
|
if self.gamepad:
|
||||||
try:
|
try:
|
||||||
@@ -1760,6 +1944,12 @@ class InputManager(QObject):
|
|||||||
Корректное завершение работы с геймпадом и udev монитором.
|
Корректное завершение работы с геймпадом и udev монитором.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
# Mouse emulation cleanup
|
||||||
|
self.disable_mouse_emulation()
|
||||||
|
|
||||||
|
# Stop focus check timer
|
||||||
|
self.focus_check_timer.stop()
|
||||||
|
|
||||||
# Флаг для остановки udev monitor loop
|
# Флаг для остановки udev monitor loop
|
||||||
self.running = False
|
self.running = False
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import psutil
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from portprotonqt.logger import get_logger
|
from portprotonqt.logger import get_logger
|
||||||
from portprotonqt.dialogs import AddGameDialog, FileExplorer, WinetricksDialog
|
from portprotonqt.dialogs import AddGameDialog, FileExplorer, WinetricksDialog, ExeSettingsDialog
|
||||||
from portprotonqt.game_card import GameCard
|
from portprotonqt.game_card import GameCard
|
||||||
from portprotonqt.animations import DetailPageAnimations
|
from portprotonqt.animations import DetailPageAnimations
|
||||||
from portprotonqt.custom_widgets import ClickableLabel, AutoSizeButton, NavLabel, FlowLayout
|
from portprotonqt.custom_widgets import ClickableLabel, AutoSizeButton, NavLabel, FlowLayout
|
||||||
@@ -30,7 +30,7 @@ from portprotonqt.config_utils import (
|
|||||||
save_display_filter, save_proxy_config, read_proxy_config, read_fullscreen_config,
|
save_display_filter, save_proxy_config, read_proxy_config, read_fullscreen_config,
|
||||||
save_fullscreen_config, read_window_geometry, save_window_geometry, reset_config,
|
save_fullscreen_config, read_window_geometry, save_window_geometry, reset_config,
|
||||||
clear_cache, read_auto_fullscreen_gamepad, save_auto_fullscreen_gamepad, read_rumble_config, save_rumble_config, read_gamepad_type, save_gamepad_type, read_minimize_to_tray, save_minimize_to_tray,
|
clear_cache, read_auto_fullscreen_gamepad, save_auto_fullscreen_gamepad, read_rumble_config, save_rumble_config, read_gamepad_type, save_gamepad_type, read_minimize_to_tray, save_minimize_to_tray,
|
||||||
read_auto_card_size, save_auto_card_size
|
read_auto_card_size, save_auto_card_size, get_portproton_start_command
|
||||||
)
|
)
|
||||||
from portprotonqt.localization import _, get_egs_language, read_metadata_translations
|
from portprotonqt.localization import _, get_egs_language, read_metadata_translations
|
||||||
from portprotonqt.howlongtobeat_api import HowLongToBeat
|
from portprotonqt.howlongtobeat_api import HowLongToBeat
|
||||||
@@ -74,6 +74,7 @@ class MainWindow(QMainWindow):
|
|||||||
self.target_exe = None
|
self.target_exe = None
|
||||||
self.current_running_button = None
|
self.current_running_button = None
|
||||||
self.portproton_location = get_portproton_location()
|
self.portproton_location = get_portproton_location()
|
||||||
|
self.start_sh = get_portproton_start_command()
|
||||||
|
|
||||||
self.game_library_manager = GameLibraryManager(self, self.theme, None)
|
self.game_library_manager = GameLibraryManager(self, self.theme, None)
|
||||||
|
|
||||||
@@ -458,11 +459,11 @@ class MainWindow(QMainWindow):
|
|||||||
self.current_install_script = script_name
|
self.current_install_script = script_name
|
||||||
self.seen_progress = False
|
self.seen_progress = False
|
||||||
self.current_percent = 0.0
|
self.current_percent = 0.0
|
||||||
start_sh = os.path.join(self.portproton_location or "", "data", "scripts", "start.sh") if self.portproton_location else ""
|
start_sh = self.start_sh
|
||||||
if not os.path.exists(start_sh):
|
if not start_sh:
|
||||||
self.installing = False
|
self.installing = False
|
||||||
return
|
return
|
||||||
cmd = [start_sh, "cli", "--autoinstall", script_name]
|
cmd = start_sh + ["cli", "--autoinstall", script_name]
|
||||||
self.install_process = QProcess(self)
|
self.install_process = QProcess(self)
|
||||||
self.install_process.finished.connect(self.on_install_finished)
|
self.install_process.finished.connect(self.on_install_finished)
|
||||||
self.install_process.errorOccurred.connect(self.on_install_error)
|
self.install_process.errorOccurred.connect(self.on_install_error)
|
||||||
@@ -1424,12 +1425,10 @@ class MainWindow(QMainWindow):
|
|||||||
prefix = self.prefixCombo.currentText()
|
prefix = self.prefixCombo.currentText()
|
||||||
if not wine or not prefix:
|
if not wine or not prefix:
|
||||||
return
|
return
|
||||||
if not self.portproton_location:
|
if not self.portproton_location or not self.start_sh:
|
||||||
return
|
return
|
||||||
start_sh = os.path.join(self.portproton_location, "data", "scripts", "start.sh")
|
start_sh = self.start_sh
|
||||||
if not os.path.exists(start_sh):
|
cmd = start_sh + ["cli", cli_arg, wine, prefix]
|
||||||
return
|
|
||||||
cmd = [start_sh, "cli", cli_arg, wine, prefix]
|
|
||||||
|
|
||||||
# Показываем прогресс-бар перед запуском
|
# Показываем прогресс-бар перед запуском
|
||||||
self.wine_progress_bar.setVisible(True)
|
self.wine_progress_bar.setVisible(True)
|
||||||
@@ -1508,12 +1507,13 @@ class MainWindow(QMainWindow):
|
|||||||
QMessageBox.warning(self, _("Error"), f"Failed to launch tool: {error}")
|
QMessageBox.warning(self, _("Error"), f"Failed to launch tool: {error}")
|
||||||
|
|
||||||
def clear_prefix(self):
|
def clear_prefix(self):
|
||||||
"""Очистка префикса (позже удалить)."""
|
"""Очищает префикс"""
|
||||||
selected_prefix = self.prefixCombo.currentText()
|
selected_prefix = self.prefixCombo.currentText()
|
||||||
selected_wine = self.wineCombo.currentText()
|
selected_wine = self.wineCombo.currentText()
|
||||||
|
|
||||||
if not selected_prefix or not selected_wine:
|
if not selected_prefix or not selected_wine:
|
||||||
return
|
return
|
||||||
if not self.portproton_location:
|
if not self.portproton_location or not self.start_sh:
|
||||||
return
|
return
|
||||||
|
|
||||||
reply = QMessageBox.question(
|
reply = QMessageBox.question(
|
||||||
@@ -1526,98 +1526,35 @@ class MainWindow(QMainWindow):
|
|||||||
if reply != QMessageBox.StandardButton.Yes:
|
if reply != QMessageBox.StandardButton.Yes:
|
||||||
return
|
return
|
||||||
|
|
||||||
prefix_dir = os.path.join(self.portproton_location, "data", "prefixes", selected_prefix)
|
start_sh = self.start_sh
|
||||||
if not os.path.exists(prefix_dir):
|
|
||||||
|
self.wine_progress_bar.setVisible(True)
|
||||||
|
self.update_status_message.emit(_("Clearing prefix..."), 0)
|
||||||
|
|
||||||
|
self.clear_process = QProcess(self)
|
||||||
|
self.clear_process.finished.connect(lambda exitCode, exitStatus: self._on_clear_prefix_finished(exitCode))
|
||||||
|
self.clear_process.errorOccurred.connect(lambda error: self._on_clear_prefix_error(error))
|
||||||
|
cmd = start_sh + ["cli", "--clear_pfx", selected_wine, selected_prefix]
|
||||||
|
self.clear_process.start(cmd[0], cmd[1:])
|
||||||
|
|
||||||
|
if not self.clear_process.waitForStarted(5000):
|
||||||
|
self.wine_progress_bar.setVisible(False)
|
||||||
|
self.update_status_message.emit("", 0)
|
||||||
|
QMessageBox.warning(self, _("Error"), _("Failed to start prefix clear process."))
|
||||||
return
|
return
|
||||||
|
|
||||||
success = True
|
def _on_clear_prefix_finished(self, exitCode):
|
||||||
errors = []
|
self.wine_progress_bar.setVisible(False)
|
||||||
|
self.update_status_message.emit("", 0)
|
||||||
# Удаление файлов
|
if exitCode == 0:
|
||||||
files_to_remove = [
|
QMessageBox.information(self, _("Success"), _("Prefix cleared successfully."))
|
||||||
os.path.join(prefix_dir, "*.dot*"),
|
|
||||||
os.path.join(prefix_dir, "*.prog*"),
|
|
||||||
os.path.join(prefix_dir, ".wine_ver"),
|
|
||||||
os.path.join(prefix_dir, "system.reg"),
|
|
||||||
os.path.join(prefix_dir, "user.reg"),
|
|
||||||
os.path.join(prefix_dir, "userdef.reg"),
|
|
||||||
os.path.join(prefix_dir, "winetricks.log"),
|
|
||||||
os.path.join(prefix_dir, ".update-timestamp"),
|
|
||||||
os.path.join(prefix_dir, "drive_c", ".windows-serial"),
|
|
||||||
]
|
|
||||||
|
|
||||||
import glob
|
|
||||||
for pattern in files_to_remove:
|
|
||||||
if "*" in pattern: # Глобальный паттерн
|
|
||||||
matches = glob.glob(pattern)
|
|
||||||
for file_path in matches:
|
|
||||||
try:
|
|
||||||
if os.path.exists(file_path):
|
|
||||||
os.remove(file_path)
|
|
||||||
except Exception as e:
|
|
||||||
success = False
|
|
||||||
errors.append(str(e))
|
|
||||||
else: # Конкретный файл
|
|
||||||
try:
|
|
||||||
if os.path.exists(pattern):
|
|
||||||
os.remove(pattern)
|
|
||||||
except Exception as e:
|
|
||||||
success = False
|
|
||||||
errors.append(str(e))
|
|
||||||
|
|
||||||
# Удаление директорий
|
|
||||||
dirs_to_remove = [
|
|
||||||
os.path.join(prefix_dir, "drive_c", "windows"),
|
|
||||||
os.path.join(prefix_dir, "drive_c", "ProgramData", "Setup"),
|
|
||||||
os.path.join(prefix_dir, "drive_c", "ProgramData", "Windows"),
|
|
||||||
os.path.join(prefix_dir, "drive_c", "ProgramData", "WindowsTask"),
|
|
||||||
os.path.join(prefix_dir, "drive_c", "ProgramData", "Package Cache"),
|
|
||||||
os.path.join(prefix_dir, "drive_c", "users", "Public", "Local Settings", "Application Data", "Microsoft"),
|
|
||||||
os.path.join(prefix_dir, "drive_c", "users", "Public", "Local Settings", "Application Data", "Temp"),
|
|
||||||
os.path.join(prefix_dir, "drive_c", "users", "Public", "Local Settings", "Temporary Internet Files"),
|
|
||||||
os.path.join(prefix_dir, "drive_c", "users", "Public", "Application Data", "Microsoft"),
|
|
||||||
os.path.join(prefix_dir, "drive_c", "users", "Public", "Application Data", "wine_gecko"),
|
|
||||||
os.path.join(prefix_dir, "drive_c", "users", "Public", "Temp"),
|
|
||||||
os.path.join(prefix_dir, "drive_c", "users", "steamuser", "Local Settings", "Application Data", "Microsoft"),
|
|
||||||
os.path.join(prefix_dir, "drive_c", "users", "steamuser", "Local Settings", "Application Data", "Temp"),
|
|
||||||
os.path.join(prefix_dir, "drive_c", "users", "steamuser", "Local Settings", "Temporary Internet Files"),
|
|
||||||
os.path.join(prefix_dir, "drive_c", "users", "steamuser", "Application Data", "Microsoft"),
|
|
||||||
os.path.join(prefix_dir, "drive_c", "users", "steamuser", "Application Data", "wine_gecko"),
|
|
||||||
os.path.join(prefix_dir, "drive_c", "users", "steamuser", "Temp"),
|
|
||||||
os.path.join(prefix_dir, "drive_c", "Program Files", "Internet Explorer"),
|
|
||||||
os.path.join(prefix_dir, "drive_c", "Program Files", "Windows Media Player"),
|
|
||||||
os.path.join(prefix_dir, "drive_c", "Program Files", "Windows NT"),
|
|
||||||
os.path.join(prefix_dir, "drive_c", "Program Files (x86)", "Internet Explorer"),
|
|
||||||
os.path.join(prefix_dir, "drive_c", "Program Files (x86)", "Windows Media Player"),
|
|
||||||
os.path.join(prefix_dir, "drive_c", "Program Files (x86)", "Windows NT"),
|
|
||||||
]
|
|
||||||
|
|
||||||
import shutil
|
|
||||||
for dir_path in dirs_to_remove:
|
|
||||||
try:
|
|
||||||
if os.path.exists(dir_path):
|
|
||||||
shutil.rmtree(dir_path)
|
|
||||||
except Exception as e:
|
|
||||||
success = False
|
|
||||||
errors.append(str(e))
|
|
||||||
|
|
||||||
tmp_path = os.path.join(self.portproton_location, "data", "tmp")
|
|
||||||
if os.path.exists(tmp_path):
|
|
||||||
import glob
|
|
||||||
bin_files = glob.glob(os.path.join(tmp_path, "*.bin"))
|
|
||||||
foz_files = glob.glob(os.path.join(tmp_path, "*.foz"))
|
|
||||||
for file_path in bin_files + foz_files:
|
|
||||||
try:
|
|
||||||
os.remove(file_path)
|
|
||||||
except Exception as e:
|
|
||||||
success = False
|
|
||||||
errors.append(str(e))
|
|
||||||
|
|
||||||
if success:
|
|
||||||
QMessageBox.information(self, _("Success"), _("Prefix '{}' cleared successfully.").format(selected_prefix))
|
|
||||||
else:
|
else:
|
||||||
error_msg = _("Prefix '{}' cleared with errors:\n{}").format(selected_prefix, "\n".join(errors[:5]))
|
QMessageBox.warning(self, _("Error"), _("Prefix clear failed with exit code {}.").format(exitCode))
|
||||||
QMessageBox.warning(self, _("Warning"), error_msg)
|
|
||||||
|
def _on_clear_prefix_error(self, error):
|
||||||
|
self.wine_progress_bar.setVisible(False)
|
||||||
|
self.update_status_message.emit("", 0)
|
||||||
|
QMessageBox.warning(self, _("Error"), _("Failed to run clear prefix command: {}").format(error))
|
||||||
|
|
||||||
def create_prefix_backup(self):
|
def create_prefix_backup(self):
|
||||||
selected_prefix = self.prefixCombo.currentText()
|
selected_prefix = self.prefixCombo.currentText()
|
||||||
@@ -1629,14 +1566,12 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
def _perform_backup(self, backup_dir, prefix_name):
|
def _perform_backup(self, backup_dir, prefix_name):
|
||||||
os.makedirs(backup_dir, exist_ok=True)
|
os.makedirs(backup_dir, exist_ok=True)
|
||||||
if not self.portproton_location:
|
if not self.portproton_location or not self.start_sh:
|
||||||
return
|
|
||||||
start_sh = os.path.join(self.portproton_location, "data", "scripts", "start.sh")
|
|
||||||
if not os.path.exists(start_sh):
|
|
||||||
return
|
return
|
||||||
|
start_sh = self.start_sh
|
||||||
self.backup_process = QProcess(self)
|
self.backup_process = QProcess(self)
|
||||||
self.backup_process.finished.connect(lambda exitCode, exitStatus: self._on_backup_finished(exitCode))
|
self.backup_process.finished.connect(lambda exitCode, exitStatus: self._on_backup_finished(exitCode))
|
||||||
cmd = [start_sh, "--backup-prefix", prefix_name, backup_dir]
|
cmd = start_sh + ["--backup-prefix", prefix_name, backup_dir]
|
||||||
self.backup_process.start(cmd[0], cmd[1:])
|
self.backup_process.start(cmd[0], cmd[1:])
|
||||||
if not self.backup_process.waitForStarted():
|
if not self.backup_process.waitForStarted():
|
||||||
QMessageBox.warning(self, _("Error"), _("Failed to start backup process."))
|
QMessageBox.warning(self, _("Error"), _("Failed to start backup process."))
|
||||||
@@ -1649,14 +1584,12 @@ class MainWindow(QMainWindow):
|
|||||||
def _perform_restore(self, file_path):
|
def _perform_restore(self, file_path):
|
||||||
if not file_path or not os.path.exists(file_path):
|
if not file_path or not os.path.exists(file_path):
|
||||||
return
|
return
|
||||||
if not self.portproton_location:
|
if not self.portproton_location or not self.start_sh:
|
||||||
return
|
|
||||||
start_sh = os.path.join(self.portproton_location, "data", "scripts", "start.sh")
|
|
||||||
if not os.path.exists(start_sh):
|
|
||||||
return
|
return
|
||||||
|
start_sh = self.start_sh
|
||||||
self.restore_process = QProcess(self)
|
self.restore_process = QProcess(self)
|
||||||
self.restore_process.finished.connect(lambda exitCode, exitStatus: self._on_restore_finished(exitCode))
|
self.restore_process.finished.connect(lambda exitCode, exitStatus: self._on_restore_finished(exitCode))
|
||||||
cmd = [start_sh, "--restore-prefix", file_path]
|
cmd = start_sh + ["--restore-prefix", file_path]
|
||||||
self.restore_process.start(cmd[0], cmd[1:])
|
self.restore_process.start(cmd[0], cmd[1:])
|
||||||
if not self.restore_process.waitForStarted():
|
if not self.restore_process.waitForStarted():
|
||||||
QMessageBox.warning(self, _("Error"), _("Failed to start restore process."))
|
QMessageBox.warning(self, _("Error"), _("Failed to start restore process."))
|
||||||
@@ -2326,6 +2259,14 @@ class MainWindow(QMainWindow):
|
|||||||
def darkenColor(self, color, factor=200):
|
def darkenColor(self, color, factor=200):
|
||||||
return color.darker(factor)
|
return color.darker(factor)
|
||||||
|
|
||||||
|
def open_exe_settings(self, exe_path):
|
||||||
|
"""Open the ExeSettingsDialog for the given executable."""
|
||||||
|
if not os.path.exists(exe_path):
|
||||||
|
QMessageBox.warning(self, _("Error"), _("Executable not found: {0}").format(exe_path))
|
||||||
|
return
|
||||||
|
dialog = ExeSettingsDialog(self, self.theme, exe_path)
|
||||||
|
dialog.exec()
|
||||||
|
|
||||||
def openGameDetailPage(self, name, description, cover_path=None, appid="", exec_line="", controller_support="", last_launch="", formatted_playtime="", protondb_tier="", game_source="", anticheat_status=""):
|
def openGameDetailPage(self, name, description, cover_path=None, appid="", exec_line="", controller_support="", last_launch="", formatted_playtime="", protondb_tier="", game_source="", anticheat_status=""):
|
||||||
detailPage = QWidget()
|
detailPage = QWidget()
|
||||||
self._animations = {}
|
self._animations = {}
|
||||||
@@ -2628,8 +2569,6 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
clear_layout(hltbLayout)
|
clear_layout(hltbLayout)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
has_data = False
|
has_data = False
|
||||||
|
|
||||||
if main_story_time is not None:
|
if main_story_time is not None:
|
||||||
@@ -2713,6 +2652,14 @@ class MainWindow(QMainWindow):
|
|||||||
playButton.clicked.connect(lambda: self.toggleGame(exec_line, playButton))
|
playButton.clicked.connect(lambda: self.toggleGame(exec_line, playButton))
|
||||||
detailsLayout.addWidget(playButton, alignment=Qt.AlignmentFlag.AlignLeft)
|
detailsLayout.addWidget(playButton, alignment=Qt.AlignmentFlag.AlignLeft)
|
||||||
|
|
||||||
|
# Settings button
|
||||||
|
settings_icon = self.theme_manager.get_icon("settings")
|
||||||
|
settings_button = AutoSizeButton(_("Settings"), icon=settings_icon)
|
||||||
|
settings_button.setFixedSize(120, 40)
|
||||||
|
settings_button.setStyleSheet(self.theme.PLAY_BUTTON_STYLE)
|
||||||
|
settings_button.clicked.connect(lambda: self.open_exe_settings(file_to_check))
|
||||||
|
detailsLayout.addWidget(settings_button, alignment=Qt.AlignmentFlag.AlignLeft)
|
||||||
|
|
||||||
contentFrameLayout.addWidget(detailsWidget)
|
contentFrameLayout.addWidget(detailsWidget)
|
||||||
mainLayout.addStretch()
|
mainLayout.addStretch()
|
||||||
|
|
||||||
@@ -2935,10 +2882,7 @@ class MainWindow(QMainWindow):
|
|||||||
env_vars = os.environ.copy()
|
env_vars = os.environ.copy()
|
||||||
env_vars['LEGENDARY_CONFIG_PATH'] = self.legendary_config_path
|
env_vars['LEGENDARY_CONFIG_PATH'] = self.legendary_config_path
|
||||||
|
|
||||||
wrapper = "flatpak run ru.linux_gaming.PortProton"
|
wrapper = self.start_sh or ""
|
||||||
if self.portproton_location is not None and ".var" not in self.portproton_location:
|
|
||||||
start_sh = os.path.join(self.portproton_location, "data", "scripts", "start.sh")
|
|
||||||
wrapper = start_sh
|
|
||||||
|
|
||||||
cmd = [wrapper, game_exe]
|
cmd = [wrapper, game_exe]
|
||||||
|
|
||||||
@@ -3032,13 +2976,6 @@ class MainWindow(QMainWindow):
|
|||||||
exe_name = os.path.splitext(current_exe)[0]
|
exe_name = os.path.splitext(current_exe)[0]
|
||||||
env_vars = os.environ.copy()
|
env_vars = os.environ.copy()
|
||||||
|
|
||||||
if entry_exec_split[0] == "env" and len(entry_exec_split) > 1 and 'data/scripts/start.sh' in entry_exec_split[1]:
|
|
||||||
env_vars['START_FROM_STEAM'] = '1'
|
|
||||||
env_vars['PROCESS_LOG'] = '1'
|
|
||||||
elif entry_exec_split[0] == "flatpak":
|
|
||||||
env_vars['START_FROM_STEAM'] = '1'
|
|
||||||
env_vars['PROCESS_LOG'] = '1'
|
|
||||||
|
|
||||||
# Запускаем игру
|
# Запускаем игру
|
||||||
try:
|
try:
|
||||||
process = subprocess.Popen(entry_exec_split, env=env_vars, shell=False, preexec_fn=os.setsid)
|
process = subprocess.Popen(entry_exec_split, env=env_vars, shell=False, preexec_fn=os.setsid)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from portprotonqt.logger import get_logger
|
|||||||
from portprotonqt.localization import get_steam_language
|
from portprotonqt.localization import get_steam_language
|
||||||
from portprotonqt.downloader import Downloader
|
from portprotonqt.downloader import Downloader
|
||||||
from portprotonqt.dialogs import generate_thumbnail
|
from portprotonqt.dialogs import generate_thumbnail
|
||||||
from portprotonqt.config_utils import get_portproton_location
|
from portprotonqt.config_utils import get_portproton_location, get_portproton_start_command
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
@@ -1004,7 +1004,8 @@ def add_to_steam(game_name: str, exec_line: str, cover_path: str) -> tuple[bool,
|
|||||||
return (False, f"Executable file not found: {exe_path}")
|
return (False, f"Executable file not found: {exe_path}")
|
||||||
|
|
||||||
portproton_dir = get_portproton_location()
|
portproton_dir = get_portproton_location()
|
||||||
if not portproton_dir:
|
start_sh = get_portproton_start_command()
|
||||||
|
if not portproton_dir or not start_sh:
|
||||||
logger.error("PortProton directory not found")
|
logger.error("PortProton directory not found")
|
||||||
return (False, "PortProton directory not found")
|
return (False, "PortProton directory not found")
|
||||||
|
|
||||||
@@ -1013,17 +1014,12 @@ def add_to_steam(game_name: str, exec_line: str, cover_path: str) -> tuple[bool,
|
|||||||
|
|
||||||
safe_game_name = re.sub(r'[<>:"/\\|?*]', '_', game_name.strip())
|
safe_game_name = re.sub(r'[<>:"/\\|?*]', '_', game_name.strip())
|
||||||
script_path = os.path.join(steam_scripts_dir, f"{safe_game_name}.sh")
|
script_path = os.path.join(steam_scripts_dir, f"{safe_game_name}.sh")
|
||||||
start_sh_path = os.path.join(portproton_dir, "data", "scripts", "start.sh")
|
|
||||||
|
|
||||||
if not os.path.exists(start_sh_path):
|
|
||||||
logger.error(f"start.sh not found at {start_sh_path}")
|
|
||||||
return (False, f"start.sh not found at {start_sh_path}")
|
|
||||||
|
|
||||||
if not os.path.exists(script_path):
|
if not os.path.exists(script_path):
|
||||||
script_content = f"""#!/usr/bin/env bash
|
script_content = f"""#!/usr/bin/env bash
|
||||||
export LD_PRELOAD=
|
export LD_PRELOAD=
|
||||||
export START_FROM_STEAM=1
|
export START_FROM_STEAM=1
|
||||||
"{start_sh_path}" "{exe_path}" "$@"
|
"{start_sh}" "{exe_path}" "$@"
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
with open(script_path, "w", encoding="utf-8") as f:
|
with open(script_path, "w", encoding="utf-8") as f:
|
||||||
|
|||||||
1
portprotonqt/themes/standart/images/icons/settings.svg
Normal file
1
portprotonqt/themes/standart/images/icons/settings.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m8.0005 1c-0.38761 0-0.77522 0.0327-1.1588 0.0979-0.16351 0.0281-0.30273 0.13627-0.37209 0.28935l-0.39088 0.86264c-0.49378 0.16682-0.96454 0.39759-1.4007 0.68616 2.5e-4 0-0.90672-0.2272-0.90672-0.2272-0.161-0.0403-0.33098 3e-3 -0.45442 0.11569-0.57867 0.5285-1.0672 1.1514-1.4451 1.8432-0.0804 0.14721-0.0841 0.32549-0.01 0.47628l0.41938 0.84865c-0.17954 0.49666-0.29567 1.0147-0.346 1.5417l-0.73995 0.57946c-0.13121 0.10289-0.20407 0.26514-0.19431 0.4335 0.0453 0.78981 0.21961 1.5666 0.51558 2.2983 0.0631 0.15587 0.1978 0.27003 0.36005 0.30467l0.91397 0.19559c0.26993 0.45234 0.59572 0.86802 0.96931 1.2363l-0.0161 0.94973c-3e-3 0.16861 0.0766 0.32755 0.21183 0.42484 0.63551 0.45642 1.3414 0.80207 2.0884 1.0229 0.15926 0.0471 0.33077 0.0109 0.45872-0.0963l0.72016-0.60485c0.51582 0.0674 1.0384 0.0674 1.5544 0l0.72016 0.60485c0.12796 0.10722 0.29946 0.14343 0.45872 0.0963 0.74693-0.22083 1.4528-0.56648 2.0883-1.0229 0.13521-0.0973 0.21465-0.25623 0.21189-0.42484l-0.0161-0.94973c0.37359-0.36829 0.69939-0.78372 0.96932-1.2363l0.91396-0.19559c0.16226-0.0347 0.29695-0.1488 0.36005-0.30467 0.29597-0.73174 0.47026-1.5085 0.51558-2.2983 0.01-0.16836-0.0631-0.33061-0.1943-0.4335l-0.73996-0.57946c-0.0501-0.52671-0.16652-1.045-0.34606-1.5417l0.41944-0.84865c0.0746-0.15079 0.0709-0.32907-0.01-0.47628-0.37785-0.69176-0.86638-1.3147-1.445-1.8432-0.12345-0.11258-0.29343-0.15594-0.45443-0.11569l-0.90697 0.2272c-0.43594-0.28857-0.9067-0.51908-1.4005-0.68616l-0.39088-0.86264c-0.0694-0.15308-0.20858-0.26132-0.37209-0.28935-0.38361-0.0653-0.77121-0.0979-1.1588-0.0979zm0 4.1365a2.8152 2.8635 0 0 1 2.8152 2.8636 2.8152 2.8635 0 0 1-2.8152 2.8635 2.8152 2.8635 0 0 1-2.8152-2.8635 2.8152 2.8635 0 0 1 2.8152-2.8636z" fill="#fff" stroke-width=".25254"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -8,7 +8,7 @@
|
|||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
"pre-commit": {
|
"pre-commit": {
|
||||||
"enabled": true
|
"enabled": false
|
||||||
},
|
},
|
||||||
"packageRules": [
|
"packageRules": [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user