Compare commits
2 Commits
Author | SHA1 | Date | |
---|---|---|---|
0a4284191b | |||
d05f2fccd6 |
@@ -94,7 +94,7 @@ jobs:
|
|||||||
name: Build Arch Package
|
name: Build Arch Package
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
container:
|
container:
|
||||||
image: archlinux:base-devel@sha256:06ab929f935145dd65994a89dd06651669ea28d43c812f3e24de990978511821
|
image: archlinux:base-devel@sha256:0589aa8f31d8f64c630a2d1cc0b4c3847a1a63c988abd63d78d3c9bd94764f64
|
||||||
volumes:
|
volumes:
|
||||||
- /usr:/usr-host
|
- /usr:/usr-host
|
||||||
- /opt:/opt-host
|
- /opt:/opt-host
|
||||||
|
@@ -8,7 +8,7 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
# Common version, will be used for tagging the release
|
# Common version, will be used for tagging the release
|
||||||
VERSION: 0.1.7
|
VERSION: 0.1.6
|
||||||
PKGDEST: "/tmp/portprotonqt"
|
PKGDEST: "/tmp/portprotonqt"
|
||||||
PACKAGE: "portprotonqt"
|
PACKAGE: "portprotonqt"
|
||||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
@@ -180,8 +180,6 @@ jobs:
|
|||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
uses: https://gitea.com/actions/gitea-release-action@v1
|
uses: https://gitea.com/actions/gitea-release-action@v1
|
||||||
env:
|
|
||||||
NODE_OPTIONS: '--experimental-fetch' # if nodejs < 18
|
|
||||||
with:
|
with:
|
||||||
body_path: changelog.txt
|
body_path: changelog.txt
|
||||||
token: ${{ env.GITEA_TOKEN }}
|
token: ${{ env.GITEA_TOKEN }}
|
||||||
|
@@ -138,7 +138,7 @@ jobs:
|
|||||||
needs: changes
|
needs: changes
|
||||||
if: needs.changes.outputs.arch == 'true' || github.event_name == 'workflow_dispatch'
|
if: needs.changes.outputs.arch == 'true' || github.event_name == 'workflow_dispatch'
|
||||||
container:
|
container:
|
||||||
image: archlinux:base-devel@sha256:06ab929f935145dd65994a89dd06651669ea28d43c812f3e24de990978511821
|
image: archlinux:base-devel@sha256:0589aa8f31d8f64c630a2d1cc0b4c3847a1a63c988abd63d78d3c9bd94764f64
|
||||||
volumes:
|
volumes:
|
||||||
- /usr:/usr-host
|
- /usr:/usr-host
|
||||||
- /opt:/opt-host
|
- /opt:/opt-host
|
||||||
|
@@ -8,7 +8,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
renovate:
|
renovate:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container: ghcr.io/renovatebot/renovate:latest@sha256:17c8966ef38fc361e108a550ffe2dcedf73e846f9975a974aea3d48c66b107a6
|
container: ghcr.io/renovatebot/renovate:latest@sha256:dd5721b9a686a40d81687643e4b71b82a0ca31fb653fd727538af69104fd388d
|
||||||
steps:
|
steps:
|
||||||
- uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
- uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||||
|
|
||||||
|
@@ -16,7 +16,7 @@ repos:
|
|||||||
- 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.0
|
rev: v0.13.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff-check
|
- id: ruff-check
|
||||||
|
|
||||||
|
17
CHANGELOG.md
@@ -3,34 +3,20 @@
|
|||||||
Все заметные изменения в этом проекте фиксируются в этом файле.
|
Все заметные изменения в этом проекте фиксируются в этом файле.
|
||||||
Формат основан на [Keep a Changelog](https://keepachangelog.com/) и придерживается принципов [Semantic Versioning](https://semver.org/).
|
Формат основан на [Keep a Changelog](https://keepachangelog.com/) и придерживается принципов [Semantic Versioning](https://semver.org/).
|
||||||
|
|
||||||
## [0.1.7] - 2025-10-12
|
## [Unreleased]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Возможность скроллинга библиотеки мышью или пальцем
|
- Возможность скроллинга библиотеки мышью или пальцем
|
||||||
- Импорт и экспорт бекапа префикса
|
|
||||||
- Диалог для управление Winetricks
|
|
||||||
- Кнопки для удаления префикса, wine или proton
|
|
||||||
- Все настройки Wine с оригинального PortProton
|
|
||||||
- Виртуальная клавиатура в диалог добавления игры и поиск по библиотеке и автоустановках
|
|
||||||
- Вкладка автоустановок
|
|
||||||
- В заголовке окна теперь отображается версия приложения и хеш коммита если запуск идёт с гита
|
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Проведён рефакторинг и оптимизация всего что связано с карточками и библиотекой игр
|
- Проведён рефакторинг и оптимизация всего что связано с карточками и библиотекой игр
|
||||||
- В диалог выбора файлов в режиме directory_only (при выборе куда сохранить бекап префикса) добавлена кнопка ./ обозначающая нынешнюю папку
|
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Исправлен вылет диалога выбора файлов при выборе обложки если в папке более сотни изображений
|
- Исправлен вылет диалога выбора файлов при выборе обложки если в папке более сотни изображений
|
||||||
- Исправлено зависание при добавлении или удалении игры в Wayland
|
- Исправлено зависание при добавлении или удалении игры в Wayland
|
||||||
- Исправлено зависание при поиске игр
|
- Исправлено зависание при поиске игр
|
||||||
- Исправлено ошибочное присвоение ID игры с названием "GAME", возникавшее, если исполняемый файл находился в подпапке `game/` (часто встречается у игр на Unity)
|
|
||||||
- Исправлена ошибка из-за которой подсказки по управлению снизу и сверху могли не совпадать с друг другом, из-за чего возле вкладок были стрелки клавиатуры, а снизу кнопки геймпада
|
|
||||||
- Исправлен выход из полноэкранного режима при отключении геймпада подключённого по USB даже если настройка "Режим полноэкранного отображения приложения при подключении геймпада" выключена
|
|
||||||
- При сохранении настроек теперь не меняется размер окна
|
|
||||||
|
|
||||||
### Contributors
|
### Contributors
|
||||||
- @wmigor (Igor Akulov)
|
|
||||||
- @Vector_null
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -53,7 +39,6 @@
|
|||||||
|
|
||||||
### Contributors
|
### Contributors
|
||||||
- @wmigor (Igor Akulov)
|
- @wmigor (Igor Akulov)
|
||||||
- @Vector_null
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@@ -36,7 +36,7 @@ AppDir:
|
|||||||
id: ru.linux_gaming.PortProtonQt
|
id: ru.linux_gaming.PortProtonQt
|
||||||
name: PortProtonQt
|
name: PortProtonQt
|
||||||
icon: ru.linux_gaming.PortProtonQt
|
icon: ru.linux_gaming.PortProtonQt
|
||||||
version: 0.1.7
|
version: 0.1.6
|
||||||
exec: usr/bin/python3
|
exec: usr/bin/python3
|
||||||
exec_args: "-m portprotonqt.app $@"
|
exec_args: "-m portprotonqt.app $@"
|
||||||
apt:
|
apt:
|
||||||
@@ -54,11 +54,6 @@ AppDir:
|
|||||||
- libxcb-cursor0
|
- libxcb-cursor0
|
||||||
- libimage-exiftool-perl
|
- libimage-exiftool-perl
|
||||||
- xdg-utils
|
- xdg-utils
|
||||||
- cabextract
|
|
||||||
- curl
|
|
||||||
- 7zip
|
|
||||||
- unzip
|
|
||||||
- unrar
|
|
||||||
exclude:
|
exclude:
|
||||||
- "*-doc"
|
- "*-doc"
|
||||||
- "*-man"
|
- "*-man"
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
pkgname=portprotonqt
|
pkgname=portprotonqt
|
||||||
pkgver=0.1.7
|
pkgver=0.1.6
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Modern GUI for managing and launching games from PortProton, Steam, and Epic Games Store"
|
pkgdesc="Modern GUI for managing and launching games from PortProton, Steam, and Epic Games Store"
|
||||||
arch=('any')
|
arch=('any')
|
||||||
url="https://git.linux-gaming.ru/Boria138/PortProtonQt"
|
url="https://git.linux-gaming.ru/Boria138/PortProtonQt"
|
||||||
license=('GPL-3.0')
|
license=('GPL-3.0')
|
||||||
depends=('python-numpy' 'python-requests' 'python-babel' 'python-evdev' 'python-pyudev' 'python-orjson'
|
depends=('python-numpy' 'python-requests' 'python-babel' 'python-evdev' 'python-pyudev' 'python-orjson'
|
||||||
'python-psutil' 'python-tqdm' 'python-vdf' 'pyside6' 'icoextract' 'python-pillow' 'perl-image-exiftool' 'xdg-utils' 'python-beautifulsoup4' 'python-websocket-client' 'cabextract' 'unzip' 'curl' 'unrar')
|
'python-psutil' 'python-tqdm' 'python-vdf' 'pyside6' 'icoextract' 'python-pillow' 'perl-image-exiftool' 'xdg-utils' 'python-beautifulsoup4' 'python-websocket-client')
|
||||||
makedepends=('python-'{'build','installer','setuptools','wheel'})
|
makedepends=('python-'{'build','installer','setuptools','wheel'})
|
||||||
source=("git+https://git.linux-gaming.ru/Boria138/PortProtonQt#tag=v$pkgver")
|
source=("git+https://git.linux-gaming.ru/Boria138/PortProtonQt#tag=v$pkgver")
|
||||||
sha256sums=('SKIP')
|
sha256sums=('SKIP')
|
||||||
|
@@ -6,7 +6,7 @@ arch=('any')
|
|||||||
url="https://git.linux-gaming.ru/Boria138/PortProtonQt"
|
url="https://git.linux-gaming.ru/Boria138/PortProtonQt"
|
||||||
license=('GPL-3.0')
|
license=('GPL-3.0')
|
||||||
depends=('python-numpy' 'python-requests' 'python-babel' 'python-evdev' 'python-pyudev' 'python-orjson'
|
depends=('python-numpy' 'python-requests' 'python-babel' 'python-evdev' 'python-pyudev' 'python-orjson'
|
||||||
'python-psutil' 'python-tqdm' 'python-vdf' 'pyside6' 'icoextract' 'python-pillow' 'perl-image-exiftool' 'xdg-utils' 'python-beautifulsoup4' 'python-websocket-client' 'cabextract' 'unzip' 'curl' 'unrar')
|
'python-psutil' 'python-tqdm' 'python-vdf' 'pyside6' 'icoextract' 'python-pillow' 'perl-image-exiftool' 'xdg-utils' 'python-beautifulsoup4' 'python-websocket-client')
|
||||||
makedepends=('python-'{'build','installer','setuptools','wheel'})
|
makedepends=('python-'{'build','installer','setuptools','wheel'})
|
||||||
source=("git+https://git.linux-gaming.ru/Boria138/PortProtonQt.git")
|
source=("git+https://git.linux-gaming.ru/Boria138/PortProtonQt.git")
|
||||||
sha256sums=('SKIP')
|
sha256sums=('SKIP')
|
||||||
|
@@ -46,11 +46,6 @@ Requires: python3-pillow
|
|||||||
Requires: perl-Image-ExifTool
|
Requires: perl-Image-ExifTool
|
||||||
Requires: xdg-utils
|
Requires: xdg-utils
|
||||||
Requires: python3-beautifulsoup4
|
Requires: python3-beautifulsoup4
|
||||||
Requires: cabextract
|
|
||||||
Requires: gzip
|
|
||||||
Requires: unzip
|
|
||||||
Requires: curl
|
|
||||||
Requires: unrar
|
|
||||||
|
|
||||||
%description -n python3-%{pypi_name}-git
|
%description -n python3-%{pypi_name}-git
|
||||||
This application provides a sleek, intuitive graphical interface for managing and launching games from PortProton, Steam, and Epic Games Store. It consolidates your game libraries into a single, user-friendly hub for seamless navigation and organization. Its lightweight structure and cross-platform support deliver a cohesive gaming experience, eliminating the need for multiple launchers. Unique PortProton integration enhances Linux gaming, enabling effortless play of Windows-based titles with minimal setup.
|
This application provides a sleek, intuitive graphical interface for managing and launching games from PortProton, Steam, and Epic Games Store. It consolidates your game libraries into a single, user-friendly hub for seamless navigation and organization. Its lightweight structure and cross-platform support deliver a cohesive gaming experience, eliminating the need for multiple launchers. Unique PortProton integration enhances Linux gaming, enabling effortless play of Windows-based titles with minimal setup.
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
%global pypi_name portprotonqt
|
%global pypi_name portprotonqt
|
||||||
%global pypi_version 0.1.7
|
%global pypi_version 0.1.6
|
||||||
%global oname PortProtonQt
|
%global oname PortProtonQt
|
||||||
%global _python_no_extras_requires 1
|
%global _python_no_extras_requires 1
|
||||||
|
|
||||||
@@ -43,11 +43,6 @@ Requires: python3-pillow
|
|||||||
Requires: perl-Image-ExifTool
|
Requires: perl-Image-ExifTool
|
||||||
Requires: xdg-utils
|
Requires: xdg-utils
|
||||||
Requires: python3-beautifulsoup4
|
Requires: python3-beautifulsoup4
|
||||||
Requires: cabextract
|
|
||||||
Requires: gzip
|
|
||||||
Requires: unzip
|
|
||||||
Requires: curl
|
|
||||||
Requires: unrar
|
|
||||||
|
|
||||||
%description -n python3-%{pypi_name}
|
%description -n python3-%{pypi_name}
|
||||||
This application provides a sleek, intuitive graphical interface for managing and launching games from PortProton, Steam, and Epic Games Store. It consolidates your game libraries into a single, user-friendly hub for seamless navigation and organization. Its lightweight structure and cross-platform support deliver a cohesive gaming experience, eliminating the need for multiple launchers. Unique PortProton integration enhances Linux gaming, enabling effortless play of Windows-based titles with minimal setup.
|
This application provides a sleek, intuitive graphical interface for managing and launching games from PortProton, Steam, and Epic Games Store. It consolidates your game libraries into a single, user-friendly hub for seamless navigation and organization. Its lightweight structure and cross-platform support deliver a cohesive gaming experience, eliminating the need for multiple launchers. Unique PortProton integration enhances Linux gaming, enabling effortless play of Windows-based titles with minimal setup.
|
||||||
|
@@ -21,9 +21,9 @@ Current translation status:
|
|||||||
|
|
||||||
| Locale | Progress | Translated |
|
| Locale | Progress | Translated |
|
||||||
| :----- | -------: | ---------: |
|
| :----- | -------: | ---------: |
|
||||||
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 240 |
|
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 193 |
|
||||||
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 240 |
|
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 193 |
|
||||||
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 240 of 240 |
|
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 193 of 193 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@@ -21,9 +21,9 @@
|
|||||||
|
|
||||||
| Локаль | Прогресс | Переведено |
|
| Локаль | Прогресс | Переведено |
|
||||||
| :----- | -------: | ---------: |
|
| :----- | -------: | ---------: |
|
||||||
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 240 |
|
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 193 |
|
||||||
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 240 |
|
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 193 |
|
||||||
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 240 из 240 |
|
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 193 из 193 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@@ -1,41 +1,17 @@
|
|||||||
import sys
|
import sys
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
from PySide6.QtCore import QLocale, QTranslator, QLibraryInfo
|
from PySide6.QtCore import QLocale, QTranslator, QLibraryInfo
|
||||||
from PySide6.QtWidgets import QApplication
|
from PySide6.QtWidgets import QApplication
|
||||||
from PySide6.QtGui import QIcon
|
from PySide6.QtGui import QIcon
|
||||||
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
|
||||||
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
|
||||||
|
|
||||||
__app_id__ = "ru.linux_gaming.PortProtonQt"
|
__app_id__ = "ru.linux_gaming.PortProtonQt"
|
||||||
__app_name__ = "PortProtonQt"
|
__app_name__ = "PortProtonQt"
|
||||||
__app_version__ = "0.1.7"
|
__app_version__ = "0.1.6"
|
||||||
|
|
||||||
def get_version():
|
|
||||||
try:
|
|
||||||
commit = subprocess.check_output(
|
|
||||||
['git', 'rev-parse', '--short', 'HEAD'],
|
|
||||||
stderr=subprocess.DEVNULL
|
|
||||||
).decode('utf-8').strip()
|
|
||||||
return f"{__app_version__} ({commit})"
|
|
||||||
except (subprocess.CalledProcessError, FileNotFoundError, OSError):
|
|
||||||
return __app_version__
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
os.environ['PW_CLI'] = '1'
|
|
||||||
os.environ['PROCESS_LOG'] = '1'
|
|
||||||
os.environ['START_FROM_STEAM'] = '1'
|
|
||||||
|
|
||||||
portproton_path = get_portproton_location()
|
|
||||||
|
|
||||||
if portproton_path is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
script_path = os.path.join(portproton_path, 'data', 'scripts', 'start.sh')
|
|
||||||
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__))
|
||||||
app.setDesktopFileName(__app_id__)
|
app.setDesktopFileName(__app_id__)
|
||||||
@@ -58,8 +34,7 @@ def main():
|
|||||||
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 language")
|
||||||
|
|
||||||
version = get_version()
|
window = MainWindow(app_name=__app_name__)
|
||||||
window = MainWindow(app_name=__app_name__, version=version)
|
|
||||||
|
|
||||||
if args.fullscreen:
|
if args.fullscreen:
|
||||||
logger.info("Launching in fullscreen mode due to --fullscreen flag")
|
logger.info("Launching in fullscreen mode due to --fullscreen flag")
|
||||||
|
Before Width: | Height: | Size: 315 KiB After Width: | Height: | Size: 315 KiB |
Before Width: | Height: | Size: 978 KiB After Width: | Height: | Size: 978 KiB |
Before Width: | Height: | Size: 634 KiB After Width: | Height: | Size: 634 KiB |
Before Width: | Height: | Size: 650 KiB After Width: | Height: | Size: 650 KiB |
Before Width: | Height: | Size: 391 KiB After Width: | Height: | Size: 391 KiB |
Before Width: | Height: | Size: 710 KiB After Width: | Height: | Size: 710 KiB |
Before Width: | Height: | Size: 627 KiB After Width: | Height: | Size: 627 KiB |
@@ -126,21 +126,7 @@ class FlowLayout(QLayout):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def heightForWidth(self, width):
|
def heightForWidth(self, width):
|
||||||
# Аналогично фильтруем видимые для тестового расчёта высоты
|
return self.doLayout(QRect(0, 0, width, 0), True)
|
||||||
visible_items = []
|
|
||||||
nat_sizes = np.empty((0, 2), dtype=np.int32)
|
|
||||||
for item in self.itemList:
|
|
||||||
if item.widget() and item.widget().isVisible():
|
|
||||||
visible_items.append(item)
|
|
||||||
s = item.sizeHint()
|
|
||||||
new_row = np.array([[s.width(), s.height()]], dtype=np.int32)
|
|
||||||
nat_sizes = np.vstack([nat_sizes, new_row]) if len(nat_sizes) > 0 else new_row
|
|
||||||
|
|
||||||
if len(visible_items) == 0:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
_, total_height = compute_layout(nat_sizes, width, self._spacing, self._max_scale)
|
|
||||||
return total_height
|
|
||||||
|
|
||||||
def setGeometry(self, rect):
|
def setGeometry(self, rect):
|
||||||
super().setGeometry(rect)
|
super().setGeometry(rect)
|
||||||
@@ -159,46 +145,26 @@ class FlowLayout(QLayout):
|
|||||||
return size
|
return size
|
||||||
|
|
||||||
def doLayout(self, rect, testOnly):
|
def doLayout(self, rect, testOnly):
|
||||||
N_total = len(self.itemList)
|
N = len(self.itemList)
|
||||||
if N_total == 0:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
# Фильтруем только видимые элементы
|
|
||||||
visible_items = []
|
|
||||||
visible_indices = [] # Индексы в оригинальном itemList для установки геометрии
|
|
||||||
nat_sizes = np.empty((0, 2), dtype=np.int32)
|
|
||||||
for i, item in enumerate(self.itemList):
|
|
||||||
if item.widget() and item.widget().isVisible():
|
|
||||||
visible_items.append(item)
|
|
||||||
visible_indices.append(i)
|
|
||||||
s = item.sizeHint()
|
|
||||||
new_row = np.array([[s.width(), s.height()]], dtype=np.int32)
|
|
||||||
nat_sizes = np.vstack([nat_sizes, new_row]) if len(nat_sizes) > 0 else new_row
|
|
||||||
|
|
||||||
N = len(visible_items)
|
|
||||||
if N == 0:
|
if N == 0:
|
||||||
# Если все скрыты, устанавливаем нулевые геометрии для всех
|
|
||||||
if not testOnly:
|
|
||||||
for item in self.itemList:
|
|
||||||
item.setGeometry(QRect())
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
nat_sizes = np.empty((N, 2), dtype=np.int32)
|
||||||
|
for i, item in enumerate(self.itemList):
|
||||||
|
s = item.sizeHint()
|
||||||
|
nat_sizes[i, 0] = s.width()
|
||||||
|
nat_sizes[i, 1] = s.height()
|
||||||
|
|
||||||
geom_array, total_height = compute_layout(nat_sizes, rect.width(), self._spacing, self._max_scale)
|
geom_array, total_height = compute_layout(nat_sizes, rect.width(), self._spacing, self._max_scale)
|
||||||
|
|
||||||
if not testOnly:
|
if not testOnly:
|
||||||
# Устанавливаем геометрии только для видимых
|
for i, item in enumerate(self.itemList):
|
||||||
for idx, (_vis_idx, item) in enumerate(zip(visible_indices, visible_items, strict=True)):
|
x = geom_array[i, 0] + rect.x()
|
||||||
x = geom_array[idx, 0] + rect.x()
|
y = geom_array[i, 1] + rect.y()
|
||||||
y = geom_array[idx, 1] + rect.y()
|
w = geom_array[i, 2]
|
||||||
w = geom_array[idx, 2]
|
h = geom_array[i, 3]
|
||||||
h = geom_array[idx, 3]
|
|
||||||
item.setGeometry(QRect(QPoint(x, y), QSize(w, h)))
|
item.setGeometry(QRect(QPoint(x, y), QSize(w, h)))
|
||||||
|
|
||||||
# Для невидимых — нулевая геометрия
|
|
||||||
for i in range(N_total):
|
|
||||||
if i not in visible_indices:
|
|
||||||
self.itemList[i].setGeometry(QRect())
|
|
||||||
|
|
||||||
return total_height
|
return total_height
|
||||||
|
|
||||||
class ClickableLabel(QLabel):
|
class ClickableLabel(QLabel):
|
||||||
|
@@ -2,13 +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
|
||||||
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
|
|
||||||
)
|
)
|
||||||
|
from PySide6.QtCore import Qt, QObject, Signal, QMimeDatabase, QTimer, QThreadPool, QRunnable, Slot
|
||||||
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
|
||||||
from portprotonqt.config_utils import get_portproton_location, read_favorite_folders, read_theme_from_config
|
from portprotonqt.config_utils import get_portproton_location, read_favorite_folders, read_theme_from_config
|
||||||
@@ -17,8 +15,6 @@ from portprotonqt.logger import get_logger
|
|||||||
from portprotonqt.theme_manager import ThemeManager
|
from portprotonqt.theme_manager import ThemeManager
|
||||||
from portprotonqt.custom_widgets import AutoSizeButton
|
from portprotonqt.custom_widgets import AutoSizeButton
|
||||||
from portprotonqt.downloader import Downloader
|
from portprotonqt.downloader import Downloader
|
||||||
from portprotonqt.virtual_keyboard import VirtualKeyboard
|
|
||||||
from portprotonqt.preloader import Preloader
|
|
||||||
import psutil
|
import psutil
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -511,8 +507,8 @@ class FileExplorer(QDialog):
|
|||||||
"""Update the list of mounted drives and favorite folders."""
|
"""Update the list of mounted drives and favorite folders."""
|
||||||
for i in reversed(range(self.drives_layout.count())):
|
for i in reversed(range(self.drives_layout.count())):
|
||||||
item = self.drives_layout.itemAt(i)
|
item = self.drives_layout.itemAt(i)
|
||||||
widget = item.widget() if item else None
|
if item and item.widget():
|
||||||
if widget:
|
widget = item.widget()
|
||||||
self.drives_layout.removeWidget(widget)
|
self.drives_layout.removeWidget(widget)
|
||||||
widget.deleteLater()
|
widget.deleteLater()
|
||||||
|
|
||||||
@@ -601,16 +597,6 @@ class FileExplorer(QDialog):
|
|||||||
self.thumbnail_cache.clear() # Clear cache when changing directories
|
self.thumbnail_cache.clear() # Clear cache when changing directories
|
||||||
self.pending_thumbnails.clear() # Clear pending thumbnails
|
self.pending_thumbnails.clear() # Clear pending thumbnails
|
||||||
try:
|
try:
|
||||||
if self.directory_only:
|
|
||||||
item = QListWidgetItem("./")
|
|
||||||
folder_icon = theme_manager.get_icon("folder")
|
|
||||||
# Ensure the icon is a QIcon
|
|
||||||
if isinstance(folder_icon, str) and os.path.isfile(folder_icon):
|
|
||||||
folder_icon = QIcon(folder_icon)
|
|
||||||
elif not isinstance(folder_icon, QIcon):
|
|
||||||
folder_icon = QIcon() # Fallback to empty icon
|
|
||||||
item.setIcon(folder_icon)
|
|
||||||
self.file_list.addItem(item)
|
|
||||||
if self.current_path != "/":
|
if self.current_path != "/":
|
||||||
item = QListWidgetItem("../")
|
item = QListWidgetItem("../")
|
||||||
folder_icon = theme_manager.get_icon("folder")
|
folder_icon = theme_manager.get_icon("folder")
|
||||||
@@ -818,60 +804,6 @@ class AddGameDialog(QDialog):
|
|||||||
if edit_mode:
|
if edit_mode:
|
||||||
self.updatePreview()
|
self.updatePreview()
|
||||||
|
|
||||||
# Инициализация клавиатуры (отдельным методом вроде лучше)
|
|
||||||
self.init_keyboard()
|
|
||||||
|
|
||||||
# Устанавливаем фокус на первое поле при открытии
|
|
||||||
QTimer.singleShot(0, self.nameEdit.setFocus)
|
|
||||||
|
|
||||||
def init_keyboard(self):
|
|
||||||
"""Инициализация виртуальной клавиатуры"""
|
|
||||||
self.keyboard = VirtualKeyboard(self, theme=self.theme, button_width=40)
|
|
||||||
self.keyboard.hide()
|
|
||||||
|
|
||||||
def show_keyboard_for_widget(self, widget):
|
|
||||||
"""Показывает клавиатуру для указанного виджета"""
|
|
||||||
if not widget or not widget.isVisible():
|
|
||||||
return
|
|
||||||
|
|
||||||
# Устанавливаем текущий виджет ввода
|
|
||||||
self.keyboard.current_input_widget = widget
|
|
||||||
|
|
||||||
# Позиционирование клавиатуры
|
|
||||||
keyboard_height = 220
|
|
||||||
self.keyboard.setFixedWidth(self.width())
|
|
||||||
self.keyboard.setFixedHeight(keyboard_height)
|
|
||||||
self.keyboard.move(0, self.height() - keyboard_height)
|
|
||||||
|
|
||||||
# Показываем и поднимаем клавиатуру
|
|
||||||
self.keyboard.setParent(self)
|
|
||||||
self.keyboard.show()
|
|
||||||
self.keyboard.raise_()
|
|
||||||
|
|
||||||
# TODO: доработать.
|
|
||||||
# Устанавливаем фокус на первую кнопку клавиатуры
|
|
||||||
first_button = self.keyboard.findFirstFocusableButton()
|
|
||||||
if first_button:
|
|
||||||
QTimer.singleShot(50, lambda: first_button.setFocus())
|
|
||||||
|
|
||||||
def closeEvent(self, event):
|
|
||||||
"""Обработчик закрытия окна"""
|
|
||||||
if hasattr(self, 'keyboard'):
|
|
||||||
self.keyboard.hide()
|
|
||||||
super().closeEvent(event)
|
|
||||||
|
|
||||||
def reject(self):
|
|
||||||
"""Обработчик кнопки Cancel"""
|
|
||||||
if hasattr(self, 'keyboard'):
|
|
||||||
self.keyboard.hide()
|
|
||||||
super().reject()
|
|
||||||
|
|
||||||
def accept(self):
|
|
||||||
"""Обработчик кнопки Apply"""
|
|
||||||
if hasattr(self, 'keyboard'):
|
|
||||||
self.keyboard.hide()
|
|
||||||
super().accept()
|
|
||||||
|
|
||||||
def browseExe(self):
|
def browseExe(self):
|
||||||
"""Открывает файловый менеджер для выбора exe-файла"""
|
"""Открывает файловый менеджер для выбора exe-файла"""
|
||||||
try:
|
try:
|
||||||
@@ -1035,465 +967,3 @@ Icon={icon_path}
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
return desktop_entry, desktop_path
|
return desktop_entry, desktop_path
|
||||||
|
|
||||||
class WinetricksDialog(QDialog):
|
|
||||||
"""Dialog for managing Winetricks components in a prefix."""
|
|
||||||
|
|
||||||
def __init__(self, parent=None, theme=None, prefix_path: str | None = None, wine_use: str | None = None):
|
|
||||||
super().__init__(parent)
|
|
||||||
self.theme = theme if theme else theme_manager.apply_theme(read_theme_from_config())
|
|
||||||
self.prefix_path: str | None = prefix_path
|
|
||||||
self.wine_use: str | None = wine_use
|
|
||||||
self.portproton_path = get_portproton_location()
|
|
||||||
if self.portproton_path is None:
|
|
||||||
logger.error("PortProton location not found")
|
|
||||||
return
|
|
||||||
self.tmp_path = os.path.join(self.portproton_path, "data", "tmp")
|
|
||||||
os.makedirs(self.tmp_path, exist_ok=True)
|
|
||||||
self.winetricks_path = os.path.join(self.tmp_path, "winetricks")
|
|
||||||
if self.prefix_path is None:
|
|
||||||
logger.error("Prefix path not provided")
|
|
||||||
return
|
|
||||||
self.log_path = os.path.join(self.prefix_path, "winetricks.log")
|
|
||||||
os.makedirs(os.path.dirname(self.log_path), exist_ok=True)
|
|
||||||
if not os.path.exists(self.log_path):
|
|
||||||
open(self.log_path, 'a').close()
|
|
||||||
|
|
||||||
self.downloader = Downloader(max_workers=4)
|
|
||||||
self.apply_process: QProcess | None = None
|
|
||||||
|
|
||||||
self.setWindowTitle(_("Prefix Manager"))
|
|
||||||
self.setModal(True)
|
|
||||||
self.resize(700, 700)
|
|
||||||
self.setStyleSheet(self.theme.MAIN_WINDOW_STYLE + self.theme.MESSAGE_BOX_STYLE)
|
|
||||||
|
|
||||||
self.update_winetricks()
|
|
||||||
self.setup_ui()
|
|
||||||
self.load_lists()
|
|
||||||
|
|
||||||
def update_winetricks(self):
|
|
||||||
"""Update the winetricks script."""
|
|
||||||
if not self.downloader.has_internet():
|
|
||||||
logger.warning("No internet connection, skipping winetricks update")
|
|
||||||
return
|
|
||||||
|
|
||||||
url = "https://raw.githubusercontent.com/Winetricks/winetricks/master/src/winetricks"
|
|
||||||
temp_path = os.path.join(self.tmp_path, "winetricks_temp")
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.downloader.download(url, temp_path)
|
|
||||||
with open(temp_path) as f:
|
|
||||||
ext_content = f.read()
|
|
||||||
ext_ver_match = re.search(r'WINETRICKS_VERSION\s*=\s*[\'"]?([^\'"\s]+)', ext_content)
|
|
||||||
ext_ver = ext_ver_match.group(1) if ext_ver_match else None
|
|
||||||
logger.info(f"External winetricks version: {ext_ver}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Failed to get external version: {e}")
|
|
||||||
ext_ver = None
|
|
||||||
if os.path.exists(temp_path):
|
|
||||||
os.remove(temp_path)
|
|
||||||
return
|
|
||||||
|
|
||||||
int_ver = None
|
|
||||||
if os.path.exists(self.winetricks_path):
|
|
||||||
try:
|
|
||||||
with open(self.winetricks_path) as f:
|
|
||||||
int_content = f.read()
|
|
||||||
int_ver_match = re.search(r'WINETRICKS_VERSION\s*=\s*[\'"]?([^\'"\s]+)', int_content)
|
|
||||||
int_ver = int_ver_match.group(1) if int_ver_match else None
|
|
||||||
logger.info(f"Internal winetricks version: {int_ver}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Failed to read internal winetricks version: {e}")
|
|
||||||
|
|
||||||
update_needed = not os.path.exists(self.winetricks_path) or (int_ver != ext_ver and ext_ver)
|
|
||||||
|
|
||||||
if update_needed:
|
|
||||||
try:
|
|
||||||
self.downloader.download(url, self.winetricks_path)
|
|
||||||
os.chmod(self.winetricks_path, 0o755)
|
|
||||||
logger.info(f"Winetricks updated to version {ext_ver}")
|
|
||||||
self.apply_modifications(self.winetricks_path)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Failed to update winetricks: {e}")
|
|
||||||
elif os.path.exists(self.winetricks_path):
|
|
||||||
self.apply_modifications(self.winetricks_path)
|
|
||||||
|
|
||||||
if os.path.exists(temp_path):
|
|
||||||
os.remove(temp_path)
|
|
||||||
|
|
||||||
def apply_modifications(self, file_path):
|
|
||||||
"""Apply custom modifications to the winetricks script."""
|
|
||||||
if not os.path.exists(file_path):
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
with open(file_path) as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
# Apply sed-like replacements
|
|
||||||
content = re.sub(r'w_metadata vcrun2015 dlls \\', r'w_metadata !dont_use_2015! dlls \\', content)
|
|
||||||
content = re.sub(r'w_metadata vcrun2017 dlls \\', r'w_metadata !dont_use_2017! dlls \\', content)
|
|
||||||
content = re.sub(r'w_metadata vcrun2019 dlls \\', r'w_metadata !dont_use_2019! dlls \\', content)
|
|
||||||
content = re.sub(r'w_set_winver win2k3', r'w_set_winver win7', content)
|
|
||||||
|
|
||||||
with open(file_path, 'w') as f:
|
|
||||||
f.write(content)
|
|
||||||
logger.info("Winetricks modifications applied")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error applying modifications to winetricks: {e}")
|
|
||||||
|
|
||||||
def setup_ui(self):
|
|
||||||
"""Set up the user interface with tabs and tables."""
|
|
||||||
main_layout = QVBoxLayout(self)
|
|
||||||
main_layout.setContentsMargins(10, 10, 10, 10)
|
|
||||||
main_layout.setSpacing(10)
|
|
||||||
|
|
||||||
# Log output
|
|
||||||
self.log_output = QTextEdit()
|
|
||||||
self.log_output.setReadOnly(True)
|
|
||||||
self.log_output.setStyleSheet(self.theme.WINETRICKS_LOG_STYLE)
|
|
||||||
main_layout.addWidget(self.log_output)
|
|
||||||
|
|
||||||
# Tab widget
|
|
||||||
self.tab_widget = QTabWidget()
|
|
||||||
self.tab_widget.setStyleSheet(self.theme.WINETRICKS_TAB_STYLE)
|
|
||||||
|
|
||||||
table_base_style = self.theme.WINETRICKS_TABBLE_STYLE
|
|
||||||
|
|
||||||
# DLLs tab
|
|
||||||
self.dll_table = QTableWidget()
|
|
||||||
self.dll_table.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
|
||||||
self.dll_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
|
||||||
self.dll_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
|
||||||
self.dll_table.setColumnCount(3)
|
|
||||||
self.dll_table.setHorizontalHeaderLabels([_("Set"), _("Libraries"), _("Information")])
|
|
||||||
self.dll_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
|
|
||||||
self.dll_table.horizontalHeader().resizeSection(0, 50)
|
|
||||||
self.dll_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
|
|
||||||
self.dll_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
|
|
||||||
self.dll_table.setStyleSheet(table_base_style)
|
|
||||||
|
|
||||||
self.dll_preloader = Preloader()
|
|
||||||
dll_preloader_container = QWidget()
|
|
||||||
dll_preloader_layout = QVBoxLayout(dll_preloader_container)
|
|
||||||
dll_preloader_layout.addStretch()
|
|
||||||
dll_preloader_hlayout = QHBoxLayout()
|
|
||||||
dll_preloader_hlayout.addStretch()
|
|
||||||
dll_preloader_hlayout.addWidget(self.dll_preloader)
|
|
||||||
dll_preloader_hlayout.addStretch()
|
|
||||||
dll_preloader_layout.addLayout(dll_preloader_hlayout)
|
|
||||||
dll_preloader_layout.addStretch()
|
|
||||||
dll_preloader_layout.setContentsMargins(0, 0, 0, 0)
|
|
||||||
dll_preloader_layout.setSpacing(0)
|
|
||||||
|
|
||||||
self.dll_container = QStackedWidget()
|
|
||||||
self.dll_container.addWidget(dll_preloader_container)
|
|
||||||
self.dll_container.addWidget(self.dll_table)
|
|
||||||
self.tab_widget.addTab(self.dll_container, "DLLs")
|
|
||||||
|
|
||||||
# Fonts tab
|
|
||||||
self.fonts_table = QTableWidget()
|
|
||||||
self.fonts_table.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
|
||||||
self.fonts_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
|
||||||
self.fonts_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
|
||||||
self.fonts_table.setColumnCount(3)
|
|
||||||
self.fonts_table.setHorizontalHeaderLabels([_("Set"), _("Fonts"), _("Information")])
|
|
||||||
self.fonts_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
|
|
||||||
self.fonts_table.horizontalHeader().resizeSection(0, 50)
|
|
||||||
self.fonts_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
|
|
||||||
self.fonts_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
|
|
||||||
self.fonts_table.setStyleSheet(table_base_style)
|
|
||||||
|
|
||||||
self.fonts_preloader = Preloader()
|
|
||||||
fonts_preloader_container = QWidget()
|
|
||||||
fonts_preloader_layout = QVBoxLayout(fonts_preloader_container)
|
|
||||||
fonts_preloader_layout.addStretch()
|
|
||||||
fonts_preloader_hlayout = QHBoxLayout()
|
|
||||||
fonts_preloader_hlayout.addStretch()
|
|
||||||
fonts_preloader_hlayout.addWidget(self.fonts_preloader)
|
|
||||||
fonts_preloader_hlayout.addStretch()
|
|
||||||
fonts_preloader_layout.addLayout(fonts_preloader_hlayout)
|
|
||||||
fonts_preloader_layout.addStretch()
|
|
||||||
fonts_preloader_layout.setContentsMargins(0, 0, 0, 0)
|
|
||||||
fonts_preloader_layout.setSpacing(0)
|
|
||||||
|
|
||||||
self.fonts_container = QStackedWidget()
|
|
||||||
self.fonts_container.addWidget(fonts_preloader_container)
|
|
||||||
self.fonts_container.addWidget(self.fonts_table)
|
|
||||||
self.tab_widget.addTab(self.fonts_container, _("Fonts"))
|
|
||||||
|
|
||||||
# Settings tab
|
|
||||||
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([_("Set"), _("Settings"), _("Information")])
|
|
||||||
self.settings_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
|
|
||||||
self.settings_table.horizontalHeader().resizeSection(0, 50)
|
|
||||||
self.settings_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
|
|
||||||
self.settings_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
|
|
||||||
self.settings_table.setStyleSheet(table_base_style)
|
|
||||||
|
|
||||||
self.settings_preloader = Preloader()
|
|
||||||
settings_preloader_container = QWidget()
|
|
||||||
settings_preloader_layout = QVBoxLayout(settings_preloader_container)
|
|
||||||
settings_preloader_layout.addStretch()
|
|
||||||
settings_preloader_hlayout = QHBoxLayout()
|
|
||||||
settings_preloader_hlayout.addStretch()
|
|
||||||
settings_preloader_hlayout.addWidget(self.settings_preloader)
|
|
||||||
settings_preloader_hlayout.addStretch()
|
|
||||||
settings_preloader_layout.addLayout(settings_preloader_hlayout)
|
|
||||||
settings_preloader_layout.addStretch()
|
|
||||||
settings_preloader_layout.setContentsMargins(0, 0, 0, 0)
|
|
||||||
settings_preloader_layout.setSpacing(0)
|
|
||||||
|
|
||||||
self.settings_container = QStackedWidget()
|
|
||||||
self.settings_container.addWidget(settings_preloader_container)
|
|
||||||
self.settings_container.addWidget(self.settings_table)
|
|
||||||
self.tab_widget.addTab(self.settings_container, _("Settings"))
|
|
||||||
|
|
||||||
self.containers = {
|
|
||||||
"dlls": self.dll_container,
|
|
||||||
"fonts": self.fonts_container,
|
|
||||||
"settings": self.settings_container
|
|
||||||
}
|
|
||||||
|
|
||||||
main_layout.addWidget(self.tab_widget)
|
|
||||||
|
|
||||||
# Buttons
|
|
||||||
button_layout = QHBoxLayout()
|
|
||||||
button_layout.setSpacing(10)
|
|
||||||
self.cancel_button = AutoSizeButton(_("Cancel"), icon=theme_manager.get_icon("cancel"))
|
|
||||||
self.force_button = AutoSizeButton(_("Force Install"), icon=theme_manager.get_icon("apply"))
|
|
||||||
self.install_button = AutoSizeButton(_("Install"), icon=theme_manager.get_icon("apply"))
|
|
||||||
self.cancel_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
|
||||||
self.force_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
|
||||||
self.install_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
|
||||||
button_layout.addWidget(self.cancel_button)
|
|
||||||
button_layout.addWidget(self.force_button)
|
|
||||||
button_layout.addWidget(self.install_button)
|
|
||||||
main_layout.addLayout(button_layout)
|
|
||||||
|
|
||||||
self.cancel_button.clicked.connect(self.reject)
|
|
||||||
self.force_button.clicked.connect(lambda: self.install_selected(force=True))
|
|
||||||
self.install_button.clicked.connect(lambda: self.install_selected(force=False))
|
|
||||||
|
|
||||||
def load_lists(self):
|
|
||||||
"""Load and populate the lists for DLLs, Fonts, and Settings"""
|
|
||||||
if not os.path.exists(self.winetricks_path):
|
|
||||||
QMessageBox.warning(self, _("Error"), _("Winetricks not found. Please try again."))
|
|
||||||
self.reject()
|
|
||||||
return
|
|
||||||
|
|
||||||
assert self.prefix_path is not None
|
|
||||||
env = QProcessEnvironment.systemEnvironment()
|
|
||||||
env.insert("WINEPREFIX", self.prefix_path)
|
|
||||||
env.insert("WINETRICKS_DOWNLOADER", "curl")
|
|
||||||
if self.wine_use is not None:
|
|
||||||
env.insert("WINE", self.wine_use)
|
|
||||||
|
|
||||||
cwd = os.path.dirname(self.winetricks_path)
|
|
||||||
|
|
||||||
# DLLs
|
|
||||||
self.containers["dlls"].setCurrentIndex(0)
|
|
||||||
self._start_list_process("dlls", self.dll_table, self.get_dll_exclusions(), env, cwd)
|
|
||||||
|
|
||||||
# Fonts
|
|
||||||
self.containers["fonts"].setCurrentIndex(0)
|
|
||||||
self._start_list_process("fonts", self.fonts_table, self.get_fonts_exclusions(), env, cwd)
|
|
||||||
|
|
||||||
# Settings
|
|
||||||
self.containers["settings"].setCurrentIndex(0)
|
|
||||||
self._start_list_process("settings", self.settings_table, self.get_settings_exclusions(), env, cwd)
|
|
||||||
|
|
||||||
def _start_list_process(self, category, table, exclusion_pattern, env, cwd):
|
|
||||||
"""Запускает QProcess для списка."""
|
|
||||||
process = QProcess(self)
|
|
||||||
process.setProcessChannelMode(QProcess.ProcessChannelMode.MergedChannels)
|
|
||||||
process.setProcessEnvironment(env)
|
|
||||||
process.finished.connect(lambda exit_code, exit_status: self._on_list_finished(category, table, exclusion_pattern, process, exit_code, exit_status))
|
|
||||||
process.start(self.winetricks_path, [category, "list"])
|
|
||||||
|
|
||||||
def _on_list_finished(self, category, table, exclusion_pattern, process: QProcess | None, exit_code, exit_status):
|
|
||||||
"""Обработчик завершения списка."""
|
|
||||||
if process is None:
|
|
||||||
logger.error(f"Process is None for {category}")
|
|
||||||
self.containers[category].setCurrentIndex(1)
|
|
||||||
return
|
|
||||||
output = bytes(process.readAllStandardOutput().data()).decode('utf-8', 'ignore')
|
|
||||||
if exit_code == 0 and exit_status == QProcess.ExitStatus.NormalExit:
|
|
||||||
self.populate_table(table, output, exclusion_pattern, self.log_path)
|
|
||||||
# Restore focus after populating
|
|
||||||
if table.rowCount() > 0:
|
|
||||||
table.setCurrentCell(0, 0)
|
|
||||||
table.setFocus(Qt.FocusReason.OtherFocusReason)
|
|
||||||
else:
|
|
||||||
error_output = bytes(process.readAllStandardError().data()).decode('utf-8', 'ignore')
|
|
||||||
logger.error(f"Failed to list {category}: {error_output}")
|
|
||||||
|
|
||||||
self.containers[category].setCurrentIndex(1)
|
|
||||||
|
|
||||||
def get_dll_exclusions(self):
|
|
||||||
"""Get regex pattern for DLL exclusions."""
|
|
||||||
return r'(d3d|directx9|dont_use|dxvk|vkd3d|galliumnine|faudio1|Foundation)'
|
|
||||||
|
|
||||||
def get_fonts_exclusions(self):
|
|
||||||
"""Get regex pattern for Fonts exclusions."""
|
|
||||||
return r'dont_use'
|
|
||||||
|
|
||||||
def get_settings_exclusions(self):
|
|
||||||
"""Get regex pattern for Settings exclusions."""
|
|
||||||
return r'(vista|alldlls|autostart_|bad|good|win|videomemory|vd=|isolate_home)'
|
|
||||||
|
|
||||||
def populate_table(self, table, output, exclusion_pattern, log_path):
|
|
||||||
"""Populate the table with items from output, checking installation status."""
|
|
||||||
table.setRowCount(0)
|
|
||||||
table.verticalHeader().setVisible(False)
|
|
||||||
lines = output.strip().split('\n')
|
|
||||||
installed = set()
|
|
||||||
if os.path.exists(log_path):
|
|
||||||
with open(log_path) as f:
|
|
||||||
for line in f:
|
|
||||||
installed.add(line.strip())
|
|
||||||
|
|
||||||
# regex-парсинг (имя - первое слово, остальное - описание)
|
|
||||||
line_re = re.compile(r"^\s*(?:\[(.)]\s+)?([^\s]+)\s*(.*)")
|
|
||||||
|
|
||||||
for line in lines:
|
|
||||||
line = line.strip()
|
|
||||||
if not line or re.search(exclusion_pattern, line, re.I):
|
|
||||||
continue
|
|
||||||
|
|
||||||
line = line.split('(', 1)[0].strip()
|
|
||||||
|
|
||||||
match = line_re.match(line)
|
|
||||||
if not match:
|
|
||||||
continue
|
|
||||||
|
|
||||||
_status, name, info = match.groups()
|
|
||||||
# Очищаем info от мусора
|
|
||||||
info = re.sub(r'\[.*?\]', '', info).strip() # Удаляем [скачивания] и т.п.
|
|
||||||
|
|
||||||
# To match bash desc extraction: after name, substr(2) to trim leading space
|
|
||||||
if info.startswith(' '):
|
|
||||||
info = info[1:].lstrip()
|
|
||||||
|
|
||||||
# Фильтр служебных строк
|
|
||||||
if '/' in name or '\\' in name or name.lower() in ('executing', 'using', 'warning:') or name.endswith(':'):
|
|
||||||
continue
|
|
||||||
|
|
||||||
checked = Qt.CheckState.Checked if name in installed else Qt.CheckState.Unchecked
|
|
||||||
|
|
||||||
row = table.rowCount()
|
|
||||||
table.insertRow(row)
|
|
||||||
|
|
||||||
# Checkbox
|
|
||||||
checkbox = QTableWidgetItem()
|
|
||||||
checkbox.setCheckState(checked)
|
|
||||||
table.setItem(row, 0, checkbox)
|
|
||||||
|
|
||||||
# Name
|
|
||||||
name_item = QTableWidgetItem(name)
|
|
||||||
table.setItem(row, 1, name_item)
|
|
||||||
|
|
||||||
# Info
|
|
||||||
info_item = QTableWidgetItem(info)
|
|
||||||
table.setItem(row, 2, info_item)
|
|
||||||
|
|
||||||
def install_selected(self, force=False):
|
|
||||||
"""Install selected components."""
|
|
||||||
selected = []
|
|
||||||
for table in [self.dll_table, self.fonts_table, self.settings_table]:
|
|
||||||
for row in range(table.rowCount()):
|
|
||||||
checkbox = table.item(row, 0)
|
|
||||||
if checkbox is not None and checkbox.checkState() == Qt.CheckState.Checked:
|
|
||||||
name_item = table.item(row, 1)
|
|
||||||
if name_item is not None:
|
|
||||||
name = name_item.text()
|
|
||||||
if name and name not in selected:
|
|
||||||
selected.append(name)
|
|
||||||
|
|
||||||
# Load installed
|
|
||||||
installed = set()
|
|
||||||
if os.path.exists(self.log_path):
|
|
||||||
with open(self.log_path) as f:
|
|
||||||
for line in f:
|
|
||||||
installed.add(line.strip())
|
|
||||||
|
|
||||||
# Filter to new selected
|
|
||||||
new_selected = [name for name in selected if name not in installed]
|
|
||||||
|
|
||||||
if not new_selected:
|
|
||||||
QMessageBox.information(self, _("Warning"), _("No components selected."))
|
|
||||||
return
|
|
||||||
|
|
||||||
self.install_button.setEnabled(False)
|
|
||||||
self.force_button.setEnabled(False)
|
|
||||||
self.cancel_button.setEnabled(False)
|
|
||||||
|
|
||||||
self._start_install_process(new_selected, force)
|
|
||||||
|
|
||||||
def _start_install_process(self, selected, force):
|
|
||||||
"""Запускает QProcess для установки."""
|
|
||||||
assert self.prefix_path is not None
|
|
||||||
env = QProcessEnvironment.systemEnvironment()
|
|
||||||
env.insert("WINEPREFIX", self.prefix_path)
|
|
||||||
if self.wine_use is not None:
|
|
||||||
env.insert("WINE", self.wine_use)
|
|
||||||
|
|
||||||
self.apply_process = QProcess(self)
|
|
||||||
self.apply_process.setProcessChannelMode(QProcess.ProcessChannelMode.MergedChannels)
|
|
||||||
self.apply_process.setProcessEnvironment(env)
|
|
||||||
self.apply_process.readyReadStandardOutput.connect(self._on_ready_read)
|
|
||||||
self.apply_process.finished.connect(lambda exit_code, exit_status: self._on_install_finished(exit_code, exit_status, selected))
|
|
||||||
args = ["--unattended"] + (["--force"] if force else []) + selected
|
|
||||||
self.apply_process.start(self.winetricks_path, args)
|
|
||||||
|
|
||||||
def _on_ready_read(self):
|
|
||||||
"""Handle ready read for install process."""
|
|
||||||
if self.apply_process is None:
|
|
||||||
return
|
|
||||||
data = self.apply_process.readAllStandardOutput().data()
|
|
||||||
message = bytes(data).decode('utf-8', 'ignore').strip()
|
|
||||||
self._log(message)
|
|
||||||
|
|
||||||
def _on_install_finished(self, exit_code, exit_status, selected):
|
|
||||||
"""Обработчик завершения установки."""
|
|
||||||
error_message = ""
|
|
||||||
if self.apply_process is not None:
|
|
||||||
# Читаем вывод в зависимости от режима каналов
|
|
||||||
if self.apply_process.processChannelMode() == QProcess.ProcessChannelMode.MergedChannels:
|
|
||||||
# Если каналы объединены, читаем из StandardOutput
|
|
||||||
output_data = self.apply_process.readAllStandardOutput().data()
|
|
||||||
error_message = bytes(output_data).decode('utf-8', 'ignore')
|
|
||||||
else:
|
|
||||||
# Если каналы разделены, читаем из StandardError
|
|
||||||
error_data = self.apply_process.readAllStandardError().data()
|
|
||||||
error_message = bytes(error_data).decode('utf-8', 'ignore')
|
|
||||||
|
|
||||||
if exit_code != 0 or exit_status != QProcess.ExitStatus.NormalExit:
|
|
||||||
logger.error(f"Winetricks install failed: {error_message}")
|
|
||||||
QMessageBox.warning(self, _("Error"), _("Installation failed. Check logs."))
|
|
||||||
else:
|
|
||||||
if os.path.exists(self.log_path):
|
|
||||||
with open(self.log_path) as f:
|
|
||||||
existing = {line.strip() for line in f if line.strip()}
|
|
||||||
else:
|
|
||||||
existing = set()
|
|
||||||
with open(self.log_path, 'a') as f:
|
|
||||||
for name in selected:
|
|
||||||
if name not in existing:
|
|
||||||
f.write(f"{name}\n")
|
|
||||||
logger.info("Winetricks installation completed successfully.")
|
|
||||||
QMessageBox.information(self, _("Success"), _("Components installed successfully."))
|
|
||||||
self.load_lists()
|
|
||||||
|
|
||||||
# Разблокировка
|
|
||||||
self.install_button.setEnabled(True)
|
|
||||||
self.force_button.setEnabled(True)
|
|
||||||
self.cancel_button.setEnabled(True)
|
|
||||||
|
|
||||||
def _log(self, message):
|
|
||||||
"""Добавляет в лог."""
|
|
||||||
self.log_output.append(message)
|
|
||||||
self.log_output.moveCursor(QTextCursor.MoveOperation.End)
|
|
||||||
|
@@ -12,7 +12,6 @@ from portprotonqt.downloader import Downloader
|
|||||||
from portprotonqt.animations import GameCardAnimations
|
from portprotonqt.animations import GameCardAnimations
|
||||||
from typing import cast
|
from typing import cast
|
||||||
|
|
||||||
|
|
||||||
class GameCard(QFrame):
|
class GameCard(QFrame):
|
||||||
borderWidthChanged = Signal()
|
borderWidthChanged = Signal()
|
||||||
gradientAngleChanged = Signal()
|
gradientAngleChanged = Signal()
|
||||||
@@ -448,7 +447,6 @@ class GameCard(QFrame):
|
|||||||
gradientAngle = Property(float, getGradientAngle, setGradientAngle, None, "", notify=cast(Callable[[], None], gradientAngleChanged))
|
gradientAngle = Property(float, getGradientAngle, setGradientAngle, None, "", notify=cast(Callable[[], None], gradientAngleChanged))
|
||||||
scale = Property(float, getScale, setScale, None, "", notify=cast(Callable[[], None], scaleChanged))
|
scale = Property(float, getScale, setScale, None, "", notify=cast(Callable[[], None], scaleChanged))
|
||||||
|
|
||||||
|
|
||||||
def paintEvent(self, event):
|
def paintEvent(self, event):
|
||||||
super().paintEvent(event)
|
super().paintEvent(event)
|
||||||
self.animations.paint_border(QPainter(self))
|
self.animations.paint_border(QPainter(self))
|
||||||
|
@@ -35,7 +35,6 @@ class MainWindowProtocol(Protocol):
|
|||||||
_last_card_width: int
|
_last_card_width: int
|
||||||
current_hovered_card: GameCard | None
|
current_hovered_card: GameCard | None
|
||||||
current_focused_card: GameCard | None
|
current_focused_card: GameCard | None
|
||||||
gamesListWidget: QWidget | None
|
|
||||||
|
|
||||||
class GameLibraryManager:
|
class GameLibraryManager:
|
||||||
def __init__(self, main_window: MainWindowProtocol, theme, context_menu_manager: ContextMenuManager | None):
|
def __init__(self, main_window: MainWindowProtocol, theme, context_menu_manager: ContextMenuManager | None):
|
||||||
@@ -56,16 +55,6 @@ class GameLibraryManager:
|
|||||||
self.is_filtering = False
|
self.is_filtering = False
|
||||||
self.dirty = False
|
self.dirty = False
|
||||||
|
|
||||||
def force_update_cards_library(self):
|
|
||||||
if self.gamesListWidget and self.gamesListLayout:
|
|
||||||
self.gamesListLayout.invalidate()
|
|
||||||
self.gamesListWidget.updateGeometry()
|
|
||||||
widget = self.gamesListWidget
|
|
||||||
QTimer.singleShot(0, lambda: (
|
|
||||||
widget.adjustSize(),
|
|
||||||
widget.updateGeometry()
|
|
||||||
))
|
|
||||||
|
|
||||||
def create_games_library_widget(self):
|
def create_games_library_widget(self):
|
||||||
"""Creates the games library widget with search, grid, and slider."""
|
"""Creates the games library widget with search, grid, and slider."""
|
||||||
self.gamesLibraryWidget = QWidget()
|
self.gamesLibraryWidget = QWidget()
|
||||||
@@ -356,8 +345,6 @@ class GameLibraryManager:
|
|||||||
self.gamesListWidget.updateGeometry()
|
self.gamesListWidget.updateGeometry()
|
||||||
self.main_window._last_card_width = self.card_width
|
self.main_window._last_card_width = self.card_width
|
||||||
|
|
||||||
self.force_update_cards_library()
|
|
||||||
|
|
||||||
self.is_filtering = False # Reset flag in any case
|
self.is_filtering = False # Reset flag in any case
|
||||||
|
|
||||||
def _apply_filter_visibility(self, search_text: str):
|
def _apply_filter_visibility(self, search_text: str):
|
||||||
@@ -375,9 +362,8 @@ class GameLibraryManager:
|
|||||||
cover_path, width, height, callback = self.pending_images.pop(game_key)
|
cover_path, width, height, callback = self.pending_images.pop(game_key)
|
||||||
load_pixmap_async(cover_path, width, height, callback)
|
load_pixmap_async(cover_path, width, height, callback)
|
||||||
|
|
||||||
# Force full relayout after visibility changes
|
# Force geometry update so FlowLayout accounts for hidden widgets
|
||||||
if self.gamesListLayout is not None:
|
if self.gamesListLayout is not None:
|
||||||
self.gamesListLayout.invalidate() # Принудительно инвалидируем для пересчёта
|
|
||||||
self.gamesListLayout.update()
|
self.gamesListLayout.update()
|
||||||
if self.gamesListWidget is not None:
|
if self.gamesListWidget is not None:
|
||||||
self.gamesListWidget.updateGeometry()
|
self.gamesListWidget.updateGeometry()
|
||||||
|
@@ -5,7 +5,7 @@ from typing import Protocol, cast
|
|||||||
from evdev import InputDevice, InputEvent, ecodes, list_devices, ff
|
from evdev import InputDevice, InputEvent, ecodes, list_devices, ff
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from pyudev import Context, Monitor, MonitorObserver, Device
|
from pyudev import Context, Monitor, MonitorObserver, Device
|
||||||
from PySide6.QtWidgets import QWidget, QStackedWidget, QApplication, QScrollArea, QLineEdit, QDialog, QMenu, QComboBox, QListView, QMessageBox, QListWidget, QTableWidget, QAbstractItemView
|
from PySide6.QtWidgets import QWidget, QStackedWidget, QApplication, QScrollArea, QLineEdit, QDialog, QMenu, QComboBox, QListView, QMessageBox, QListWidget
|
||||||
from PySide6.QtCore import Qt, QObject, QEvent, QPoint, Signal, Slot, QTimer
|
from PySide6.QtCore import Qt, QObject, QEvent, QPoint, Signal, Slot, QTimer
|
||||||
from PySide6.QtGui import QKeyEvent, QMouseEvent
|
from PySide6.QtGui import QKeyEvent, QMouseEvent
|
||||||
from portprotonqt.logger import get_logger
|
from portprotonqt.logger import get_logger
|
||||||
@@ -13,8 +13,7 @@ from portprotonqt.image_utils import FullscreenDialog
|
|||||||
from portprotonqt.custom_widgets import NavLabel, AutoSizeButton
|
from portprotonqt.custom_widgets import NavLabel, AutoSizeButton
|
||||||
from portprotonqt.game_card import GameCard
|
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
|
from portprotonqt.config_utils import read_fullscreen_config, read_window_geometry, save_window_geometry, read_auto_fullscreen_gamepad, read_rumble_config
|
||||||
from portprotonqt.dialogs import AddGameDialog, WinetricksDialog
|
from portprotonqt.dialogs import AddGameDialog
|
||||||
from portprotonqt.virtual_keyboard import VirtualKeyboard
|
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -38,7 +37,6 @@ class MainWindowProtocol(Protocol):
|
|||||||
stackedWidget: QStackedWidget
|
stackedWidget: QStackedWidget
|
||||||
tabButtons: dict[int, QWidget]
|
tabButtons: dict[int, QWidget]
|
||||||
gamesListWidget: QWidget
|
gamesListWidget: QWidget
|
||||||
autoInstallContainer: QWidget | None
|
|
||||||
currentDetailPage: QWidget | None
|
currentDetailPage: QWidget | None
|
||||||
current_exec_line: str | None
|
current_exec_line: str | None
|
||||||
current_add_game_dialog: AddGameDialog | None
|
current_add_game_dialog: AddGameDialog | None
|
||||||
@@ -73,7 +71,7 @@ class InputManager(QObject):
|
|||||||
for seamless UI interaction.
|
for seamless UI interaction.
|
||||||
"""
|
"""
|
||||||
# Signals for gamepad events
|
# Signals for gamepad events
|
||||||
button_event = Signal(int, int) # Signal for button events: (code, value) where value=1 (press), 0 (release)
|
button_pressed = Signal(int) # Signal for button presses
|
||||||
dpad_moved = Signal(int, int, float) # Signal for D-pad movements
|
dpad_moved = Signal(int, int, float) # Signal for D-pad movements
|
||||||
toggle_fullscreen = Signal(bool) # Signal for toggling fullscreen mode (True for fullscreen, False for normal)
|
toggle_fullscreen = Signal(bool) # Signal for toggling fullscreen mode (True for fullscreen, False for normal)
|
||||||
|
|
||||||
@@ -92,7 +90,6 @@ class InputManager(QObject):
|
|||||||
self._parent.currentDetailPage = getattr(self._parent, 'currentDetailPage', None)
|
self._parent.currentDetailPage = getattr(self._parent, 'currentDetailPage', None)
|
||||||
self._parent.current_exec_line = getattr(self._parent, 'current_exec_line', None)
|
self._parent.current_exec_line = getattr(self._parent, 'current_exec_line', None)
|
||||||
self._parent.current_add_game_dialog = getattr(self._parent, 'current_add_game_dialog', None)
|
self._parent.current_add_game_dialog = getattr(self._parent, 'current_add_game_dialog', None)
|
||||||
self._parent.autoInstallContainer = getattr(self._parent, 'autoInstallContainer', None)
|
|
||||||
self.axis_deadzone = axis_deadzone
|
self.axis_deadzone = axis_deadzone
|
||||||
self.initial_axis_move_delay = initial_axis_move_delay
|
self.initial_axis_move_delay = initial_axis_move_delay
|
||||||
self.repeat_axis_move_delay = repeat_axis_move_delay
|
self.repeat_axis_move_delay = repeat_axis_move_delay
|
||||||
@@ -133,7 +130,7 @@ class InputManager(QObject):
|
|||||||
self.current_dpad_value = 0 # Tracks the current D-pad direction value (e.g., -1, 1)
|
self.current_dpad_value = 0 # Tracks the current D-pad direction value (e.g., -1, 1)
|
||||||
|
|
||||||
# Connect signals to slots
|
# Connect signals to slots
|
||||||
self.button_event.connect(self.handle_button_slot)
|
self.button_pressed.connect(self.handle_button_slot)
|
||||||
self.dpad_moved.connect(self.handle_dpad_slot)
|
self.dpad_moved.connect(self.handle_dpad_slot)
|
||||||
self.toggle_fullscreen.connect(self.handle_fullscreen_slot)
|
self.toggle_fullscreen.connect(self.handle_fullscreen_slot)
|
||||||
|
|
||||||
@@ -145,132 +142,6 @@ class InputManager(QObject):
|
|||||||
# Initialize evdev + hotplug
|
# Initialize evdev + hotplug
|
||||||
self.init_gamepad()
|
self.init_gamepad()
|
||||||
|
|
||||||
def _navigate_game_cards(self, container, tab_index: int, code: int, value: int) -> None:
|
|
||||||
"""Common navigation logic for game cards in a container."""
|
|
||||||
if container is None:
|
|
||||||
return
|
|
||||||
focused = QApplication.focusWidget()
|
|
||||||
game_cards = container.findChildren(GameCard)
|
|
||||||
if not game_cards:
|
|
||||||
return
|
|
||||||
|
|
||||||
scroll_area = container.parentWidget()
|
|
||||||
while scroll_area and not isinstance(scroll_area, QScrollArea):
|
|
||||||
scroll_area = scroll_area.parentWidget()
|
|
||||||
|
|
||||||
# If no focused widget or not a GameCard, focus the first card
|
|
||||||
if not isinstance(focused, GameCard) or focused not in game_cards:
|
|
||||||
game_cards[0].setFocus()
|
|
||||||
if scroll_area:
|
|
||||||
scroll_area.ensureWidgetVisible(game_cards[0], 50, 50)
|
|
||||||
return
|
|
||||||
|
|
||||||
cards = container.findChildren(GameCard, options=Qt.FindChildOption.FindChildrenRecursively)
|
|
||||||
if not cards:
|
|
||||||
return
|
|
||||||
# Group cards by rows with tolerance for y-position
|
|
||||||
rows = {}
|
|
||||||
y_tolerance = 10 # Allow slight variations in y-position
|
|
||||||
for card in cards:
|
|
||||||
y = card.pos().y()
|
|
||||||
matched = False
|
|
||||||
for row_y in rows:
|
|
||||||
if abs(y - row_y) <= y_tolerance:
|
|
||||||
rows[row_y].append(card)
|
|
||||||
matched = True
|
|
||||||
break
|
|
||||||
if not matched:
|
|
||||||
rows[y] = [card]
|
|
||||||
sorted_rows = sorted(rows.items(), key=lambda x: x[0])
|
|
||||||
if not sorted_rows:
|
|
||||||
return
|
|
||||||
current_row_idx = None
|
|
||||||
current_col_idx = None
|
|
||||||
for row_idx, (_y, row_cards) in enumerate(sorted_rows):
|
|
||||||
for idx, card in enumerate(row_cards):
|
|
||||||
if card == focused:
|
|
||||||
current_row_idx = row_idx
|
|
||||||
current_col_idx = idx
|
|
||||||
break
|
|
||||||
if current_row_idx is not None:
|
|
||||||
break
|
|
||||||
|
|
||||||
# Fallback: if focused card not found, select closest row by y-position
|
|
||||||
if current_row_idx is None:
|
|
||||||
if not sorted_rows: # Additional safety check
|
|
||||||
return
|
|
||||||
focused_y = focused.pos().y()
|
|
||||||
current_row_idx = min(range(len(sorted_rows)), key=lambda i: abs(sorted_rows[i][0] - focused_y))
|
|
||||||
if current_row_idx >= len(sorted_rows): # Safety check
|
|
||||||
return
|
|
||||||
current_row = sorted_rows[current_row_idx][1]
|
|
||||||
focused_x = focused.pos().x() + focused.width() / 2
|
|
||||||
current_col_idx = min(range(len(current_row)), key=lambda i: abs((current_row[i].pos().x() + current_row[i].width() / 2) - focused_x), default=0) # type: ignore
|
|
||||||
|
|
||||||
# Add null checks before using current_row_idx and current_col_idx
|
|
||||||
if current_row_idx is None or current_col_idx is None or current_row_idx >= len(sorted_rows):
|
|
||||||
return
|
|
||||||
|
|
||||||
current_row = sorted_rows[current_row_idx][1]
|
|
||||||
if code == ecodes.ABS_HAT0X and value != 0:
|
|
||||||
if value < 0: # Left
|
|
||||||
if current_col_idx > 0:
|
|
||||||
next_card = current_row[current_col_idx - 1]
|
|
||||||
next_card.setFocus(Qt.FocusReason.OtherFocusReason)
|
|
||||||
if scroll_area:
|
|
||||||
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
|
||||||
else:
|
|
||||||
if current_row_idx > 0:
|
|
||||||
prev_row = sorted_rows[current_row_idx - 1][1]
|
|
||||||
next_card = prev_row[-1] if prev_row else None
|
|
||||||
if next_card:
|
|
||||||
next_card.setFocus(Qt.FocusReason.OtherFocusReason)
|
|
||||||
if scroll_area:
|
|
||||||
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
|
||||||
elif value > 0: # Right
|
|
||||||
if current_col_idx < len(current_row) - 1:
|
|
||||||
next_card = current_row[current_col_idx + 1]
|
|
||||||
next_card.setFocus(Qt.FocusReason.OtherFocusReason)
|
|
||||||
if scroll_area:
|
|
||||||
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
|
||||||
else:
|
|
||||||
if current_row_idx < len(sorted_rows) - 1:
|
|
||||||
next_row = sorted_rows[current_row_idx + 1][1]
|
|
||||||
next_card = next_row[0] if next_row else None
|
|
||||||
if next_card:
|
|
||||||
next_card.setFocus(Qt.FocusReason.OtherFocusReason)
|
|
||||||
if scroll_area:
|
|
||||||
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
|
||||||
elif code == ecodes.ABS_HAT0Y and value != 0:
|
|
||||||
if value > 0: # Down
|
|
||||||
if current_row_idx < len(sorted_rows) - 1:
|
|
||||||
next_row = sorted_rows[current_row_idx + 1][1]
|
|
||||||
current_x = focused.pos().x() + focused.width() / 2
|
|
||||||
next_card = min(
|
|
||||||
next_row,
|
|
||||||
key=lambda c: abs((c.pos().x() + c.width() / 2) - current_x),
|
|
||||||
default=None
|
|
||||||
)
|
|
||||||
if next_card:
|
|
||||||
next_card.setFocus(Qt.FocusReason.OtherFocusReason)
|
|
||||||
if scroll_area:
|
|
||||||
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
|
||||||
elif value < 0: # Up
|
|
||||||
if current_row_idx > 0:
|
|
||||||
prev_row = sorted_rows[current_row_idx - 1][1]
|
|
||||||
current_x = focused.pos().x() + focused.width() / 2
|
|
||||||
next_card = min(
|
|
||||||
prev_row,
|
|
||||||
key=lambda c: abs((c.pos().x() + c.width() / 2) - current_x),
|
|
||||||
default=None
|
|
||||||
)
|
|
||||||
if next_card:
|
|
||||||
next_card.setFocus(Qt.FocusReason.OtherFocusReason)
|
|
||||||
if scroll_area:
|
|
||||||
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
|
||||||
elif current_row_idx == 0:
|
|
||||||
self._parent.tabButtons[tab_index].setFocus(Qt.FocusReason.OtherFocusReason)
|
|
||||||
|
|
||||||
def detect_gamepad_type(self, device: InputDevice) -> GamepadType:
|
def detect_gamepad_type(self, device: InputDevice) -> GamepadType:
|
||||||
"""
|
"""
|
||||||
Определяет тип геймпада по capabilities
|
Определяет тип геймпада по capabilities
|
||||||
@@ -330,9 +201,7 @@ class InputManager(QObject):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error restoring gamepad handlers: {e}")
|
logger.error(f"Error restoring gamepad handlers: {e}")
|
||||||
|
|
||||||
def handle_file_explorer_button(self, button_code, value):
|
def handle_file_explorer_button(self, button_code):
|
||||||
if value == 0: # Ignore releases
|
|
||||||
return
|
|
||||||
try:
|
try:
|
||||||
popup = QApplication.activePopupWidget()
|
popup = QApplication.activePopupWidget()
|
||||||
if isinstance(popup, QMenu):
|
if isinstance(popup, QMenu):
|
||||||
@@ -572,33 +441,8 @@ class InputManager(QObject):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error stopping rumble: {e}", exc_info=True)
|
logger.error(f"Error stopping rumble: {e}", exc_info=True)
|
||||||
|
|
||||||
@Slot(int, int)
|
@Slot(int)
|
||||||
def handle_button_slot(self, button_code: int, value: int) -> None:
|
def handle_button_slot(self, button_code: int) -> None:
|
||||||
active_window = QApplication.activeWindow()
|
|
||||||
|
|
||||||
# Обработка виртуальной клавиатуры в AddGameDialog (handle both press and release)
|
|
||||||
if isinstance(active_window, AddGameDialog):
|
|
||||||
focused = QApplication.focusWidget()
|
|
||||||
if button_code in BUTTONS['confirm'] and value == 1 and isinstance(focused, QLineEdit):
|
|
||||||
# Показываем клавиатуру при нажатии A на поле ввода (only on press)
|
|
||||||
active_window.show_keyboard_for_widget(focused)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Если клавиатура видима, обрабатываем её кнопки (including release)
|
|
||||||
if hasattr(active_window, 'keyboard') and active_window.keyboard.isVisible():
|
|
||||||
self.handle_virtual_keyboard(button_code, value)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Main window keyboard handling (including release)
|
|
||||||
keyboard = getattr(self._parent, 'keyboard', None)
|
|
||||||
if keyboard and keyboard.isVisible():
|
|
||||||
self.handle_virtual_keyboard(button_code, value)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Ignore releases for all other (non-keyboard) button handling
|
|
||||||
if value == 0:
|
|
||||||
return
|
|
||||||
|
|
||||||
if not self._gamepad_handling_enabled:
|
if not self._gamepad_handling_enabled:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
@@ -611,31 +455,6 @@ class InputManager(QObject):
|
|||||||
if not app or not active:
|
if not app or not active:
|
||||||
return
|
return
|
||||||
|
|
||||||
current_tab_index = self._parent.stackedWidget.currentIndex()
|
|
||||||
|
|
||||||
if button_code in BUTTONS['confirm'] and isinstance(focused, QLineEdit):
|
|
||||||
search_edit = None
|
|
||||||
if current_tab_index == 0:
|
|
||||||
search_edit = getattr(self._parent, 'searchEdit', None)
|
|
||||||
elif current_tab_index == 1:
|
|
||||||
search_edit = getattr(self._parent, 'autoInstallSearchLineEdit', None)
|
|
||||||
if focused == search_edit:
|
|
||||||
keyboard = getattr(self._parent, 'keyboard', None)
|
|
||||||
if keyboard:
|
|
||||||
keyboard.show_for_widget(focused)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Handle Y button to focus search
|
|
||||||
if button_code in BUTTONS['prev_dir']: # Y button
|
|
||||||
search_edit = None
|
|
||||||
if current_tab_index == 0:
|
|
||||||
search_edit = getattr(self._parent, 'searchEdit', None)
|
|
||||||
elif current_tab_index == 1:
|
|
||||||
search_edit = getattr(self._parent, 'autoInstallSearchLineEdit', None)
|
|
||||||
if search_edit:
|
|
||||||
search_edit.setFocus()
|
|
||||||
return
|
|
||||||
|
|
||||||
# Handle Guide button to open system overlay
|
# Handle Guide button to open system overlay
|
||||||
if button_code in BUTTONS['guide']:
|
if button_code in BUTTONS['guide']:
|
||||||
if not popup and not isinstance(active, QDialog):
|
if not popup and not isinstance(active, QDialog):
|
||||||
@@ -732,39 +551,6 @@ class InputManager(QObject):
|
|||||||
self._parent.toggleGame(self._parent.current_exec_line, None)
|
self._parent.toggleGame(self._parent.current_exec_line, None)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
if isinstance(active, WinetricksDialog):
|
|
||||||
if button_code in BUTTONS['confirm']: # A button - toggle checkbox
|
|
||||||
current_table = active.tab_widget.currentWidget()
|
|
||||||
if isinstance(current_table, QTableWidget):
|
|
||||||
current_row = current_table.currentRow()
|
|
||||||
if current_row >= 0:
|
|
||||||
checkbox = current_table.item(current_row, 0)
|
|
||||||
if checkbox:
|
|
||||||
checkbox.setCheckState(
|
|
||||||
Qt.CheckState.Unchecked if checkbox.checkState() == Qt.CheckState.Checked else Qt.CheckState.Checked
|
|
||||||
)
|
|
||||||
return
|
|
||||||
elif button_code in BUTTONS['add_game']: # X button - install
|
|
||||||
active.install_selected(force=False)
|
|
||||||
return
|
|
||||||
elif button_code in BUTTONS['prev_dir']: # Y button - force install
|
|
||||||
active.install_selected(force=True)
|
|
||||||
return
|
|
||||||
elif button_code in BUTTONS['back']: # B button - close dialog
|
|
||||||
active.reject()
|
|
||||||
return
|
|
||||||
elif button_code in BUTTONS['prev_tab']: # LB - previous tab
|
|
||||||
current_idx = active.tab_widget.currentIndex()
|
|
||||||
new_idx = (current_idx - 1) % active.tab_widget.count()
|
|
||||||
active.tab_widget.setCurrentIndex(new_idx)
|
|
||||||
return
|
|
||||||
elif button_code in BUTTONS['next_tab']: # RB - next tab
|
|
||||||
current_idx = active.tab_widget.currentIndex()
|
|
||||||
new_idx = (current_idx + 1) % active.tab_widget.count()
|
|
||||||
active.tab_widget.setCurrentIndex(new_idx)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Standard navigation
|
# Standard navigation
|
||||||
if button_code in BUTTONS['confirm']:
|
if button_code in BUTTONS['confirm']:
|
||||||
self._parent.activateFocusedWidget()
|
self._parent.activateFocusedWidget()
|
||||||
@@ -809,83 +595,8 @@ class InputManager(QObject):
|
|||||||
|
|
||||||
@Slot(int, int, float)
|
@Slot(int, int, float)
|
||||||
def handle_dpad_slot(self, code: int, value: int, current_time: float) -> None:
|
def handle_dpad_slot(self, code: int, value: int, current_time: float) -> None:
|
||||||
keyboard = None
|
|
||||||
active_window = QApplication.activeWindow()
|
|
||||||
|
|
||||||
# Проверяем клавиатуру в активном окне (AddGameDialog или главном окне)
|
|
||||||
if isinstance(active_window, AddGameDialog):
|
|
||||||
keyboard = getattr(active_window, 'keyboard', None)
|
|
||||||
else:
|
|
||||||
keyboard = getattr(self._parent, 'keyboard', None)
|
|
||||||
|
|
||||||
# Handle release early
|
|
||||||
if value == 0:
|
|
||||||
self.current_dpad_code = None
|
|
||||||
self.current_dpad_value = 0
|
|
||||||
self.axis_moving = False
|
|
||||||
self.current_axis_delay = self.initial_axis_move_delay
|
|
||||||
self.dpad_timer.stop()
|
|
||||||
return
|
|
||||||
|
|
||||||
# Update D-pad state for continuous movement
|
|
||||||
self.current_dpad_code = code
|
|
||||||
self.current_dpad_value = value
|
|
||||||
if not self.axis_moving:
|
|
||||||
self.axis_moving = True
|
|
||||||
self.last_move_time = current_time
|
|
||||||
self.current_axis_delay = self.initial_axis_move_delay
|
|
||||||
self.dpad_timer.start(int(self.repeat_axis_move_delay * 1000))
|
|
||||||
|
|
||||||
if keyboard and keyboard.isVisible():
|
|
||||||
# Обработка горизонтального перемещения (LEFT/RIGHT)
|
|
||||||
if code in (ecodes.ABS_HAT0X, ecodes.ABS_X):
|
|
||||||
normalized_value = 0
|
|
||||||
if code == ecodes.ABS_X: # Левый стик
|
|
||||||
# Применяем мертвую зону
|
|
||||||
if abs(value) < self.dead_zone:
|
|
||||||
self.current_dpad_code = None
|
|
||||||
self.current_dpad_value = 0
|
|
||||||
self.axis_moving = False
|
|
||||||
self.dpad_timer.stop()
|
|
||||||
return
|
|
||||||
normalized_value = 1 if value > self.dead_zone else -1
|
|
||||||
else: # D-pad
|
|
||||||
normalized_value = value # D-pad уже дает -1, 0, 1
|
|
||||||
|
|
||||||
if normalized_value != 0:
|
|
||||||
if normalized_value > 0: # Вправо
|
|
||||||
keyboard.move_focus_right()
|
|
||||||
elif normalized_value < 0: # Влево
|
|
||||||
keyboard.move_focus_left()
|
|
||||||
return
|
|
||||||
|
|
||||||
# Обработка вертикального перемещения (UP/DOWN)
|
|
||||||
elif code in (ecodes.ABS_HAT0Y, ecodes.ABS_Y):
|
|
||||||
normalized_value = 0
|
|
||||||
if code == ecodes.ABS_Y: # Левый стик
|
|
||||||
# Применяем мертвую зону
|
|
||||||
if abs(value) < self.dead_zone:
|
|
||||||
self.current_dpad_code = None
|
|
||||||
self.current_dpad_value = 0
|
|
||||||
self.axis_moving = False
|
|
||||||
self.dpad_timer.stop()
|
|
||||||
return
|
|
||||||
normalized_value = 1 if value > self.dead_zone else -1
|
|
||||||
else: # D-pad
|
|
||||||
normalized_value = value # D-pad уже дает -1, 0, 1
|
|
||||||
|
|
||||||
if normalized_value != 0:
|
|
||||||
if normalized_value > 0: # Вниз
|
|
||||||
keyboard.move_focus_down()
|
|
||||||
elif normalized_value < 0: # Вверх
|
|
||||||
keyboard.move_focus_up()
|
|
||||||
return
|
|
||||||
|
|
||||||
if not self._gamepad_handling_enabled:
|
if not self._gamepad_handling_enabled:
|
||||||
return
|
return
|
||||||
if not hasattr(self._parent, 'gamesListWidget') or self._parent.gamesListWidget is None:
|
|
||||||
logger.error("gamesListWidget not available yet, skipping D-pad navigation")
|
|
||||||
return
|
|
||||||
try:
|
try:
|
||||||
|
|
||||||
app = QApplication.instance()
|
app = QApplication.instance()
|
||||||
@@ -895,6 +606,23 @@ class InputManager(QObject):
|
|||||||
if not app or not active:
|
if not app or not active:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Update D-pad state
|
||||||
|
if value != 0:
|
||||||
|
self.current_dpad_code = code
|
||||||
|
self.current_dpad_value = value
|
||||||
|
if not self.axis_moving:
|
||||||
|
self.axis_moving = True
|
||||||
|
self.last_move_time = current_time
|
||||||
|
self.current_axis_delay = self.initial_axis_move_delay
|
||||||
|
self.dpad_timer.start(int(self.repeat_axis_move_delay * 1000)) # Start timer (in milliseconds)
|
||||||
|
else:
|
||||||
|
self.current_dpad_code = None
|
||||||
|
self.current_dpad_value = 0
|
||||||
|
self.axis_moving = False
|
||||||
|
self.current_axis_delay = self.initial_axis_move_delay
|
||||||
|
self.dpad_timer.stop() # Stop timer when D-pad is released
|
||||||
|
return
|
||||||
|
|
||||||
# Handle SystemOverlay, AddGameDialog, or QMessageBox navigation with D-pad
|
# Handle SystemOverlay, AddGameDialog, or QMessageBox navigation with D-pad
|
||||||
if isinstance(active, QDialog) and code == ecodes.ABS_HAT0X and value != 0:
|
if isinstance(active, QDialog) and code == ecodes.ABS_HAT0X and value != 0:
|
||||||
if isinstance(active, QMessageBox): # Specific handling for QMessageBox
|
if isinstance(active, QMessageBox): # Specific handling for QMessageBox
|
||||||
@@ -910,7 +638,7 @@ class InputManager(QObject):
|
|||||||
elif value < 0: # Left
|
elif value < 0: # Left
|
||||||
active.focusPreviousChild()
|
active.focusPreviousChild()
|
||||||
return
|
return
|
||||||
elif isinstance(active, QDialog) and code == ecodes.ABS_HAT0Y and value != 0 and not isinstance(focused, QTableWidget): # Keep up/down for other dialogs
|
elif isinstance(active, QDialog) and code == ecodes.ABS_HAT0Y and value != 0: # Keep up/down for other dialogs
|
||||||
if not focused or not active.focusWidget():
|
if not focused or not active.focusWidget():
|
||||||
# If no widget is focused, focus the first focusable widget
|
# If no widget is focused, focus the first focusable widget
|
||||||
focusables = active.findChildren(QWidget, options=Qt.FindChildOption.FindChildrenRecursively)
|
focusables = active.findChildren(QWidget, options=Qt.FindChildOption.FindChildrenRecursively)
|
||||||
@@ -963,90 +691,132 @@ class InputManager(QObject):
|
|||||||
active.show_next()
|
active.show_next()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Library tab navigation (index 0)
|
||||||
# Table navigation
|
if self._parent.stackedWidget.currentIndex() == 0 and code in (ecodes.ABS_HAT0X, ecodes.ABS_HAT0Y):
|
||||||
if isinstance(focused, QTableWidget):
|
|
||||||
row_count = focused.rowCount()
|
|
||||||
if row_count <= 0:
|
|
||||||
return
|
|
||||||
current_row = focused.currentRow()
|
|
||||||
if current_row < 0:
|
|
||||||
current_row = 0
|
|
||||||
focused.setCurrentCell(0, 0)
|
|
||||||
|
|
||||||
if code == ecodes.ABS_HAT0Y and value != 0:
|
|
||||||
# Vertical navigation
|
|
||||||
if value > 0: # Down
|
|
||||||
new_row = min(current_row + 1, row_count - 1)
|
|
||||||
elif value < 0: # Up
|
|
||||||
new_row = max(current_row - 1, 0)
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
|
|
||||||
focused.setCurrentCell(new_row, focused.currentColumn())
|
|
||||||
item = focused.item(new_row, focused.currentColumn())
|
|
||||||
if item:
|
|
||||||
focused.scrollToItem(
|
|
||||||
item,
|
|
||||||
QAbstractItemView.ScrollHint.PositionAtCenter
|
|
||||||
)
|
|
||||||
focused.setFocus(Qt.FocusReason.OtherFocusReason)
|
|
||||||
return
|
|
||||||
elif code == ecodes.ABS_HAT0X and value != 0:
|
|
||||||
# Horizontal navigation
|
|
||||||
col_count = focused.columnCount()
|
|
||||||
current_col = focused.currentColumn()
|
|
||||||
if current_col < 0:
|
|
||||||
current_col = 0
|
|
||||||
|
|
||||||
if value < 0: # Left
|
|
||||||
new_col = max(current_col - 1, 0)
|
|
||||||
elif value > 0: # Right
|
|
||||||
new_col = min(current_col + 1, col_count - 1)
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
|
|
||||||
focused.setCurrentCell(focused.currentRow(), new_col)
|
|
||||||
focused.setFocus(Qt.FocusReason.OtherFocusReason)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Search focus logic for tabs 0 and 1
|
|
||||||
if code == ecodes.ABS_HAT0Y and value < 0:
|
|
||||||
focused = QApplication.focusWidget()
|
focused = QApplication.focusWidget()
|
||||||
current_index = self._parent.stackedWidget.currentIndex()
|
game_cards = self._parent.gamesListWidget.findChildren(GameCard)
|
||||||
if current_index in (0, 1) and isinstance(focused, GameCard):
|
if not game_cards:
|
||||||
if current_index == 0:
|
|
||||||
container = self._parent.gamesListWidget
|
|
||||||
search_edit = getattr(self._parent, 'searchEdit', None)
|
|
||||||
else:
|
|
||||||
container = self._parent.autoInstallContainer
|
|
||||||
search_edit = getattr(self._parent, 'autoInstallSearchLineEdit', None)
|
|
||||||
if container and search_edit:
|
|
||||||
game_cards = container.findChildren(GameCard)
|
|
||||||
if game_cards:
|
|
||||||
current_card_pos = focused.pos()
|
|
||||||
current_row_y = current_card_pos.y()
|
|
||||||
is_first_row = True
|
|
||||||
for card in game_cards:
|
|
||||||
if card.pos().y() < current_row_y and card.isVisible():
|
|
||||||
is_first_row = False
|
|
||||||
break
|
|
||||||
if is_first_row:
|
|
||||||
search_edit.setFocus()
|
|
||||||
return
|
|
||||||
|
|
||||||
# Game cards navigation for tabs 0 and 1
|
|
||||||
if code in (ecodes.ABS_HAT0X, ecodes.ABS_HAT0Y):
|
|
||||||
current_index = self._parent.stackedWidget.currentIndex()
|
|
||||||
if current_index in (0, 1):
|
|
||||||
container = self._parent.gamesListWidget if current_index == 0 else self._parent.autoInstallContainer
|
|
||||||
if container is None:
|
|
||||||
return
|
|
||||||
self._navigate_game_cards(container, current_index, code, value)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
scroll_area = self._parent.gamesListWidget.parentWidget()
|
||||||
|
while scroll_area and not isinstance(scroll_area, QScrollArea):
|
||||||
|
scroll_area = scroll_area.parentWidget()
|
||||||
|
|
||||||
|
# If no focused widget or not a GameCard, focus the first card
|
||||||
|
if not isinstance(focused, GameCard) or focused not in game_cards:
|
||||||
|
game_cards[0].setFocus()
|
||||||
|
if scroll_area:
|
||||||
|
scroll_area.ensureWidgetVisible(game_cards[0], 50, 50)
|
||||||
|
return
|
||||||
|
|
||||||
|
cards = self._parent.gamesListWidget.findChildren(GameCard, options=Qt.FindChildOption.FindChildrenRecursively)
|
||||||
|
if not cards:
|
||||||
|
return
|
||||||
|
# Group cards by rows with tolerance for y-position
|
||||||
|
rows = {}
|
||||||
|
y_tolerance = 10 # Allow slight variations in y-position
|
||||||
|
for card in cards:
|
||||||
|
y = card.pos().y()
|
||||||
|
matched = False
|
||||||
|
for row_y in rows:
|
||||||
|
if abs(y - row_y) <= y_tolerance:
|
||||||
|
rows[row_y].append(card)
|
||||||
|
matched = True
|
||||||
|
break
|
||||||
|
if not matched:
|
||||||
|
rows[y] = [card]
|
||||||
|
sorted_rows = sorted(rows.items(), key=lambda x: x[0])
|
||||||
|
if not sorted_rows:
|
||||||
|
return
|
||||||
|
current_row_idx = None
|
||||||
|
current_col_idx = None
|
||||||
|
for row_idx, (_y, row_cards) in enumerate(sorted_rows):
|
||||||
|
for idx, card in enumerate(row_cards):
|
||||||
|
if card == focused:
|
||||||
|
current_row_idx = row_idx
|
||||||
|
current_col_idx = idx
|
||||||
|
break
|
||||||
|
if current_row_idx is not None:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Fallback: if focused card not found, select closest row by y-position
|
||||||
|
if current_row_idx is None:
|
||||||
|
if not sorted_rows: # Additional safety check
|
||||||
|
return
|
||||||
|
focused_y = focused.pos().y()
|
||||||
|
current_row_idx = min(range(len(sorted_rows)), key=lambda i: abs(sorted_rows[i][0] - focused_y))
|
||||||
|
if current_row_idx >= len(sorted_rows): # Safety check
|
||||||
|
return
|
||||||
|
current_row = sorted_rows[current_row_idx][1]
|
||||||
|
focused_x = focused.pos().x() + focused.width() / 2
|
||||||
|
current_col_idx = min(range(len(current_row)), key=lambda i: abs((current_row[i].pos().x() + current_row[i].width() / 2) - focused_x), default=0) # type: ignore
|
||||||
|
|
||||||
|
# Add null checks before using current_row_idx and current_col_idx
|
||||||
|
if current_row_idx is None or current_col_idx is None or current_row_idx >= len(sorted_rows):
|
||||||
|
return
|
||||||
|
|
||||||
|
current_row = sorted_rows[current_row_idx][1]
|
||||||
|
if code == ecodes.ABS_HAT0X and value != 0:
|
||||||
|
if value < 0: # Left
|
||||||
|
if current_col_idx > 0:
|
||||||
|
next_card = current_row[current_col_idx - 1]
|
||||||
|
next_card.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||||
|
if scroll_area:
|
||||||
|
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
||||||
|
else:
|
||||||
|
if current_row_idx > 0:
|
||||||
|
prev_row = sorted_rows[current_row_idx - 1][1]
|
||||||
|
next_card = prev_row[-1] if prev_row else None
|
||||||
|
if next_card:
|
||||||
|
next_card.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||||
|
if scroll_area:
|
||||||
|
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
||||||
|
elif value > 0: # Right
|
||||||
|
if current_col_idx < len(current_row) - 1:
|
||||||
|
next_card = current_row[current_col_idx + 1]
|
||||||
|
next_card.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||||
|
if scroll_area:
|
||||||
|
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
||||||
|
else:
|
||||||
|
if current_row_idx < len(sorted_rows) - 1:
|
||||||
|
next_row = sorted_rows[current_row_idx + 1][1]
|
||||||
|
next_card = next_row[0] if next_row else None
|
||||||
|
if next_card:
|
||||||
|
next_card.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||||
|
if scroll_area:
|
||||||
|
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
||||||
|
elif code == ecodes.ABS_HAT0Y and value != 0:
|
||||||
|
if value > 0: # Down
|
||||||
|
if current_row_idx < len(sorted_rows) - 1:
|
||||||
|
next_row = sorted_rows[current_row_idx + 1][1]
|
||||||
|
current_x = focused.pos().x() + focused.width() / 2
|
||||||
|
next_card = min(
|
||||||
|
next_row,
|
||||||
|
key=lambda c: abs((c.pos().x() + c.width() / 2) - current_x),
|
||||||
|
default=None
|
||||||
|
)
|
||||||
|
if next_card:
|
||||||
|
next_card.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||||
|
if scroll_area:
|
||||||
|
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
||||||
|
elif value < 0: # Up
|
||||||
|
if current_row_idx > 0:
|
||||||
|
prev_row = sorted_rows[current_row_idx - 1][1]
|
||||||
|
current_x = focused.pos().x() + focused.width() / 2
|
||||||
|
next_card = min(
|
||||||
|
prev_row,
|
||||||
|
key=lambda c: abs((c.pos().x() + c.width() / 2) - current_x),
|
||||||
|
default=None
|
||||||
|
)
|
||||||
|
if next_card:
|
||||||
|
next_card.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||||
|
if scroll_area:
|
||||||
|
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
||||||
|
elif current_row_idx == 0:
|
||||||
|
self._parent.tabButtons[0].setFocus(Qt.FocusReason.OtherFocusReason)
|
||||||
|
|
||||||
# Vertical navigation in other tabs
|
# Vertical navigation in other tabs
|
||||||
if code == ecodes.ABS_HAT0Y and value != 0:
|
elif code == ecodes.ABS_HAT0Y and value != 0:
|
||||||
focused = QApplication.focusWidget()
|
focused = QApplication.focusWidget()
|
||||||
page = self._parent.stackedWidget.currentWidget()
|
page = self._parent.stackedWidget.currentWidget()
|
||||||
if value > 0: # Down
|
if value > 0: # Down
|
||||||
@@ -1066,52 +836,6 @@ class InputManager(QObject):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in handle_dpad_slot: {e}", exc_info=True)
|
logger.error(f"Error in handle_dpad_slot: {e}", exc_info=True)
|
||||||
|
|
||||||
def handle_virtual_keyboard(self, button_code: int, value: int) -> None:
|
|
||||||
# Проверяем клавиатуру в активном окне
|
|
||||||
active_window = QApplication.activeWindow()
|
|
||||||
keyboard = None
|
|
||||||
|
|
||||||
# Сначала проверяем AddGameDialog
|
|
||||||
if isinstance(active_window, AddGameDialog):
|
|
||||||
keyboard = getattr(active_window, 'keyboard', None)
|
|
||||||
else:
|
|
||||||
# Если это не AddGameDialog, проверяем клавиатуру в главном окне
|
|
||||||
keyboard = getattr(self._parent, 'keyboard', None)
|
|
||||||
|
|
||||||
if not keyboard or not isinstance(keyboard, VirtualKeyboard) or not keyboard.isVisible():
|
|
||||||
return
|
|
||||||
|
|
||||||
# Обработка кнопок геймпада
|
|
||||||
if button_code in BUTTONS['confirm']: # Кнопка A/Cross - подтверждение
|
|
||||||
if value == 1:
|
|
||||||
keyboard.activateFocusedKey()
|
|
||||||
elif button_code in BUTTONS['back']: # Кнопка B/Circle - скрыть клавиатуру
|
|
||||||
if value == 1:
|
|
||||||
keyboard.hide()
|
|
||||||
# Возвращаем фокус на поле ввода
|
|
||||||
if keyboard.current_input_widget:
|
|
||||||
keyboard.current_input_widget.setFocus()
|
|
||||||
elif button_code in BUTTONS['prev_tab']: # LB/L1 - переключение раскладки
|
|
||||||
if value == 1:
|
|
||||||
keyboard.on_lang_click()
|
|
||||||
elif button_code in BUTTONS['next_tab']: # RB/R1 - переключение Shift
|
|
||||||
if value == 1:
|
|
||||||
keyboard.on_shift_click(not keyboard.shift_pressed)
|
|
||||||
elif button_code in BUTTONS['context_menu']: # Кнопка Start - подтверждение
|
|
||||||
if value == 1:
|
|
||||||
keyboard.activateFocusedKey()
|
|
||||||
elif button_code in BUTTONS['menu']: # Кнопка Select - скрыть клавиатуру
|
|
||||||
if value == 1:
|
|
||||||
keyboard.hide()
|
|
||||||
# Возвращаем фокус на поле ввода
|
|
||||||
if keyboard.current_input_widget:
|
|
||||||
keyboard.current_input_widget.setFocus()
|
|
||||||
elif button_code in BUTTONS['add_game']: # Кнопка X - Backspace (now holdable)
|
|
||||||
if value == 1:
|
|
||||||
keyboard.on_backspace_pressed()
|
|
||||||
elif value == 0:
|
|
||||||
keyboard.stop_backspace_repeat()
|
|
||||||
|
|
||||||
def eventFilter(self, obj: QObject, event: QEvent) -> bool:
|
def eventFilter(self, obj: QObject, event: QEvent) -> bool:
|
||||||
app = QApplication.instance()
|
app = QApplication.instance()
|
||||||
if not app:
|
if not app:
|
||||||
@@ -1359,8 +1083,8 @@ class InputManager(QObject):
|
|||||||
self.gamepad = None
|
self.gamepad = None
|
||||||
if self.gamepad_thread:
|
if self.gamepad_thread:
|
||||||
self.gamepad_thread.join()
|
self.gamepad_thread.join()
|
||||||
if read_auto_fullscreen_gamepad() and not read_fullscreen_config():
|
# Signal to exit fullscreen mode
|
||||||
self.toggle_fullscreen.emit(False)
|
self.toggle_fullscreen.emit(False)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error handling udev event: {e}", exc_info=True)
|
logger.error(f"Error handling udev event: {e}", exc_info=True)
|
||||||
|
|
||||||
@@ -1418,12 +1142,11 @@ class InputManager(QObject):
|
|||||||
if not app or not active:
|
if not app or not active:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if event.type == ecodes.EV_KEY:
|
if event.type == ecodes.EV_KEY and event.value == 1:
|
||||||
# Emit on both press (1) and release (0)
|
if event.code in BUTTONS['menu'] and not self._is_gamescope_session:
|
||||||
self.button_event.emit(event.code, event.value)
|
|
||||||
# Special handling for menu on press only
|
|
||||||
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)
|
||||||
|
else:
|
||||||
|
self.button_pressed.emit(event.code)
|
||||||
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}:
|
||||||
# Проверяем, достаточно ли времени прошло с последнего срабатывания
|
# Проверяем, достаточно ли времени прошло с последнего срабатывания
|
||||||
@@ -1432,19 +1155,17 @@ class InputManager(QObject):
|
|||||||
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_pressed.emit(event.code)
|
||||||
self.last_trigger_time = now
|
self.last_trigger_time = now
|
||||||
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
|
|
||||||
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_pressed.emit(event.code)
|
||||||
self.last_trigger_time = now
|
self.last_trigger_time = now
|
||||||
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
|
|
||||||
else:
|
else:
|
||||||
self.dpad_moved.emit(event.code, event.value, now)
|
self.dpad_moved.emit(event.code, event.value, now)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
|
@@ -1,73 +0,0 @@
|
|||||||
# keyboard_layouts.py
|
|
||||||
keyboard_layouts = {
|
|
||||||
'en': {
|
|
||||||
'normal': [
|
|
||||||
['`', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '='],
|
|
||||||
['TAB', 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']', '\\'],
|
|
||||||
['CAPS', 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', "'"],
|
|
||||||
['⬆', 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/']
|
|
||||||
],
|
|
||||||
'shift': [
|
|
||||||
['~', '!', '@', '#', '$', '%', '^', '&&', '*', '(', ')', '_', '+'],
|
|
||||||
['TAB', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '{', '}', '|'],
|
|
||||||
['CAPS', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ':', '"'],
|
|
||||||
['⬆', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', '<', '>', '?']
|
|
||||||
]
|
|
||||||
},
|
|
||||||
'ru': {
|
|
||||||
'normal': [
|
|
||||||
['ё', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '='],
|
|
||||||
['TAB', 'й', 'ц', 'у', 'к', 'е', 'н', 'г', 'ш', 'щ', 'з', 'х', 'ъ', '\\'],
|
|
||||||
['CAPS', 'ф', 'ы', 'в', 'а', 'п', 'р', 'о', 'л', 'д', 'ж', 'э'],
|
|
||||||
['⬆', 'я', 'ч', 'с', 'м', 'и', 'т', 'ь', 'б', 'ю', '.']
|
|
||||||
],
|
|
||||||
'shift': [
|
|
||||||
['Ё', '!', '"', '№', ';', '%', ':', '?', '*', '(', ')', '_', '+'],
|
|
||||||
['TAB', 'Й', 'Ц', 'У', 'К', 'Е', 'Н', 'Г', 'Ш', 'Щ', 'З', 'Х', 'Ъ', '/'],
|
|
||||||
['CAPS', 'Ф', 'Ы', 'В', 'А', 'П', 'Р', 'О', 'Л', 'Д', 'Ж', 'Э'],
|
|
||||||
['⬆', 'Я', 'Ч', 'С', 'М', 'И', 'Т', 'Ь', 'Б', 'Ю', ',']
|
|
||||||
]
|
|
||||||
},
|
|
||||||
'fr': {
|
|
||||||
'normal': [
|
|
||||||
['²', '&', 'é', '"', "'", '(', '-', 'è', '_', 'ç', 'à', ')', '='],
|
|
||||||
['TAB', 'a', 'z', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '^', '$', '*'],
|
|
||||||
['CAPS', 'q', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'm', 'ù'],
|
|
||||||
['⬆', 'w', 'x', 'c', 'v', 'b', 'n', ',', ';', ':', '!']
|
|
||||||
],
|
|
||||||
'shift': [
|
|
||||||
['', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '°', '+'],
|
|
||||||
['TAB', 'A', 'Z', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '¨', '£', 'µ'],
|
|
||||||
['CAPS', 'Q', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'M', '%'],
|
|
||||||
['⬆', 'W', 'X', 'C', 'V', 'B', 'N', '?', '.', '/', '§']
|
|
||||||
]
|
|
||||||
},
|
|
||||||
'es': {
|
|
||||||
'normal': [
|
|
||||||
['º', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', "'", '¡'],
|
|
||||||
['TAB', 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '`', '+', '\\'],
|
|
||||||
['CAPS', 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'ñ', "'", 'ç'],
|
|
||||||
['⬆', 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '-']
|
|
||||||
],
|
|
||||||
'shift': [
|
|
||||||
['ª', '!', '"', '·', '$', '%', '&', '/', '(', ')', '=', '?', '¿'],
|
|
||||||
['TAB', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '^', '*', '|'],
|
|
||||||
['CAPS', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'Ñ', '"', 'Ç'],
|
|
||||||
['⬆', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', ';', ':', '_']
|
|
||||||
]
|
|
||||||
},
|
|
||||||
'de': {
|
|
||||||
'normal': [
|
|
||||||
['^', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'ß', '´'],
|
|
||||||
['TAB', 'q', 'w', 'e', 'r', 't', 'z', 'u', 'i', 'o', 'p', 'ü', '+', '#'],
|
|
||||||
['CAPS', 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'ö', 'ä'],
|
|
||||||
['⬆', '<', 'y', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '-']
|
|
||||||
],
|
|
||||||
'shift': [
|
|
||||||
['°', '!', '"', '§', '$', '%', '&', '/', '(', ')', '=', '?', '`'],
|
|
||||||
['TAB', 'Q', 'W', 'E', 'R', 'T', 'Z', 'U', 'I', 'O', 'P', 'Ü', '*', '\''],
|
|
||||||
['CAPS', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'Ö', 'Ä'],
|
|
||||||
['⬆', '>', 'Y', 'X', 'C', 'V', 'B', 'N', 'M', ';', ':', '_']
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
@@ -9,7 +9,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PROJECT VERSION\n"
|
"Project-Id-Version: PROJECT VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
"POT-Creation-Date: 2025-10-12 17:14+0500\n"
|
"POT-Creation-Date: 2025-09-23 22:23+0500\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language: de_DE\n"
|
"Language: de_DE\n"
|
||||||
@@ -191,10 +191,6 @@ msgstr ""
|
|||||||
msgid "Failed to delete custom data: {error}"
|
msgid "Failed to delete custom data: {error}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Added '{game_name}' successfully"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Game name and executable path are required"
|
msgid "Game name and executable path are required"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -308,45 +304,6 @@ msgstr ""
|
|||||||
msgid "No cover selected"
|
msgid "No cover selected"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Prefix Manager"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Set"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Libraries"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Information"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Fonts"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Settings"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Force Install"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Install"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Winetricks not found. Please try again."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Warning"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "No components selected."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Installation failed. Check logs."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Components installed successfully."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Loading Epic Games Store games..."
|
msgid "Loading Epic Games Store games..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -395,6 +352,9 @@ msgstr ""
|
|||||||
msgid "Auto Install"
|
msgid "Auto Install"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Emulators"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Wine Settings"
|
msgid "Wine Settings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -410,28 +370,6 @@ msgstr ""
|
|||||||
msgid "Fullscreen"
|
msgid "Fullscreen"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Search"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Installation already in progress."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Failed to start installation."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Processed {} installation..."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Installation completed successfully."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Installation failed."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Installation error."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Loading Steam games..."
|
msgid "Loading Steam games..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -444,106 +382,13 @@ msgstr ""
|
|||||||
msgid "Find Games ..."
|
msgid "Find Games ..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#, python-brace-format
|
msgid "Here you can configure automatic game installation..."
|
||||||
msgid "Added '{name}'"
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Compatibility tool:"
|
msgid "List of available emulators and their configuration..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Prefix:"
|
msgid "Various Wine parameters and versions..."
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Wine Configuration"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Registry Editor"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Command Prompt"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Uninstaller"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Create Prefix Backup"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Load Prefix Backup"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Delete Compatibility Tool"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Delete Prefix"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Clear Prefix"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Launching tool..."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Failed to start process."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Confirm Clear"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Are you sure you want to clear prefix '{}'?"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Prefix '{}' cleared successfully."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid ""
|
|
||||||
"Prefix '{}' cleared with errors:\n"
|
|
||||||
"{}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Failed to start backup process."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Failed to start restore process."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Prefix backup completed."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Prefix backup failed."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Prefix restore completed."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Prefix restore failed."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Are you sure you want to delete prefix '{}'?"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Prefix '{}' deleted."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Failed to delete prefix: {}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Are you sure you want to delete compatibility tool '{}'?"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Compatibility tool '{}' deleted."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Failed to delete compatibility tool: {}"
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Main PortProton parameters..."
|
msgid "Main PortProton parameters..."
|
||||||
|
@@ -9,7 +9,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PROJECT VERSION\n"
|
"Project-Id-Version: PROJECT VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
"POT-Creation-Date: 2025-10-12 17:14+0500\n"
|
"POT-Creation-Date: 2025-09-23 22:23+0500\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language: es_ES\n"
|
"Language: es_ES\n"
|
||||||
@@ -191,10 +191,6 @@ msgstr ""
|
|||||||
msgid "Failed to delete custom data: {error}"
|
msgid "Failed to delete custom data: {error}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Added '{game_name}' successfully"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Game name and executable path are required"
|
msgid "Game name and executable path are required"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -308,45 +304,6 @@ msgstr ""
|
|||||||
msgid "No cover selected"
|
msgid "No cover selected"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Prefix Manager"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Set"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Libraries"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Information"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Fonts"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Settings"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Force Install"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Install"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Winetricks not found. Please try again."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Warning"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "No components selected."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Installation failed. Check logs."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Components installed successfully."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Loading Epic Games Store games..."
|
msgid "Loading Epic Games Store games..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -395,6 +352,9 @@ msgstr ""
|
|||||||
msgid "Auto Install"
|
msgid "Auto Install"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Emulators"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Wine Settings"
|
msgid "Wine Settings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -410,28 +370,6 @@ msgstr ""
|
|||||||
msgid "Fullscreen"
|
msgid "Fullscreen"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Search"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Installation already in progress."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Failed to start installation."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Processed {} installation..."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Installation completed successfully."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Installation failed."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Installation error."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Loading Steam games..."
|
msgid "Loading Steam games..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -444,106 +382,13 @@ msgstr ""
|
|||||||
msgid "Find Games ..."
|
msgid "Find Games ..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#, python-brace-format
|
msgid "Here you can configure automatic game installation..."
|
||||||
msgid "Added '{name}'"
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Compatibility tool:"
|
msgid "List of available emulators and their configuration..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Prefix:"
|
msgid "Various Wine parameters and versions..."
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Wine Configuration"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Registry Editor"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Command Prompt"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Uninstaller"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Create Prefix Backup"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Load Prefix Backup"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Delete Compatibility Tool"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Delete Prefix"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Clear Prefix"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Launching tool..."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Failed to start process."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Confirm Clear"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Are you sure you want to clear prefix '{}'?"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Prefix '{}' cleared successfully."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid ""
|
|
||||||
"Prefix '{}' cleared with errors:\n"
|
|
||||||
"{}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Failed to start backup process."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Failed to start restore process."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Prefix backup completed."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Prefix backup failed."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Prefix restore completed."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Prefix restore failed."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Are you sure you want to delete prefix '{}'?"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Prefix '{}' deleted."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Failed to delete prefix: {}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Are you sure you want to delete compatibility tool '{}'?"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Compatibility tool '{}' deleted."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Failed to delete compatibility tool: {}"
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Main PortProton parameters..."
|
msgid "Main PortProton parameters..."
|
||||||
|
@@ -9,7 +9,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PortProtonQt 0.1.1\n"
|
"Project-Id-Version: PortProtonQt 0.1.1\n"
|
||||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
"POT-Creation-Date: 2025-10-12 17:14+0500\n"
|
"POT-Creation-Date: 2025-09-23 22:23+0500\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@@ -189,10 +189,6 @@ msgstr ""
|
|||||||
msgid "Failed to delete custom data: {error}"
|
msgid "Failed to delete custom data: {error}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Added '{game_name}' successfully"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Game name and executable path are required"
|
msgid "Game name and executable path are required"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -306,45 +302,6 @@ msgstr ""
|
|||||||
msgid "No cover selected"
|
msgid "No cover selected"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Prefix Manager"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Set"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Libraries"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Information"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Fonts"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Settings"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Force Install"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Install"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Winetricks not found. Please try again."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Warning"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "No components selected."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Installation failed. Check logs."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Components installed successfully."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Loading Epic Games Store games..."
|
msgid "Loading Epic Games Store games..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -393,6 +350,9 @@ msgstr ""
|
|||||||
msgid "Auto Install"
|
msgid "Auto Install"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Emulators"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Wine Settings"
|
msgid "Wine Settings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -408,28 +368,6 @@ msgstr ""
|
|||||||
msgid "Fullscreen"
|
msgid "Fullscreen"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Search"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Installation already in progress."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Failed to start installation."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Processed {} installation..."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Installation completed successfully."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Installation failed."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Installation error."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Loading Steam games..."
|
msgid "Loading Steam games..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -442,106 +380,13 @@ msgstr ""
|
|||||||
msgid "Find Games ..."
|
msgid "Find Games ..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#, python-brace-format
|
msgid "Here you can configure automatic game installation..."
|
||||||
msgid "Added '{name}'"
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Compatibility tool:"
|
msgid "List of available emulators and their configuration..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Prefix:"
|
msgid "Various Wine parameters and versions..."
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Wine Configuration"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Registry Editor"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Command Prompt"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Uninstaller"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Create Prefix Backup"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Load Prefix Backup"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Delete Compatibility Tool"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Delete Prefix"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Clear Prefix"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Launching tool..."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Failed to start process."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Confirm Clear"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Are you sure you want to clear prefix '{}'?"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Prefix '{}' cleared successfully."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid ""
|
|
||||||
"Prefix '{}' cleared with errors:\n"
|
|
||||||
"{}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Failed to start backup process."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Failed to start restore process."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Prefix backup completed."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Prefix backup failed."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Prefix restore completed."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Prefix restore failed."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Are you sure you want to delete prefix '{}'?"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Prefix '{}' deleted."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Failed to delete prefix: {}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Are you sure you want to delete compatibility tool '{}'?"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Compatibility tool '{}' deleted."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Failed to delete compatibility tool: {}"
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Main PortProton parameters..."
|
msgid "Main PortProton parameters..."
|
||||||
|
@@ -9,17 +9,18 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PROJECT VERSION\n"
|
"Project-Id-Version: PROJECT VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
"POT-Creation-Date: 2025-10-12 17:14+0500\n"
|
"POT-Creation-Date: 2025-09-23 22:23+0500\n"
|
||||||
"PO-Revision-Date: 2025-10-12 17:13+0500\n"
|
"PO-Revision-Date: 2025-09-23 22:23+0500\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language: ru_RU\n"
|
|
||||||
"Language-Team: ru_RU <LL@li.org>\n"
|
"Language-Team: ru_RU <LL@li.org>\n"
|
||||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
|
"Language: ru_RU\n"
|
||||||
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=utf-8\n"
|
"Content-Type: text/plain; charset=utf-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 "
|
||||||
|
"&& (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
||||||
"Generated-By: Babel 2.17.0\n"
|
"Generated-By: Babel 2.17.0\n"
|
||||||
|
"X-Generator: Poedit 3.6\n"
|
||||||
|
|
||||||
msgid "Error"
|
msgid "Error"
|
||||||
msgstr "Ошибка"
|
msgstr "Ошибка"
|
||||||
@@ -86,11 +87,11 @@ msgstr "Успешно"
|
|||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"'{game_name}' was added to Steam. Please restart Steam for changes to "
|
"'{game_name}' was added to Steam. Please restart Steam for changes to take "
|
||||||
"take effect."
|
"effect."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"'{game_name}' был(а) добавлен(а) в Steam. Пожалуйста, перезапустите "
|
"'{game_name}' был(а) добавлен(а) в Steam. Пожалуйста, перезапустите Steam, "
|
||||||
"Steam, чтобы изменения вступили в силу."
|
"чтобы изменения вступили в силу."
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Executable not found for game: {game_name}"
|
msgid "Executable not found for game: {game_name}"
|
||||||
@@ -178,11 +179,11 @@ msgstr "Подтвердите удаление"
|
|||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"Are you sure you want to delete '{game_name}'? This will remove the "
|
"Are you sure you want to delete '{game_name}'? This will remove the .desktop "
|
||||||
".desktop file and custom data."
|
"file and custom data."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Вы уверены, что хотите удалить '{game_name}'? Это приведёт к удалению "
|
"Вы уверены, что хотите удалить '{game_name}'? Это приведёт к удалению файла ."
|
||||||
"файла .desktop и пользовательских данных."
|
"desktop и пользовательских данных."
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Failed to delete .desktop file: {error}"
|
msgid "Failed to delete .desktop file: {error}"
|
||||||
@@ -196,10 +197,6 @@ msgstr "'{game_name}' был(а) успешно удалён(а)"
|
|||||||
msgid "Failed to delete custom data: {error}"
|
msgid "Failed to delete custom data: {error}"
|
||||||
msgstr "Не удалось удалить пользовательские данные: {error}"
|
msgstr "Не удалось удалить пользовательские данные: {error}"
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Added '{game_name}' successfully"
|
|
||||||
msgstr "'{game_name}' успешно добавлен(а)"
|
|
||||||
|
|
||||||
msgid "Game name and executable path are required"
|
msgid "Game name and executable path are required"
|
||||||
msgstr "Требуются название игры и путь к исполняемому файлу"
|
msgstr "Требуются название игры и путь к исполняемому файлу"
|
||||||
|
|
||||||
@@ -228,11 +225,11 @@ msgstr "Не удалось добавить '{game_name}' в Steam: {error}"
|
|||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"'{game_name}' was removed from Steam. Please restart Steam for changes to"
|
"'{game_name}' was removed from Steam. Please restart Steam for changes to take "
|
||||||
" take effect."
|
"effect."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"'{game_name}' был(а) удалён(а) из Steam. Пожалуйста, перезапустите Steam,"
|
"'{game_name}' был(а) удалён(а) из Steam. Пожалуйста, перезапустите Steam, чтобы "
|
||||||
" чтобы изменения вступили в силу."
|
"изменения вступили в силу."
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Failed to remove game '{game_name}' from Steam: {error}"
|
msgid "Failed to remove game '{game_name}' from Steam: {error}"
|
||||||
@@ -277,7 +274,7 @@ msgstr "Путь: "
|
|||||||
|
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Access denied: %s"
|
msgid "Access denied: %s"
|
||||||
msgstr "Доступ запрещён: %s"
|
msgstr "Доступ запрещен: %s"
|
||||||
|
|
||||||
msgid "Edit Game"
|
msgid "Edit Game"
|
||||||
msgstr "Редактировать игру"
|
msgstr "Редактировать игру"
|
||||||
@@ -315,45 +312,6 @@ msgstr "Скачивание обложки..."
|
|||||||
msgid "No cover selected"
|
msgid "No cover selected"
|
||||||
msgstr "Обложка не выбрана"
|
msgstr "Обложка не выбрана"
|
||||||
|
|
||||||
msgid "Prefix Manager"
|
|
||||||
msgstr "Менеджер префиксов"
|
|
||||||
|
|
||||||
msgid "Set"
|
|
||||||
msgstr "Выбор"
|
|
||||||
|
|
||||||
msgid "Libraries"
|
|
||||||
msgstr "Библиотеки"
|
|
||||||
|
|
||||||
msgid "Information"
|
|
||||||
msgstr "Описание"
|
|
||||||
|
|
||||||
msgid "Fonts"
|
|
||||||
msgstr "Шрифты"
|
|
||||||
|
|
||||||
msgid "Settings"
|
|
||||||
msgstr "Настройки"
|
|
||||||
|
|
||||||
msgid "Force Install"
|
|
||||||
msgstr "Принудительно установить"
|
|
||||||
|
|
||||||
msgid "Install"
|
|
||||||
msgstr "Установить"
|
|
||||||
|
|
||||||
msgid "Winetricks not found. Please try again."
|
|
||||||
msgstr "Winetricks не найден. Повторите попытку."
|
|
||||||
|
|
||||||
msgid "Warning"
|
|
||||||
msgstr "Предупреждение"
|
|
||||||
|
|
||||||
msgid "No components selected."
|
|
||||||
msgstr "Не выбрано ни одного компонента."
|
|
||||||
|
|
||||||
msgid "Installation failed. Check logs."
|
|
||||||
msgstr "Установка не удалась. Проверьте журналы."
|
|
||||||
|
|
||||||
msgid "Components installed successfully."
|
|
||||||
msgstr "Компоненты успешно установлены."
|
|
||||||
|
|
||||||
msgid "Loading Epic Games Store games..."
|
msgid "Loading Epic Games Store games..."
|
||||||
msgstr "Загрузка игр из Epic Games Store..."
|
msgstr "Загрузка игр из Epic Games Store..."
|
||||||
|
|
||||||
@@ -402,6 +360,9 @@ msgstr "Библиотека"
|
|||||||
msgid "Auto Install"
|
msgid "Auto Install"
|
||||||
msgstr "Автоустановка"
|
msgstr "Автоустановка"
|
||||||
|
|
||||||
|
msgid "Emulators"
|
||||||
|
msgstr "Эмуляторы"
|
||||||
|
|
||||||
msgid "Wine Settings"
|
msgid "Wine Settings"
|
||||||
msgstr "Настройки wine"
|
msgstr "Настройки wine"
|
||||||
|
|
||||||
@@ -417,28 +378,6 @@ msgstr "Назад"
|
|||||||
msgid "Fullscreen"
|
msgid "Fullscreen"
|
||||||
msgstr "Полный экран"
|
msgstr "Полный экран"
|
||||||
|
|
||||||
msgid "Search"
|
|
||||||
msgstr "Поиск"
|
|
||||||
|
|
||||||
msgid "Installation already in progress."
|
|
||||||
msgstr "Установка уже выполняется."
|
|
||||||
|
|
||||||
msgid "Failed to start installation."
|
|
||||||
msgstr "Не удалось запустить установку."
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Processed {} installation..."
|
|
||||||
msgstr "В процессе установки {}..."
|
|
||||||
|
|
||||||
msgid "Installation completed successfully."
|
|
||||||
msgstr "Установка завершена успешно."
|
|
||||||
|
|
||||||
msgid "Installation failed."
|
|
||||||
msgstr "Установка не удалась."
|
|
||||||
|
|
||||||
msgid "Installation error."
|
|
||||||
msgstr "Ошибка установки."
|
|
||||||
|
|
||||||
msgid "Loading Steam games..."
|
msgid "Loading Steam games..."
|
||||||
msgstr "Загрузка игр из Steam..."
|
msgstr "Загрузка игр из Steam..."
|
||||||
|
|
||||||
@@ -451,109 +390,14 @@ msgstr "Игровая библиотека"
|
|||||||
msgid "Find Games ..."
|
msgid "Find Games ..."
|
||||||
msgstr "Найти игры..."
|
msgstr "Найти игры..."
|
||||||
|
|
||||||
#, python-brace-format
|
msgid "Here you can configure automatic game installation..."
|
||||||
msgid "Added '{name}'"
|
msgstr "Здесь можно настроить автоматическую установку игр..."
|
||||||
msgstr "'{name}' добавлен(а)"
|
|
||||||
|
|
||||||
msgid "Compatibility tool:"
|
msgid "List of available emulators and their configuration..."
|
||||||
msgstr "Инструмент совместимости:"
|
msgstr "Список доступных эмуляторов и их настройка..."
|
||||||
|
|
||||||
msgid "Prefix:"
|
msgid "Various Wine parameters and versions..."
|
||||||
msgstr "Префикс:"
|
msgstr "Различные параметры и версии wine..."
|
||||||
|
|
||||||
msgid "Wine Configuration"
|
|
||||||
msgstr "Конфигурация Wine"
|
|
||||||
|
|
||||||
msgid "Registry Editor"
|
|
||||||
msgstr "Редактор реестра"
|
|
||||||
|
|
||||||
msgid "Command Prompt"
|
|
||||||
msgstr "Командная строка"
|
|
||||||
|
|
||||||
msgid "Uninstaller"
|
|
||||||
msgstr "Удаление программ"
|
|
||||||
|
|
||||||
msgid "Create Prefix Backup"
|
|
||||||
msgstr "Создать резервную копию префикса"
|
|
||||||
|
|
||||||
msgid "Load Prefix Backup"
|
|
||||||
msgstr "Загрузить резервную копию префикса"
|
|
||||||
|
|
||||||
msgid "Delete Compatibility Tool"
|
|
||||||
msgstr "Удалить Инструмент совместимости"
|
|
||||||
|
|
||||||
msgid "Delete Prefix"
|
|
||||||
msgstr "Удалить Префикс"
|
|
||||||
|
|
||||||
msgid "Clear Prefix"
|
|
||||||
msgstr "Очистить Префикс"
|
|
||||||
|
|
||||||
msgid "Launching tool..."
|
|
||||||
msgstr "Запуск инструмента..."
|
|
||||||
|
|
||||||
msgid "Failed to start process."
|
|
||||||
msgstr "Не удалось запустить процесс."
|
|
||||||
|
|
||||||
msgid "Confirm Clear"
|
|
||||||
msgstr "Подтвердите очистку"
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Are you sure you want to clear prefix '{}'?"
|
|
||||||
msgstr "Вы уверены, что хотите очистить префикс «{}»?"
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Prefix '{}' cleared successfully."
|
|
||||||
msgstr "Префикс '{}' успешно удален."
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid ""
|
|
||||||
"Prefix '{}' cleared with errors:\n"
|
|
||||||
"{}"
|
|
||||||
msgstr ""
|
|
||||||
"Префикс '{}' очищен с ошибками:\n"
|
|
||||||
"{}"
|
|
||||||
|
|
||||||
msgid "Failed to start backup process."
|
|
||||||
msgstr "Не удалось запустить процесс резервного копирования."
|
|
||||||
|
|
||||||
msgid "Failed to start restore process."
|
|
||||||
msgstr "Не удалось запустить процесс восстановления."
|
|
||||||
|
|
||||||
msgid "Prefix backup completed."
|
|
||||||
msgstr "Резервное копирование префикса завершено."
|
|
||||||
|
|
||||||
msgid "Prefix backup failed."
|
|
||||||
msgstr "Сбой резервного копирования префикса."
|
|
||||||
|
|
||||||
msgid "Prefix restore completed."
|
|
||||||
msgstr "Восстановление префикса завершено."
|
|
||||||
|
|
||||||
msgid "Prefix restore failed."
|
|
||||||
msgstr "Восстановление префикса не удалось."
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Are you sure you want to delete prefix '{}'?"
|
|
||||||
msgstr "Вы уверены, что хотите удалить префикс «{}»?"
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Prefix '{}' deleted."
|
|
||||||
msgstr "Префикс «{}» удален."
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Failed to delete prefix: {}"
|
|
||||||
msgstr "Не удалось удалить префикс: {}"
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Are you sure you want to delete compatibility tool '{}'?"
|
|
||||||
msgstr "Вы уверены, что хотите удалить инструмент совместимости «{}»?"
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Compatibility tool '{}' deleted."
|
|
||||||
msgstr "Инструмент совместимости «{}» удален."
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Failed to delete compatibility tool: {}"
|
|
||||||
msgstr "Не удалось удалить инструмент совместимости: {}"
|
|
||||||
|
|
||||||
msgid "Main PortProton parameters..."
|
msgid "Main PortProton parameters..."
|
||||||
msgstr "Основные параметры PortProton..."
|
msgstr "Основные параметры PortProton..."
|
||||||
@@ -638,8 +482,7 @@ msgstr "Подтвердите удаление"
|
|||||||
|
|
||||||
msgid "Are you sure you want to reset all settings? This action cannot be undone."
|
msgid "Are you sure you want to reset all settings? This action cannot be undone."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Вы уверены, что хотите сбросить все настройки? Это действие нельзя "
|
"Вы уверены, что хотите сбросить все настройки? Это действие нельзя отменить."
|
||||||
"отменить."
|
|
||||||
|
|
||||||
msgid "Settings reset. Restarting..."
|
msgid "Settings reset. Restarting..."
|
||||||
msgstr "Настройки сброшены. Перезапуск..."
|
msgstr "Настройки сброшены. Перезапуск..."
|
||||||
@@ -811,4 +654,3 @@ msgstr "Нет избранных"
|
|||||||
|
|
||||||
msgid "No recent games"
|
msgid "No recent games"
|
||||||
msgstr "Нет недавних игр"
|
msgstr "Нет недавних игр"
|
||||||
|
|
||||||
|
@@ -4,12 +4,9 @@ import orjson
|
|||||||
import requests
|
import requests
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import time
|
import time
|
||||||
import glob
|
|
||||||
import re
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from portprotonqt.downloader import Downloader
|
from portprotonqt.downloader import Downloader
|
||||||
from portprotonqt.logger import get_logger
|
from portprotonqt.logger import get_logger
|
||||||
from portprotonqt.config_utils import get_portproton_location
|
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
CACHE_DURATION = 30 * 24 * 60 * 60 # 30 days in seconds
|
CACHE_DURATION = 30 * 24 * 60 * 60 # 30 days in seconds
|
||||||
@@ -55,9 +52,6 @@ class PortProtonAPI:
|
|||||||
self.xdg_data_home = os.getenv("XDG_DATA_HOME", os.path.join(os.path.expanduser("~"), ".local", "share"))
|
self.xdg_data_home = os.getenv("XDG_DATA_HOME", os.path.join(os.path.expanduser("~"), ".local", "share"))
|
||||||
self.custom_data_dir = os.path.join(self.xdg_data_home, "PortProtonQt", "custom_data")
|
self.custom_data_dir = os.path.join(self.xdg_data_home, "PortProtonQt", "custom_data")
|
||||||
os.makedirs(self.custom_data_dir, exist_ok=True)
|
os.makedirs(self.custom_data_dir, exist_ok=True)
|
||||||
self.portproton_location = get_portproton_location()
|
|
||||||
self.repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
||||||
self.builtin_custom_folder = os.path.join(self.repo_root, "custom_data")
|
|
||||||
self._topics_data = None
|
self._topics_data = None
|
||||||
|
|
||||||
def _get_game_dir(self, exe_name: str) -> str:
|
def _get_game_dir(self, exe_name: str) -> str:
|
||||||
@@ -74,6 +68,40 @@ class PortProtonAPI:
|
|||||||
logger.debug(f"Failed to check file at {url}: {e}")
|
logger.debug(f"Failed to check file at {url}: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def download_game_assets(self, exe_name: str, timeout: int = 5) -> dict[str, str | None]:
|
||||||
|
game_dir = self._get_game_dir(exe_name)
|
||||||
|
results: dict[str, str | None] = {"cover": None, "metadata": None}
|
||||||
|
cover_extensions = [".png", ".jpg", ".jpeg", ".bmp"]
|
||||||
|
cover_url_base = f"{self.base_url}/{exe_name}/cover"
|
||||||
|
metadata_url = f"{self.base_url}/{exe_name}/metadata.txt"
|
||||||
|
|
||||||
|
for ext in cover_extensions:
|
||||||
|
cover_url = f"{cover_url_base}{ext}"
|
||||||
|
if self._check_file_exists(cover_url, timeout):
|
||||||
|
local_cover_path = os.path.join(game_dir, f"cover{ext}")
|
||||||
|
result = self.downloader.download(cover_url, local_cover_path, timeout=timeout)
|
||||||
|
if result:
|
||||||
|
results["cover"] = result
|
||||||
|
logger.info(f"Downloaded cover for {exe_name} to {result}")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
logger.error(f"Failed to download cover for {exe_name} from {cover_url}")
|
||||||
|
else:
|
||||||
|
logger.debug(f"No cover found for {exe_name} with extension {ext}")
|
||||||
|
|
||||||
|
if self._check_file_exists(metadata_url, timeout):
|
||||||
|
local_metadata_path = os.path.join(game_dir, "metadata.txt")
|
||||||
|
result = self.downloader.download(metadata_url, local_metadata_path, timeout=timeout)
|
||||||
|
if result:
|
||||||
|
results["metadata"] = result
|
||||||
|
logger.info(f"Downloaded metadata for {exe_name} to {result}")
|
||||||
|
else:
|
||||||
|
logger.error(f"Failed to download metadata for {exe_name} from {metadata_url}")
|
||||||
|
else:
|
||||||
|
logger.debug(f"No metadata found for {exe_name}")
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
def download_game_assets_async(self, exe_name: str, timeout: int = 5, callback: Callable[[dict[str, str | None]], None] | None = None) -> None:
|
def download_game_assets_async(self, exe_name: str, timeout: int = 5, callback: Callable[[dict[str, str | None]], None] | None = None) -> None:
|
||||||
game_dir = self._get_game_dir(exe_name)
|
game_dir = self._get_game_dir(exe_name)
|
||||||
cover_extensions = [".png", ".jpg", ".jpeg", ".bmp"]
|
cover_extensions = [".png", ".jpg", ".jpeg", ".bmp"]
|
||||||
@@ -135,164 +163,6 @@ class PortProtonAPI:
|
|||||||
if callback:
|
if callback:
|
||||||
callback(results)
|
callback(results)
|
||||||
|
|
||||||
def download_autoinstall_cover_async(self, exe_name: str, timeout: int = 5, callback: Callable[[str | None], None] | None = None) -> None:
|
|
||||||
"""Download only autoinstall cover image (PNG only, no metadata)."""
|
|
||||||
xdg_data_home = os.getenv("XDG_DATA_HOME",
|
|
||||||
os.path.join(os.path.expanduser("~"), ".local", "share"))
|
|
||||||
autoinstall_root = os.path.join(xdg_data_home, "PortProtonQt", "custom_data", "autoinstall")
|
|
||||||
user_game_folder = os.path.join(autoinstall_root, exe_name)
|
|
||||||
|
|
||||||
if not os.path.isdir(user_game_folder):
|
|
||||||
try:
|
|
||||||
os.mkdir(user_game_folder)
|
|
||||||
except FileExistsError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
cover_url = f"{self.base_url}/{exe_name}/cover.png"
|
|
||||||
local_cover_path = os.path.join(user_game_folder, "cover.png")
|
|
||||||
|
|
||||||
def on_cover_downloaded(local_path: str | None):
|
|
||||||
if local_path:
|
|
||||||
logger.info(f"Async autoinstall cover downloaded for {exe_name}: {local_path}")
|
|
||||||
else:
|
|
||||||
logger.debug(f"No autoinstall cover downloaded for {exe_name}")
|
|
||||||
if callback:
|
|
||||||
callback(local_path)
|
|
||||||
|
|
||||||
if self._check_file_exists(cover_url, timeout):
|
|
||||||
self.downloader.download_async(
|
|
||||||
cover_url,
|
|
||||||
local_cover_path,
|
|
||||||
timeout=timeout,
|
|
||||||
callback=on_cover_downloaded
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logger.debug(f"No autoinstall cover found for {exe_name}")
|
|
||||||
if callback:
|
|
||||||
callback(None)
|
|
||||||
|
|
||||||
def parse_autoinstall_script(self, file_path: str) -> tuple[str | None, str | None]:
|
|
||||||
"""Extract display_name from # name comment and exe_name from autoinstall bash script."""
|
|
||||||
try:
|
|
||||||
with open(file_path, encoding='utf-8') as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
# Skip emulators
|
|
||||||
if re.search(r'#\s*type\s*:\s*emulators', content, re.IGNORECASE):
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
display_name = None
|
|
||||||
exe_name = None
|
|
||||||
|
|
||||||
# Extract display_name from "# name:" comment
|
|
||||||
name_match = re.search(r'#\s*name\s*:\s*(.+)', content, re.IGNORECASE)
|
|
||||||
if name_match:
|
|
||||||
display_name = name_match.group(1).strip()
|
|
||||||
|
|
||||||
# --- pw_create_unique_exe ---
|
|
||||||
pw_match = re.search(r'pw_create_unique_exe(?:\s+["\']([^"\']+)["\'])?', content)
|
|
||||||
if pw_match:
|
|
||||||
arg = pw_match.group(1)
|
|
||||||
if arg:
|
|
||||||
exe_name = arg.strip()
|
|
||||||
if not exe_name.lower().endswith(".exe"):
|
|
||||||
exe_name += ".exe"
|
|
||||||
else:
|
|
||||||
export_match = re.search(
|
|
||||||
r'export\s+PORTWINE_CREATE_SHORTCUT_NAME\s*=\s*["\']([^"\']+)["\']',
|
|
||||||
content, re.IGNORECASE)
|
|
||||||
if export_match:
|
|
||||||
exe_name = f"{export_match.group(1).strip()}.exe"
|
|
||||||
|
|
||||||
else:
|
|
||||||
portwine_match = None
|
|
||||||
for line in content.splitlines():
|
|
||||||
stripped = line.strip()
|
|
||||||
if stripped.startswith("#"):
|
|
||||||
continue
|
|
||||||
if "portwine_exe" in stripped and "=" in stripped:
|
|
||||||
portwine_match = stripped
|
|
||||||
break
|
|
||||||
|
|
||||||
if portwine_match:
|
|
||||||
exe_expr = portwine_match.split("=", 1)[1].strip().strip("'\" ")
|
|
||||||
exe_candidates = re.findall(r'[-\w\s/\\\.]+\.exe', exe_expr)
|
|
||||||
if exe_candidates:
|
|
||||||
exe_name = os.path.basename(exe_candidates[-1].strip())
|
|
||||||
|
|
||||||
|
|
||||||
# Fallback
|
|
||||||
if not display_name and exe_name:
|
|
||||||
display_name = exe_name
|
|
||||||
|
|
||||||
return display_name, exe_name
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Failed to parse {file_path}: {e}")
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
def get_autoinstall_games_async(self, callback: Callable[[list[tuple]], None]) -> None:
|
|
||||||
"""Load auto-install games with user/builtin covers (no async download here)."""
|
|
||||||
games = []
|
|
||||||
auto_dir = os.path.join(self.portproton_location or "", "data", "scripts", "pw_autoinstall") if self.portproton_location else ""
|
|
||||||
if not os.path.exists(auto_dir):
|
|
||||||
callback(games)
|
|
||||||
return
|
|
||||||
|
|
||||||
scripts = sorted(glob.glob(os.path.join(auto_dir, "*")))
|
|
||||||
if not scripts:
|
|
||||||
callback(games)
|
|
||||||
return
|
|
||||||
|
|
||||||
xdg_data_home = os.getenv("XDG_DATA_HOME",
|
|
||||||
os.path.join(os.path.expanduser("~"), ".local", "share"))
|
|
||||||
base_autoinstall_dir = os.path.join(xdg_data_home, "PortProtonQt", "custom_data", "autoinstall")
|
|
||||||
os.makedirs(base_autoinstall_dir, exist_ok=True)
|
|
||||||
|
|
||||||
for script_path in scripts:
|
|
||||||
display_name, exe_name = self.parse_autoinstall_script(script_path)
|
|
||||||
script_name = os.path.splitext(os.path.basename(script_path))[0]
|
|
||||||
|
|
||||||
if not (display_name and exe_name):
|
|
||||||
continue
|
|
||||||
|
|
||||||
exe_name = os.path.splitext(exe_name)[0] # Без .exe
|
|
||||||
user_game_folder = os.path.join(base_autoinstall_dir, exe_name)
|
|
||||||
os.makedirs(user_game_folder, exist_ok=True)
|
|
||||||
|
|
||||||
# Поиск обложки
|
|
||||||
cover_path = ""
|
|
||||||
user_files = set(os.listdir(user_game_folder)) if os.path.exists(user_game_folder) else set()
|
|
||||||
for ext in [".jpg", ".png", ".jpeg", ".bmp"]:
|
|
||||||
candidate = f"cover{ext}"
|
|
||||||
if candidate in user_files:
|
|
||||||
cover_path = os.path.join(user_game_folder, candidate)
|
|
||||||
break
|
|
||||||
|
|
||||||
if not cover_path:
|
|
||||||
logger.debug(f"No local cover found for autoinstall {exe_name}")
|
|
||||||
|
|
||||||
# Формируем кортеж игры (добавлен exe_name в конец)
|
|
||||||
game_tuple = (
|
|
||||||
display_name, # name
|
|
||||||
"", # description
|
|
||||||
cover_path, # cover
|
|
||||||
"", # appid
|
|
||||||
f"autoinstall:{script_name}", # exec_line
|
|
||||||
"", # controller_support
|
|
||||||
"Never", # last_launch
|
|
||||||
"0h 0m", # formatted_playtime
|
|
||||||
"", # protondb_tier
|
|
||||||
"", # anticheat_status
|
|
||||||
0, # last_played
|
|
||||||
0, # playtime_seconds
|
|
||||||
"autoinstall", # game_source
|
|
||||||
exe_name # exe_name
|
|
||||||
)
|
|
||||||
games.append(game_tuple)
|
|
||||||
|
|
||||||
callback(games)
|
|
||||||
|
|
||||||
def _load_topics_data(self):
|
def _load_topics_data(self):
|
||||||
"""Load and cache linux_gaming_topics_min.json from the archive."""
|
"""Load and cache linux_gaming_topics_min.json from the archive."""
|
||||||
if self._topics_data is not None:
|
if self._topics_data is not None:
|
||||||
|
@@ -4,6 +4,7 @@ from PySide6.QtCore import QRect
|
|||||||
from PySide6.QtGui import QPainter, QPen, QBrush, Qt, QColor, QConicalGradient
|
from PySide6.QtGui import QPainter, QPen, QBrush, Qt, QColor, QConicalGradient
|
||||||
from PySide6.QtWidgets import QWidget
|
from PySide6.QtWidgets import QWidget
|
||||||
|
|
||||||
|
|
||||||
class Preloader(QWidget):
|
class Preloader(QWidget):
|
||||||
def __init__(self, speed=180.0, line_line_width=20, color=QColor(0, 120, 215), parent=None):
|
def __init__(self, speed=180.0, line_line_width=20, color=QColor(0, 120, 215), parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
@@ -211,28 +211,14 @@ def normalize_name(s):
|
|||||||
|
|
||||||
def is_valid_candidate(candidate):
|
def is_valid_candidate(candidate):
|
||||||
"""
|
"""
|
||||||
Determines whether a given candidate string is valid for use as a game name.
|
Checks if a candidate contains forbidden substrings:
|
||||||
|
- win32
|
||||||
The function performs the following checks:
|
- win64
|
||||||
1. Normalizes the candidate using `normalize_name()`.
|
- gamelauncher
|
||||||
2. Rejects the candidate if the normalized name is exactly "game"
|
Additionally checks the string without spaces.
|
||||||
(to avoid overly generic names).
|
Returns True if the candidate is valid, otherwise False.
|
||||||
3. Removes spaces and checks for forbidden substrings:
|
|
||||||
- "win32"
|
|
||||||
- "win64"
|
|
||||||
- "gamelauncher"
|
|
||||||
These are checked in the space-free version of the string.
|
|
||||||
4. Returns True only if none of the forbidden conditions are met.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
candidate (str): The candidate string to validate.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if the candidate is valid, False otherwise.
|
|
||||||
"""
|
"""
|
||||||
normalized_candidate = normalize_name(candidate)
|
normalized_candidate = normalize_name(candidate)
|
||||||
if normalized_candidate == "game":
|
|
||||||
return False
|
|
||||||
normalized_no_space = normalized_candidate.replace(" ", "")
|
normalized_no_space = normalized_candidate.replace(" ", "")
|
||||||
forbidden = ["win32", "win64", "gamelauncher"]
|
forbidden = ["win32", "win64", "gamelauncher"]
|
||||||
for token in forbidden:
|
for token in forbidden:
|
||||||
|
@@ -1 +0,0 @@
|
|||||||
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m47 24a23 23 0 0 1-23 23 23 23 0 0 1-23-23 23 23 0 0 1 23-23 23 23 0 0 1 23 23z" fill="#3f424d"/><path d="M 42.513,24 A 18.513,18.513 0 0 1 24,42.513 18.513,18.513 0 0 1 5.4869995,24 18.513,18.513 0 0 1 24,5.4869995 18.513,18.513 0 0 1 42.513,24 Z" fill="#fff"/><path d="m33.019 13.476h-18.037c-0.8274 0-1.5059 0.67847-1.5059 1.5059v18.037c0 0.8274 0.67847 1.5059 1.5059 1.5059h18.037c0.8274 0 1.5059-0.67847 1.5059-1.5059v-18.037c0-0.8274-0.66192-1.5059-1.5059-1.5059zm-1.4893 18.037h-15.026v-15.026h15.026z" fill="#3f424d" stroke-width="1.6548"/></svg>
|
|
Before Width: | Height: | Size: 682 B |
@@ -1 +0,0 @@
|
|||||||
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m47 24a23 23 0 0 1-23 23 23 23 0 0 1-23-23 23 23 0 0 1 23-23 23 23 0 0 1 23 23z" fill="#3f424d"/><path d="M 42.513,24 A 18.513,18.513 0 0 1 24,42.513 18.513,18.513 0 0 1 5.4869995,24 18.513,18.513 0 0 1 24,5.4869995 18.513,18.513 0 0 1 42.513,24 Z" fill="#fff"/><path d="m21.438 26.092-7.6218-12.616h5.7406l4.4433 8.238 4.4109-8.238h5.7731l-7.6866 12.552v8.4974h-5.0595z" fill="#3f424d" stroke-width="1.0811" aria-label="Y"/></svg>
|
|
Before Width: | Height: | Size: 559 B |
@@ -217,56 +217,6 @@ CONTEXT_MENU_STYLE = f"""
|
|||||||
}}
|
}}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
VIRTUAL_KEYBOARD_STYLE = """
|
|
||||||
VirtualKeyboard {
|
|
||||||
background-color: rgba(30, 30, 30, 200);
|
|
||||||
border-radius: 0px;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
QPushButton {
|
|
||||||
font-size: 14px;
|
|
||||||
border: 1px solid #555;
|
|
||||||
border-top-color: #666;
|
|
||||||
border-left-color: #666;
|
|
||||||
border-radius: 3px;
|
|
||||||
min-width: 30px;
|
|
||||||
min-height: 30px;
|
|
||||||
padding: 4px;
|
|
||||||
background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #505050, stop:1 #404040);
|
|
||||||
color: #e0e0e0;
|
|
||||||
}
|
|
||||||
QPushButton:hover {
|
|
||||||
background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #606060, stop:1 #505050);
|
|
||||||
border: 1px solid #666;
|
|
||||||
border-top-color: #777;
|
|
||||||
border-left-color: #777;
|
|
||||||
}
|
|
||||||
QPushButton:focus {
|
|
||||||
border: 2px solid #4a90e2;
|
|
||||||
background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #5a5a5a, stop:1 #454545);
|
|
||||||
}
|
|
||||||
QPushButton:pressed {
|
|
||||||
background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #3a3a3a, stop:1 #303030);
|
|
||||||
border: 1px solid #444;
|
|
||||||
border-bottom-color: #555;
|
|
||||||
border-right-color: #555;
|
|
||||||
padding-top: 5px;
|
|
||||||
padding-bottom: 3px;
|
|
||||||
padding-left: 5px;
|
|
||||||
padding-right: 3px;
|
|
||||||
}
|
|
||||||
QPushButton[checked="true"] {
|
|
||||||
background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #4a90e2, stop:1 #3a7ad2);
|
|
||||||
color: white;
|
|
||||||
border: 1px solid #2a6ac2;
|
|
||||||
border-top-color: #5aa0f2;
|
|
||||||
border-left-color: #5aa0f2;
|
|
||||||
}
|
|
||||||
QPushButton[checked="true"]:focus {
|
|
||||||
border: 2px solid #6aa3f5;
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
# ГЛОБАЛЬНЫЙ СТИЛЬ ДЛЯ ОКНА (ФОН), ЛЭЙБЛОВ, КНОПОК
|
# ГЛОБАЛЬНЫЙ СТИЛЬ ДЛЯ ОКНА (ФОН), ЛЭЙБЛОВ, КНОПОК
|
||||||
MAIN_WINDOW_STYLE = f"""
|
MAIN_WINDOW_STYLE = f"""
|
||||||
QWidget {{
|
QWidget {{
|
||||||
@@ -966,96 +916,6 @@ SETTINGS_CHECKBOX_STYLE = f"""
|
|||||||
}}
|
}}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
WINETRICKS_TAB_STYLE = f"""
|
|
||||||
QTabWidget::pane {{
|
|
||||||
border: 1px solid {color_d};
|
|
||||||
background: {color_b};
|
|
||||||
border-radius: {border_radius_a};
|
|
||||||
}}
|
|
||||||
QTabBar::tab {{
|
|
||||||
background: {color_c};
|
|
||||||
color: {color_f};
|
|
||||||
padding: 8px 16px;
|
|
||||||
border-top-left-radius: {border_radius_a};
|
|
||||||
border-top-right-radius: {border_radius_a};
|
|
||||||
margin-right: 2px;
|
|
||||||
}}
|
|
||||||
QTabBar::tab:selected {{
|
|
||||||
background: {color_a};
|
|
||||||
color: {color_f};
|
|
||||||
}}
|
|
||||||
QTabBar::tab:hover {{
|
|
||||||
background: {color_e};
|
|
||||||
}}
|
|
||||||
"""
|
|
||||||
|
|
||||||
WINETRICKS_TABBLE_STYLE = f"""
|
|
||||||
QTableWidget {{
|
|
||||||
background: {color_c};
|
|
||||||
color: {color_f};
|
|
||||||
gridline-color: {color_d};
|
|
||||||
alternate-background-color: {color_d};
|
|
||||||
border: {border_a};
|
|
||||||
border-radius: {border_radius_a};
|
|
||||||
font-family: '{font_family}';
|
|
||||||
font-size: {font_size_a};
|
|
||||||
}}
|
|
||||||
QHeaderView::section {{
|
|
||||||
background: {color_d};
|
|
||||||
color: {color_f};
|
|
||||||
padding: 5px;
|
|
||||||
border: {border_a};
|
|
||||||
font-weight: bold;
|
|
||||||
}}
|
|
||||||
QTableWidget::item {{
|
|
||||||
padding: 8px;
|
|
||||||
border-bottom: 1px solid {color_d};
|
|
||||||
}}
|
|
||||||
QTableWidget::item:selected {{
|
|
||||||
background: {color_a};
|
|
||||||
color: {color_f};
|
|
||||||
}}
|
|
||||||
QTableWidget::item:hover {{
|
|
||||||
background: {color_e};
|
|
||||||
}}
|
|
||||||
QTableWidget::indicator {{
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
border: {border_b} {color_a};
|
|
||||||
border-radius: {border_radius_a};
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
}}
|
|
||||||
QTableWidget::indicator:unchecked {{
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
image: none;
|
|
||||||
}}
|
|
||||||
QTableWidget::indicator:checked {{
|
|
||||||
background: {color_a};
|
|
||||||
image: url({theme_manager.get_icon("check", current_theme_name, as_path=True)});
|
|
||||||
border: {border_b} {color_f};
|
|
||||||
}}
|
|
||||||
QTableWidget::indicator:hover {{
|
|
||||||
background: rgba(255, 255, 255, 0.2);
|
|
||||||
border: {border_b} {color_a};
|
|
||||||
}}
|
|
||||||
QTableWidget::indicator:focus {{
|
|
||||||
border: {border_c} {color_a};
|
|
||||||
}}
|
|
||||||
{SCROLL_AREA_STYLE}
|
|
||||||
"""
|
|
||||||
|
|
||||||
WINETRICKS_LOG_STYLE = f"""
|
|
||||||
QTextEdit {{
|
|
||||||
background: {color_c};
|
|
||||||
border: {border_a};
|
|
||||||
border-radius: {border_radius_a};
|
|
||||||
color: {color_f};
|
|
||||||
font-family: '{font_family}';
|
|
||||||
font-size: {font_size_a};
|
|
||||||
padding: 5px;
|
|
||||||
}}
|
|
||||||
"""
|
|
||||||
|
|
||||||
FILE_EXPLORER_STYLE = f"""
|
FILE_EXPLORER_STYLE = f"""
|
||||||
QListView {{
|
QListView {{
|
||||||
font-size: {font_size_a};
|
font-size: {font_size_a};
|
||||||
|
@@ -1,586 +0,0 @@
|
|||||||
from typing import cast
|
|
||||||
from PySide6.QtWidgets import (QFrame, QVBoxLayout, QPushButton, QGridLayout,
|
|
||||||
QSizePolicy, QWidget, QLineEdit)
|
|
||||||
from PySide6.QtCore import Qt, Signal, QProcess
|
|
||||||
from portprotonqt.keyboard_layouts import keyboard_layouts
|
|
||||||
from portprotonqt.theme_manager import ThemeManager
|
|
||||||
from portprotonqt.config_utils import read_theme_from_config
|
|
||||||
|
|
||||||
theme_manager = ThemeManager()
|
|
||||||
|
|
||||||
class VirtualKeyboard(QFrame):
|
|
||||||
keyPressed = Signal(str)
|
|
||||||
|
|
||||||
def __init__(self, parent: QWidget | None = None, theme=None, button_width: int = 80):
|
|
||||||
super().__init__(parent)
|
|
||||||
self._parent: QWidget | None = parent
|
|
||||||
self.available_layouts: list[str] = self.get_layouts_setxkbmap()
|
|
||||||
self.theme = theme if theme else theme_manager.apply_theme(read_theme_from_config())
|
|
||||||
if not self.available_layouts:
|
|
||||||
self.available_layouts.append('en')
|
|
||||||
self.current_layout: str = self.available_layouts[0]
|
|
||||||
|
|
||||||
self.focus_timer = None
|
|
||||||
self.focus_delay = 150 # Задержка между перемещениями в мс
|
|
||||||
self.last_focus_time = 0
|
|
||||||
|
|
||||||
self.backspace_pressed = False
|
|
||||||
self.backspace_timer = None
|
|
||||||
self.backspace_initial_delay = 500
|
|
||||||
self.backspace_repeat_delay = 50
|
|
||||||
self.gamepad_x_pressed = False
|
|
||||||
self.caps_lock = False
|
|
||||||
self.shift_pressed = False
|
|
||||||
self.current_input_widget = None
|
|
||||||
self.cursor_visible = True
|
|
||||||
self.last_focused_button = None
|
|
||||||
|
|
||||||
self.base_button_width = 40
|
|
||||||
self.base_min_width = 574
|
|
||||||
self.button_width = button_width
|
|
||||||
self.button_height = 40
|
|
||||||
self.spacing = 4
|
|
||||||
self.margins = 10
|
|
||||||
self.num_cols = 14
|
|
||||||
|
|
||||||
self.initUI()
|
|
||||||
self.hide()
|
|
||||||
|
|
||||||
self.setStyleSheet(self.theme.VIRTUAL_KEYBOARD_STYLE)
|
|
||||||
|
|
||||||
def highlight_cursor_position(self):
|
|
||||||
"""Подсвечиваем текущую позицию курсора"""
|
|
||||||
if not self.current_input_widget or not isinstance(self.current_input_widget, QLineEdit):
|
|
||||||
return
|
|
||||||
|
|
||||||
# Просто устанавливаем курсор на нужную позицию без выделения
|
|
||||||
self.current_input_widget.setCursorPosition(self.current_input_widget.cursorPosition())
|
|
||||||
|
|
||||||
def initUI(self):
|
|
||||||
layout = QVBoxLayout()
|
|
||||||
layout.setContentsMargins(0, 0, 0, 0)
|
|
||||||
layout.setSpacing(0)
|
|
||||||
|
|
||||||
self.keyboard_layout = QGridLayout()
|
|
||||||
self.keyboard_layout.setSpacing(self.spacing)
|
|
||||||
self.keyboard_layout.setContentsMargins(self.margins // 2, self.margins // 2, self.margins // 2, self.margins // 2)
|
|
||||||
self.create_keyboard()
|
|
||||||
|
|
||||||
self.keyboard_container = QWidget()
|
|
||||||
self.keyboard_container.setLayout(self.keyboard_layout)
|
|
||||||
ratio = self.button_width / self.base_button_width
|
|
||||||
self.keyboard_container.setMinimumWidth(int(self.base_min_width * ratio))
|
|
||||||
self.keyboard_container.setMinimumHeight(220)
|
|
||||||
self.keyboard_container.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
|
||||||
|
|
||||||
layout.addWidget(self.keyboard_container, 0, Qt.AlignmentFlag.AlignHCenter)
|
|
||||||
self.setLayout(layout)
|
|
||||||
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
|
||||||
|
|
||||||
def run_shell_command(self, cmd: str) -> str | None:
|
|
||||||
process = QProcess(self)
|
|
||||||
process.start("sh", ["-c", cmd])
|
|
||||||
process.waitForFinished(-1)
|
|
||||||
if process.exitCode() == 0:
|
|
||||||
output_bytes = process.readAllStandardOutput().data()
|
|
||||||
if isinstance(output_bytes, memoryview):
|
|
||||||
output_str = output_bytes.tobytes().decode('utf-8').strip()
|
|
||||||
else:
|
|
||||||
output_str = output_bytes.decode('utf-8').strip()
|
|
||||||
return output_str
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_layouts_setxkbmap(self) -> list[str]:
|
|
||||||
"""Получаем раскладки, которые используются в системе, возвращаем список вида ['us', 'ru'] и т.п."""
|
|
||||||
cmd = r'''localectl status | awk -F: '/X11 Layout/ {gsub(/^[ \t]+/, "", $2); print $2}' '''
|
|
||||||
output = self.run_shell_command(cmd)
|
|
||||||
if output:
|
|
||||||
layouts = [lang.strip() for lang in output.split(',') if lang.strip()]
|
|
||||||
return layouts if layouts else ['en']
|
|
||||||
else:
|
|
||||||
return ['en']
|
|
||||||
|
|
||||||
def create_keyboard(self):
|
|
||||||
# TODO: сделать нормальное описание (сейчас лень)
|
|
||||||
# Основные раскладки с учетом Shift
|
|
||||||
# Фильтруем доступные раскладки
|
|
||||||
|
|
||||||
LAYOUT_MAP = {'us': 'en'}
|
|
||||||
|
|
||||||
# Assume keyboard_layouts is dict[str, dict[str, list[list[str]]]]
|
|
||||||
self.layouts: dict[str, dict[str, list[list[str]]]] = {
|
|
||||||
lang: keyboard_layouts.get(LAYOUT_MAP.get(lang, lang), keyboard_layouts['en'])
|
|
||||||
for lang in self.available_layouts
|
|
||||||
}
|
|
||||||
|
|
||||||
self.current_layout = (self.current_layout if self.current_layout in self.layouts else next(iter(self.layouts.keys()), None) or 'en')
|
|
||||||
|
|
||||||
self.buttons: dict[str, QPushButton] = {}
|
|
||||||
self.update_keyboard()
|
|
||||||
|
|
||||||
def update_keyboard(self):
|
|
||||||
coords = self._save_focused_coords()
|
|
||||||
|
|
||||||
# Очищаем предыдущие кнопки
|
|
||||||
while self.keyboard_layout.count():
|
|
||||||
item = self.keyboard_layout.takeAt(0)
|
|
||||||
if item.widget():
|
|
||||||
item.widget().deleteLater()
|
|
||||||
|
|
||||||
fixed_w = self.button_width
|
|
||||||
fixed_h = self.button_height
|
|
||||||
|
|
||||||
# Выбираем текущую раскладку (обычная или с shift)
|
|
||||||
layout_mode = 'shift' if self.shift_pressed else 'normal'
|
|
||||||
layout_data = self.layouts.get(self.current_layout, {})
|
|
||||||
buttons: list[list[str]] = layout_data.get(layout_mode, [])
|
|
||||||
|
|
||||||
# Добавляем основные кнопки
|
|
||||||
for row_idx, row in enumerate(buttons):
|
|
||||||
for col_idx, key in enumerate(row):
|
|
||||||
button = QPushButton(key)
|
|
||||||
button.setFixedSize(fixed_w, fixed_h)
|
|
||||||
|
|
||||||
# Обработчики для CAPS и левого Shift
|
|
||||||
if key == 'CAPS':
|
|
||||||
button.setCheckable(True)
|
|
||||||
button.setChecked(self.caps_lock)
|
|
||||||
button.clicked.connect(self.on_caps_click)
|
|
||||||
elif key == '⬆': # Левый Shift
|
|
||||||
button.setCheckable(True)
|
|
||||||
button.setChecked(self.shift_pressed)
|
|
||||||
button.clicked.connect(lambda checked: self.on_shift_click(checked))
|
|
||||||
else:
|
|
||||||
button.clicked.connect(lambda checked=False, k=key: self.on_button_click(k))
|
|
||||||
|
|
||||||
self.keyboard_layout.addWidget(button, row_idx, col_idx)
|
|
||||||
self.buttons[key] = button
|
|
||||||
|
|
||||||
# Нижний ряд (специальные кнопки)
|
|
||||||
shift = QPushButton('⬆')
|
|
||||||
shift.setFixedSize(fixed_w * 3 + 2 * self.spacing, fixed_h)
|
|
||||||
shift.setCheckable(True)
|
|
||||||
shift.setChecked(self.shift_pressed)
|
|
||||||
shift.clicked.connect(lambda checked: self.on_shift_click(checked))
|
|
||||||
self.keyboard_layout.addWidget(shift, 3, 11, 1, 3)
|
|
||||||
|
|
||||||
button = QPushButton('CAPS')
|
|
||||||
button.setCheckable(True)
|
|
||||||
button.setChecked(self.caps_lock)
|
|
||||||
button.clicked.connect(self.on_caps_click)
|
|
||||||
|
|
||||||
space = QPushButton('Space')
|
|
||||||
space.setFixedSize(fixed_w * 5 + 4 * self.spacing, fixed_h)
|
|
||||||
space.clicked.connect(lambda: self.on_button_click(' '))
|
|
||||||
self.keyboard_layout.addWidget(space, 4, 1, 1, 5)
|
|
||||||
|
|
||||||
backspace = QPushButton('⌫')
|
|
||||||
backspace.setFixedSize(fixed_w, fixed_h)
|
|
||||||
backspace.pressed.connect(self.on_backspace_pressed)
|
|
||||||
backspace.released.connect(self.stop_backspace_repeat)
|
|
||||||
self.keyboard_layout.addWidget(backspace, 0, 13, 1, 1)
|
|
||||||
|
|
||||||
enter = QPushButton('Enter')
|
|
||||||
enter.setFixedSize(fixed_w * 2 + self.spacing, fixed_h)
|
|
||||||
enter.clicked.connect(self.on_enter_click)
|
|
||||||
self.keyboard_layout.addWidget(enter, 2, 12, 1, 2)
|
|
||||||
|
|
||||||
lang = QPushButton('🌐')
|
|
||||||
lang.setFixedSize(fixed_w, fixed_h)
|
|
||||||
lang.clicked.connect(self.on_lang_click)
|
|
||||||
self.keyboard_layout.addWidget(lang, 4, 0, 1, 1)
|
|
||||||
|
|
||||||
clear = QPushButton('Clear')
|
|
||||||
clear.setFixedSize(fixed_w * 2 + self.spacing, fixed_h)
|
|
||||||
clear.clicked.connect(self.on_clear_click)
|
|
||||||
self.keyboard_layout.addWidget(clear, 4, 10, 1, 2)
|
|
||||||
|
|
||||||
up = QPushButton('▲')
|
|
||||||
up.setFixedSize(fixed_w, fixed_h)
|
|
||||||
up.clicked.connect(self.up_key) # Обработка клика мышью - управление курсором
|
|
||||||
self.keyboard_layout.addWidget(up, 4, 6, 1, 1)
|
|
||||||
|
|
||||||
down = QPushButton('▼')
|
|
||||||
down.setFixedSize(fixed_w, fixed_h)
|
|
||||||
down.clicked.connect(self.down_key)
|
|
||||||
self.keyboard_layout.addWidget(down, 4, 7, 1, 1)
|
|
||||||
|
|
||||||
left = QPushButton('◄')
|
|
||||||
left.setFixedSize(fixed_w, fixed_h)
|
|
||||||
left.clicked.connect(self.left_key)
|
|
||||||
self.keyboard_layout.addWidget(left, 4, 8, 1, 1)
|
|
||||||
|
|
||||||
right = QPushButton('►')
|
|
||||||
right.setFixedSize(fixed_w, fixed_h)
|
|
||||||
right.clicked.connect(self.right_key)
|
|
||||||
self.keyboard_layout.addWidget(right, 4, 9, 1, 1)
|
|
||||||
|
|
||||||
hide_button = QPushButton('Hide')
|
|
||||||
hide_button.setFixedSize(fixed_w * 2 + self.spacing, fixed_h)
|
|
||||||
hide_button.clicked.connect(self.hide)
|
|
||||||
self.keyboard_layout.addWidget(hide_button, 4, 12, 1, 2)
|
|
||||||
|
|
||||||
if coords:
|
|
||||||
row, col = coords
|
|
||||||
item = self.keyboard_layout.itemAtPosition(row, col)
|
|
||||||
if item and item.widget():
|
|
||||||
item.widget().setFocus()
|
|
||||||
|
|
||||||
def up_key(self):
|
|
||||||
"""Перемещает курсор в QLineEdit вверх/в начало, если клавиатура видима"""
|
|
||||||
if self.current_input_widget and isinstance(self.current_input_widget, QLineEdit):
|
|
||||||
self.current_input_widget.setCursorPosition(0)
|
|
||||||
self.current_input_widget.setFocus()
|
|
||||||
|
|
||||||
def down_key(self):
|
|
||||||
"""Перемещает курсор в QLineEdit вниз/в конец, если клавиатура видима"""
|
|
||||||
if self.current_input_widget and isinstance(self.current_input_widget, QLineEdit):
|
|
||||||
self.current_input_widget.setCursorPosition(len(self.current_input_widget.text()))
|
|
||||||
self.current_input_widget.setFocus()
|
|
||||||
|
|
||||||
def left_key(self):
|
|
||||||
"""Перемещает курсор в QLineEdit влево, если клавиатура видима"""
|
|
||||||
if self.current_input_widget and isinstance(self.current_input_widget, QLineEdit):
|
|
||||||
pos = self.current_input_widget.cursorPosition()
|
|
||||||
if pos > 0:
|
|
||||||
self.current_input_widget.setCursorPosition(pos - 1)
|
|
||||||
self.current_input_widget.setFocus()
|
|
||||||
|
|
||||||
def right_key(self):
|
|
||||||
"""Перемещает курсор в QLineEdit вправо, если клавиатура видима"""
|
|
||||||
if self.current_input_widget and isinstance(self.current_input_widget, QLineEdit):
|
|
||||||
pos = self.current_input_widget.cursorPosition()
|
|
||||||
text_len = len(self.current_input_widget.text())
|
|
||||||
if pos < text_len:
|
|
||||||
self.current_input_widget.setCursorPosition(pos + 1)
|
|
||||||
self.current_input_widget.setFocus()
|
|
||||||
|
|
||||||
def move_focus_up(self):
|
|
||||||
"""Перемещает фокус по кнопкам клавиатуры вверх с фиксированной скоростью"""
|
|
||||||
current_time = self.get_current_time()
|
|
||||||
if current_time - self.last_focus_time >= self.focus_delay:
|
|
||||||
self.focusNextKey("up")
|
|
||||||
self.last_focus_time = current_time
|
|
||||||
|
|
||||||
def move_focus_down(self):
|
|
||||||
"""Перемещает фокус по кнопкам клавиатуры вниз с фиксированной скоростью"""
|
|
||||||
current_time = self.get_current_time()
|
|
||||||
if current_time - self.last_focus_time >= self.focus_delay:
|
|
||||||
self.focusNextKey("down")
|
|
||||||
self.last_focus_time = current_time
|
|
||||||
|
|
||||||
def move_focus_left(self):
|
|
||||||
"""Перемещает фокус по кнопкам клавиатуры влево с фиксированной скоростью"""
|
|
||||||
current_time = self.get_current_time()
|
|
||||||
if current_time - self.last_focus_time >= self.focus_delay:
|
|
||||||
self.focusNextKey("left")
|
|
||||||
self.last_focus_time = current_time
|
|
||||||
|
|
||||||
def move_focus_right(self):
|
|
||||||
"""Перемещает фокус по кнопкам клавиатуры вправо с фиксированной скоростью"""
|
|
||||||
current_time = self.get_current_time()
|
|
||||||
if current_time - self.last_focus_time >= self.focus_delay:
|
|
||||||
self.focusNextKey("right")
|
|
||||||
self.last_focus_time = current_time
|
|
||||||
|
|
||||||
def get_current_time(self):
|
|
||||||
"""Возвращает текущее время в миллисекундах"""
|
|
||||||
from time import time
|
|
||||||
return int(time() * 1000)
|
|
||||||
|
|
||||||
def _save_focused_coords(self) -> tuple[int, int] | None:
|
|
||||||
"""Возвращает (row, col) кнопки с фокусом или None"""
|
|
||||||
current = self.focusWidget()
|
|
||||||
if not current:
|
|
||||||
return None
|
|
||||||
idx = self.keyboard_layout.indexOf(current)
|
|
||||||
if idx == -1:
|
|
||||||
return None
|
|
||||||
position = cast(tuple[int, int, int, int], self.keyboard_layout.getItemPosition(idx))
|
|
||||||
return position[:2] # row, col
|
|
||||||
|
|
||||||
def on_button_click(self, key):
|
|
||||||
if key in ['TAB', 'CAPS', '⬆']:
|
|
||||||
if key == 'TAB':
|
|
||||||
self.on_tab_click()
|
|
||||||
elif key == 'CAPS':
|
|
||||||
self.on_caps_click()
|
|
||||||
elif key == '⬆':
|
|
||||||
self.on_shift_click(not self.shift_pressed)
|
|
||||||
self.highlight_cursor_position()
|
|
||||||
elif self.current_input_widget is not None:
|
|
||||||
# Сохраняем текущую кнопку с фокусом
|
|
||||||
focused_button = self.focusWidget()
|
|
||||||
key_to_restore = None
|
|
||||||
if isinstance(focused_button, QPushButton) and focused_button in self.buttons.values():
|
|
||||||
key_to_restore = next((k for k, btn in self.buttons.items() if btn == focused_button), None)
|
|
||||||
|
|
||||||
key = "&" if key == "&&" else key
|
|
||||||
cursor_pos = self.current_input_widget.cursorPosition()
|
|
||||||
text = self.current_input_widget.text()
|
|
||||||
new_text = text[:cursor_pos] + key + text[cursor_pos:]
|
|
||||||
self.current_input_widget.setText(new_text)
|
|
||||||
self.current_input_widget.setCursorPosition(cursor_pos + len(key))
|
|
||||||
self.keyPressed.emit(key)
|
|
||||||
self.highlight_cursor_position()
|
|
||||||
|
|
||||||
# Если был нажат SHIFT, но не CapsLock, отключаем его после ввода символа
|
|
||||||
if self.shift_pressed and not self.caps_lock:
|
|
||||||
self.shift_pressed = False
|
|
||||||
self.update_keyboard()
|
|
||||||
if key_to_restore and key_to_restore in self.buttons:
|
|
||||||
self.buttons[key_to_restore].setFocus()
|
|
||||||
|
|
||||||
def on_tab_click(self):
|
|
||||||
if self.current_input_widget is not None:
|
|
||||||
self.current_input_widget.insert('\t')
|
|
||||||
self.keyPressed.emit('Tab')
|
|
||||||
self.current_input_widget.setFocus()
|
|
||||||
self.highlight_cursor_position()
|
|
||||||
|
|
||||||
def on_caps_click(self):
|
|
||||||
"""Включаем/выключаем CapsLock"""
|
|
||||||
self.caps_lock = not self.caps_lock
|
|
||||||
self.shift_pressed = self.caps_lock
|
|
||||||
self.update_keyboard()
|
|
||||||
|
|
||||||
# ---------- таймерное событие ----------
|
|
||||||
def timerEvent(self, event):
|
|
||||||
if event.timerId() == self.backspace_timer:
|
|
||||||
self.on_backspace_click() # стираем ещё один символ
|
|
||||||
# первое срабатывание прошло – ускоряем
|
|
||||||
if self.backspace_timer:
|
|
||||||
self.killTimer(self.backspace_timer)
|
|
||||||
self.backspace_timer = self.startTimer(self.backspace_repeat_delay)
|
|
||||||
def on_backspace_click(self):
|
|
||||||
"""Обработка одного нажатия Backspace"""
|
|
||||||
if self.current_input_widget is not None:
|
|
||||||
cursor_pos = self.current_input_widget.cursorPosition()
|
|
||||||
text = self.current_input_widget.text()
|
|
||||||
|
|
||||||
if cursor_pos > 0:
|
|
||||||
new_text = text[:cursor_pos - 1] + text[cursor_pos:]
|
|
||||||
self.current_input_widget.setText(new_text)
|
|
||||||
self.current_input_widget.setCursorPosition(cursor_pos - 1)
|
|
||||||
self.keyPressed.emit('Backspace')
|
|
||||||
self.highlight_cursor_position()
|
|
||||||
|
|
||||||
def on_backspace_pressed(self):
|
|
||||||
"""Обработка зажатого Backspace"""
|
|
||||||
self.backspace_pressed = True
|
|
||||||
self.start_backspace_repeat()
|
|
||||||
|
|
||||||
def start_backspace_repeat(self):
|
|
||||||
"""Запуск автоповтора нажатия Backspace"""
|
|
||||||
self.on_backspace_click() # Первое нажатие
|
|
||||||
self.backspace_timer = self.startTimer(self.backspace_initial_delay)
|
|
||||||
|
|
||||||
def stop_backspace_repeat(self):
|
|
||||||
"""Остановка автоповтора нажатия Backspace"""
|
|
||||||
if self.backspace_timer:
|
|
||||||
self.killTimer(self.backspace_timer)
|
|
||||||
self.backspace_timer = None
|
|
||||||
self.backspace_pressed = False
|
|
||||||
|
|
||||||
def on_enter_click(self):
|
|
||||||
"""Обработка действия кнопки Enter"""
|
|
||||||
# TODO: тут подумать, как обрабатывать нажатие.
|
|
||||||
# Пока болванка перехода на новую строку, в QlineEdit работает как нажатие пробела
|
|
||||||
if self.current_input_widget is not None:
|
|
||||||
self.current_input_widget.insert('\n')
|
|
||||||
self.keyPressed.emit('Enter')
|
|
||||||
|
|
||||||
def on_clear_click(self):
|
|
||||||
"""Чистим строку от введённого текста"""
|
|
||||||
if self.current_input_widget is not None:
|
|
||||||
self.current_input_widget.clear()
|
|
||||||
self.keyPressed.emit('Clear')
|
|
||||||
self.highlight_cursor_position()
|
|
||||||
|
|
||||||
def on_lang_click(self):
|
|
||||||
"""Переключение раскладки"""
|
|
||||||
if not self.available_layouts:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
current_index = self.available_layouts.index(self.current_layout)
|
|
||||||
next_index = (current_index + 1) % len(self.available_layouts)
|
|
||||||
self.current_layout = self.available_layouts[next_index]
|
|
||||||
except ValueError:
|
|
||||||
# Если текущей раскладки нет в available_layouts
|
|
||||||
self.current_layout = self.available_layouts[0] if self.available_layouts else 'en'
|
|
||||||
|
|
||||||
self.update_keyboard()
|
|
||||||
|
|
||||||
def on_shift_click(self, checked):
|
|
||||||
self.shift_pressed = checked
|
|
||||||
if not checked and self.caps_lock:
|
|
||||||
self.caps_lock = False
|
|
||||||
self.update_keyboard()
|
|
||||||
|
|
||||||
def show_for_widget(self, widget):
|
|
||||||
self.current_input_widget = widget
|
|
||||||
if widget:
|
|
||||||
widget.setFocus()
|
|
||||||
self.highlight_cursor_position()
|
|
||||||
|
|
||||||
# Позиционирование клавиатуры внизу родительского виджета
|
|
||||||
if self._parent and isinstance(self._parent, QWidget):
|
|
||||||
keyboard_height = 220
|
|
||||||
self.setFixedWidth(self._parent.width())
|
|
||||||
self.setFixedHeight(keyboard_height)
|
|
||||||
self.move(0, self._parent.height() - keyboard_height)
|
|
||||||
|
|
||||||
self.show()
|
|
||||||
self.raise_()
|
|
||||||
|
|
||||||
# Установить фокус на первую кнопку, если нет фокуса на виджете ввода
|
|
||||||
if not widget:
|
|
||||||
first_button: QPushButton | None = next((cast(QPushButton, btn) for btn in self.buttons.values()), None)
|
|
||||||
if first_button:
|
|
||||||
first_button.setFocus()
|
|
||||||
|
|
||||||
def activateFocusedKey(self):
|
|
||||||
"""Активирует текущую выделенную кнопку на клавиатуре"""
|
|
||||||
focused = self.focusWidget()
|
|
||||||
if isinstance(focused, QPushButton):
|
|
||||||
focused.animateClick()
|
|
||||||
|
|
||||||
def focusNextKey(self, direction: str):
|
|
||||||
"""Перемещает фокус на следующую кнопку в указанном направлении с обертыванием"""
|
|
||||||
current = self.focusWidget()
|
|
||||||
if not current:
|
|
||||||
first_button = self.findFirstFocusableButton()
|
|
||||||
if first_button:
|
|
||||||
first_button.setFocus()
|
|
||||||
return
|
|
||||||
|
|
||||||
current_idx = self.keyboard_layout.indexOf(current)
|
|
||||||
if current_idx == -1:
|
|
||||||
return
|
|
||||||
|
|
||||||
position = cast(tuple[int, int, int, int], self.keyboard_layout.getItemPosition(current_idx))
|
|
||||||
current_row, current_col, row_span, col_span = position
|
|
||||||
|
|
||||||
num_rows = self.keyboard_layout.rowCount()
|
|
||||||
num_cols = self.keyboard_layout.columnCount()
|
|
||||||
|
|
||||||
found = False
|
|
||||||
|
|
||||||
if direction == "right":
|
|
||||||
# Сначала ищем в той же строке вправо
|
|
||||||
search_row = current_row
|
|
||||||
search_col = current_col + col_span
|
|
||||||
while search_col < num_cols:
|
|
||||||
item = self.keyboard_layout.itemAtPosition(search_row, search_col)
|
|
||||||
if item and item.widget() and item.widget().isEnabled():
|
|
||||||
next_button = cast(QPushButton, item.widget())
|
|
||||||
next_button.setFocus()
|
|
||||||
found = True
|
|
||||||
break
|
|
||||||
search_col += 1
|
|
||||||
|
|
||||||
if not found:
|
|
||||||
# Переходим к следующей строке, начиная с col 0
|
|
||||||
search_row = (current_row + 1) % num_rows
|
|
||||||
search_col = 0
|
|
||||||
# Ищем первую кнопку в этой строке
|
|
||||||
while search_col < num_cols:
|
|
||||||
item = self.keyboard_layout.itemAtPosition(search_row, search_col)
|
|
||||||
if item and item.widget() and item.widget().isEnabled():
|
|
||||||
next_button = cast(QPushButton, item.widget())
|
|
||||||
next_button.setFocus()
|
|
||||||
found = True
|
|
||||||
break
|
|
||||||
search_col += 1
|
|
||||||
|
|
||||||
elif direction == "left":
|
|
||||||
# Сначала ищем в той же строке влево
|
|
||||||
search_row = current_row
|
|
||||||
search_col = current_col - 1
|
|
||||||
while search_col >= 0:
|
|
||||||
item = self.keyboard_layout.itemAtPosition(search_row, search_col)
|
|
||||||
if item and item.widget() and item.widget().isEnabled():
|
|
||||||
next_button = cast(QPushButton, item.widget())
|
|
||||||
next_button.setFocus()
|
|
||||||
found = True
|
|
||||||
break
|
|
||||||
search_col -= 1
|
|
||||||
|
|
||||||
if not found:
|
|
||||||
# Переходим к предыдущей строке, начиная с последнего столбца
|
|
||||||
search_row = (current_row - 1) % num_rows
|
|
||||||
search_col = num_cols - 1
|
|
||||||
# Ищем последнюю кнопку в этой строке
|
|
||||||
while search_col >= 0:
|
|
||||||
item = self.keyboard_layout.itemAtPosition(search_row, search_col)
|
|
||||||
if item and item.widget() and item.widget().isEnabled():
|
|
||||||
next_button = cast(QPushButton, item.widget())
|
|
||||||
next_button.setFocus()
|
|
||||||
found = True
|
|
||||||
break
|
|
||||||
search_col -= 1
|
|
||||||
|
|
||||||
elif direction == "down":
|
|
||||||
# Сначала ищем в том же столбце вниз
|
|
||||||
search_col = current_col
|
|
||||||
search_row = current_row + row_span
|
|
||||||
while search_row < num_rows:
|
|
||||||
item = self.keyboard_layout.itemAtPosition(search_row, search_col)
|
|
||||||
if item and item.widget() and item.widget().isEnabled():
|
|
||||||
next_button = cast(QPushButton, item.widget())
|
|
||||||
next_button.setFocus()
|
|
||||||
found = True
|
|
||||||
break
|
|
||||||
search_row += 1
|
|
||||||
|
|
||||||
if not found:
|
|
||||||
# Переходим к следующему столбцу, начиная с row 0
|
|
||||||
search_col = (current_col + col_span) % num_cols
|
|
||||||
search_row = 0
|
|
||||||
# Ищем первую кнопку в этом столбце
|
|
||||||
while search_row < num_rows:
|
|
||||||
item = self.keyboard_layout.itemAtPosition(search_row, search_col)
|
|
||||||
if item and item.widget() and item.widget().isEnabled():
|
|
||||||
next_button = cast(QPushButton, item.widget())
|
|
||||||
next_button.setFocus()
|
|
||||||
found = True
|
|
||||||
break
|
|
||||||
search_row += 1
|
|
||||||
|
|
||||||
elif direction == "up":
|
|
||||||
# Сначала ищем в том же столбце вверх
|
|
||||||
search_col = current_col
|
|
||||||
search_row = current_row - 1
|
|
||||||
while search_row >= 0:
|
|
||||||
item = self.keyboard_layout.itemAtPosition(search_row, search_col)
|
|
||||||
if item and item.widget() and item.widget().isEnabled():
|
|
||||||
next_button = cast(QPushButton, item.widget())
|
|
||||||
next_button.setFocus()
|
|
||||||
found = True
|
|
||||||
break
|
|
||||||
search_row -= 1
|
|
||||||
|
|
||||||
if not found:
|
|
||||||
# Переходим к предыдущему столбцу, начиная с последней строки
|
|
||||||
search_col = (current_col - 1) % num_cols
|
|
||||||
search_row = num_rows - 1
|
|
||||||
# Ищем последнюю кнопку в этом столбце
|
|
||||||
while search_row >= 0:
|
|
||||||
item = self.keyboard_layout.itemAtPosition(search_row, search_col)
|
|
||||||
if item and item.widget() and item.widget().isEnabled():
|
|
||||||
next_button = cast(QPushButton, item.widget())
|
|
||||||
next_button.setFocus()
|
|
||||||
found = True
|
|
||||||
break
|
|
||||||
search_row -= 1
|
|
||||||
|
|
||||||
def findFirstFocusableButton(self) -> QPushButton | None:
|
|
||||||
"""Находит первую фокусируемую кнопку на клавиатуре"""
|
|
||||||
for row in range(self.keyboard_layout.rowCount()):
|
|
||||||
for col in range(self.keyboard_layout.columnCount()):
|
|
||||||
item = self.keyboard_layout.itemAtPosition(row, col)
|
|
||||||
if item and item.widget() and item.widget().isEnabled():
|
|
||||||
return cast(QPushButton, item.widget())
|
|
||||||
return None
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "portprotonqt"
|
name = "portprotonqt"
|
||||||
version = "0.1.7"
|
version = "0.1.6"
|
||||||
description = "A project to rewrite PortProton (PortWINE) using PySide"
|
description = "A project to rewrite PortProton (PortWINE) using PySide"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = { text = "GPL-3.0" }
|
license = { text = "GPL-3.0" }
|
||||||
@@ -27,19 +27,19 @@ classifiers = [
|
|||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"babel>=2.17.0",
|
"babel>=2.17.0",
|
||||||
"beautifulsoup4>=4.14.2",
|
"beautifulsoup4>=4.13.5",
|
||||||
"evdev>=1.9.2",
|
"evdev>=1.9.2",
|
||||||
"icoextract>=0.2.0",
|
"icoextract>=0.2.0",
|
||||||
"numpy>=2.2.4",
|
"numpy>=2.2.4",
|
||||||
"orjson>=3.11.3",
|
"orjson>=3.11.2",
|
||||||
"pillow>=11.3.0",
|
"pillow>=11.3.0",
|
||||||
"psutil>=7.1.0",
|
"psutil>=7.0.0",
|
||||||
"pyside6==6.9.1",
|
"pyside6>=6.9.1",
|
||||||
"pyudev>=0.24.3",
|
"pyudev>=0.24.3",
|
||||||
"requests>=2.32.5",
|
"requests>=2.32.5",
|
||||||
"tqdm>=4.67.1",
|
"tqdm>=4.67.1",
|
||||||
"vdf>=3.4",
|
"vdf>=3.4",
|
||||||
"websocket-client>=1.9.0",
|
"websocket-client>=1.8.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
@@ -105,5 +105,5 @@ ignore = [
|
|||||||
dev = [
|
dev = [
|
||||||
"pre-commit>=4.3.0",
|
"pre-commit>=4.3.0",
|
||||||
"pyaspeller>=2.0.2",
|
"pyaspeller>=2.0.2",
|
||||||
"pyright>=1.1.406",
|
"pyright>=1.1.404",
|
||||||
]
|
]
|
||||||
|
@@ -1,8 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
"extends": [
|
"extends": ["config:best-practices"],
|
||||||
"config:best-practices"
|
|
||||||
],
|
|
||||||
"rebaseWhen": "never",
|
"rebaseWhen": "never",
|
||||||
"lockFileMaintenance": {
|
"lockFileMaintenance": {
|
||||||
"enabled": true
|
"enabled": true
|
||||||
@@ -11,23 +9,6 @@
|
|||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
"packageRules": [
|
"packageRules": [
|
||||||
{
|
|
||||||
"description": "Update renovate only weekly",
|
|
||||||
"matchDepNames": ["ghcr.io/renovatebot/renovate"],
|
|
||||||
"extends": ["schedule:weekly"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Automerge renovate updates",
|
|
||||||
"matchPackageNames": [
|
|
||||||
"ghcr.io/renovatebot/renovate"
|
|
||||||
],
|
|
||||||
"matchUpdateTypes": [
|
|
||||||
"minor",
|
|
||||||
"patch",
|
|
||||||
"digest"
|
|
||||||
],
|
|
||||||
"automerge": true
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"matchUpdateTypes": ["minor", "patch"],
|
"matchUpdateTypes": ["minor", "patch"],
|
||||||
"automerge": true
|
"automerge": true
|
||||||
@@ -52,7 +33,7 @@
|
|||||||
"groupName": "Python dependencies"
|
"groupName": "Python dependencies"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"matchPackageNames": ["numpy", "setuptools", "python", "pyside6"],
|
"matchPackageNames": ["numpy", "setuptools", "python"],
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"description": "Disabled due to Python 3.10 incompatibility with numpy>=2.3.2 (requires Python>=3.11)"
|
"description": "Disabled due to Python 3.10 incompatibility with numpy>=2.3.2 (requires Python>=3.11)"
|
||||||
},
|
},
|
||||||
|