Compare commits
17 Commits
Author | SHA1 | Date | |
---|---|---|---|
596aed0077
|
|||
6fc6cb1e02
|
|||
186e28a19b
|
|||
28e4d1e77c
|
|||
fff1f888c4
|
|||
fdd5a0a3d5
|
|||
792e52d981
|
|||
84d5e46a74
|
|||
4bc764d568
|
|||
9a18aa037e
|
|||
ed62d2d1c4
|
|||
accc9b18b6
|
|||
82249d7eab
|
|||
476c896940
|
|||
b1047ba18e
|
|||
987199d8e6
|
|||
|
ef1acd4581 |
@@ -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:b3809917ab5a7840d42237f5f92d92660cd036bd75ae343e7825e6a24401f166
|
image: archlinux:base-devel@sha256:06ab929f935145dd65994a89dd06651669ea28d43c812f3e24de990978511821
|
||||||
volumes:
|
volumes:
|
||||||
- /usr:/usr-host
|
- /usr:/usr-host
|
||||||
- /opt:/opt-host
|
- /opt:/opt-host
|
||||||
|
@@ -180,6 +180,8 @@ 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:b3809917ab5a7840d42237f5f92d92660cd036bd75ae343e7825e6a24401f166
|
image: archlinux:base-devel@sha256:06ab929f935145dd65994a89dd06651669ea28d43c812f3e24de990978511821
|
||||||
volumes:
|
volumes:
|
||||||
- /usr:/usr-host
|
- /usr:/usr-host
|
||||||
- /opt:/opt-host
|
- /opt:/opt-host
|
||||||
|
17
CHANGELOG.md
17
CHANGELOG.md
@@ -3,6 +3,23 @@
|
|||||||
Все заметные изменения в этом проекте фиксируются в этом файле.
|
Все заметные изменения в этом проекте фиксируются в этом файле.
|
||||||
Формат основан на [Keep a Changelog](https://keepachangelog.com/) и придерживается принципов [Semantic Versioning](https://semver.org/).
|
Формат основан на [Keep a Changelog](https://keepachangelog.com/) и придерживается принципов [Semantic Versioning](https://semver.org/).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- В настройки добавлен пункт для выбора типа геймпада для подсказок по управлению
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- При завершении автоустановки приложение больше не перезапускается
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Исправлено наложение карточек при смене фильтра игр
|
||||||
|
|
||||||
|
|
||||||
|
### Contributors
|
||||||
|
- @Vector_null
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [0.1.7] - 2025-10-12
|
## [0.1.7] - 2025-10-12
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
15
TODO.md
15
TODO.md
@@ -1,6 +1,6 @@
|
|||||||
- [X] Адаптировать структуру проекта для поддержки инструментов сборки
|
- [X] Адаптировать структуру проекта для поддержки инструментов сборки
|
||||||
- [X] Добавить возможность управления с геймпада
|
- [X] Добавить возможность управления с геймпада
|
||||||
- [ ] Добавить возможность управления с тачскрина
|
- [X] Добавить возможность управления с тачскрина (Формально и так есть)
|
||||||
- [X] Добавить возможность управления с мыши и клавиатуры
|
- [X] Добавить возможность управления с мыши и клавиатуры
|
||||||
- [X] Добавить систему тем [Документация](documentation/theme_guide)
|
- [X] Добавить систему тем [Документация](documentation/theme_guide)
|
||||||
- [X] Вынести все константы, такие как уровень закругления карточек, в темы (частично выполнено)
|
- [X] Вынести все константы, такие как уровень закругления карточек, в темы (частично выполнено)
|
||||||
@@ -11,18 +11,18 @@
|
|||||||
- [ ] Разработать адаптивный дизайн (за эталон берётся Steam Deck с разрешением 1280×800)
|
- [ ] Разработать адаптивный дизайн (за эталон берётся Steam Deck с разрешением 1280×800)
|
||||||
- [ ] Переделать скриншоты для соответствия [гайдлайнам Flathub](https://docs.flathub.org/docs/for-app-authors/metainfo-guidelines/quality-guidelines#screenshots)
|
- [ ] Переделать скриншоты для соответствия [гайдлайнам Flathub](https://docs.flathub.org/docs/for-app-authors/metainfo-guidelines/quality-guidelines#screenshots)
|
||||||
- [X] Получать описания и названия игр из базы данных Steam
|
- [X] Получать описания и названия игр из базы данных Steam
|
||||||
- [X] Получать обложки для игр из SteamGridDB или CDN Steam
|
- [X] Получать обложки для игр из CDN Steam
|
||||||
- [X] Оптимизировать работу со Steam API для ускорения времени запуска
|
- [X] Оптимизировать работу со Steam API для ускорения времени запуска
|
||||||
- [X] Улучшить функцию поиска в Steam API для исправления некорректного определения ID (например, Graven определялся как ENGRAVEN или GRAVENFALL, Spore — как SporeBound или Spore Valley)
|
- [X] Улучшить функцию поиска в Steam API для исправления некорректного определения ID (например, Graven определялся как ENGRAVEN или GRAVENFALL, Spore — как SporeBound или Spore Valley)
|
||||||
- [ ] Убрать логи Steam API в релизной версии, так как они замедляют выполнение кода
|
- [X] Убрать логи Steam API в релизной версии, так как они замедляют выполнение кода
|
||||||
- [X] Решить проблему с ограничением Steam API в 50 тысяч игр за один запрос (иногда нужные игры не попадают в выборку и остаются без обложки)
|
- [X] Решить проблему с ограничением Steam API в 50 тысяч игр за один запрос (иногда нужные игры не попадают в выборку и остаются без обложки)
|
||||||
- [X] Избавиться от вызовов yad
|
- [X] Избавиться от вызовов yad
|
||||||
- [X] Реализовать собственный системный трей вместо использования трея PortProton
|
- [X] Реализовать собственный системный трей вместо использования трея PortProton
|
||||||
- [X] Добавить экранную клавиатуру в поиск (реализация собственной клавиатуры слишком затратна, поэтому используется встроенная в DE клавиатура: Maliit в KDE, gjs-osk в GNOME, Squeekboard в Phosh, клавиатура SteamOS и т.д.)
|
- [X] Добавить экранную клавиатуру в поиск
|
||||||
- [X] Добавить сортировку карточек по различным критериям (доступны: по недавности, количеству наигранного времени, избранному или алфавиту)
|
- [X] Добавить сортировку карточек по различным критериям (доступны: по недавности, количеству наигранного времени, избранному или алфавиту)
|
||||||
- [X] Добавить индикацию запуска приложения
|
- [X] Добавить индикацию запуска приложения
|
||||||
- [X] Достигнуть паритета функциональности с Ingame
|
- [X] Достигнуть паритета функциональности с Ingame
|
||||||
- [ ] Достигнуть паритета функциональности с PortProton
|
- [ ] Достигнуть паритета функциональности с PortProton (остались настройки игр и обновление скриптов)
|
||||||
- [X] Добавить возможность изменения названия, описания и обложки через файлы `.local/share/PortProtonQT/custom_data/exe_name/{desc,name,cover}`
|
- [X] Добавить возможность изменения названия, описания и обложки через файлы `.local/share/PortProtonQT/custom_data/exe_name/{desc,name,cover}`
|
||||||
- [X] Добавить встроенное переопределение названия, описания и обложки, например, по пути `portprotonqt/custom_data` [Документация](documentation/metadata_override/)
|
- [X] Добавить встроенное переопределение названия, описания и обложки, например, по пути `portprotonqt/custom_data` [Документация](documentation/metadata_override/)
|
||||||
- [X] Добавить переводы в переопределения
|
- [X] Добавить переводы в переопределения
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
- [X] Добавить недокументированные параметры конфигурации в GUI (time_detail_level, games_sort_method, games_display_filter)
|
- [X] Добавить недокументированные параметры конфигурации в GUI (time_detail_level, games_sort_method, games_display_filter)
|
||||||
- [X] Добавить систему избранного для карточек
|
- [X] Добавить систему избранного для карточек
|
||||||
- [X] Заменить все `print` на `logging`
|
- [X] Заменить все `print` на `logging`
|
||||||
- [ ] Привести все логи к единому языку
|
- [X] Привести все логи к единому языку
|
||||||
- [X] Уменьшить количество подстановок в переводах
|
- [X] Уменьшить количество подстановок в переводах
|
||||||
- [X] Стилизовать все элементы без стилей (QMessageBox, QSlider, QDialog)
|
- [X] Стилизовать все элементы без стилей (QMessageBox, QSlider, QDialog)
|
||||||
- [X] Убрать жёсткую привязку путей к стрелочкам QComboBox в `styles.py`
|
- [X] Убрать жёсткую привязку путей к стрелочкам QComboBox в `styles.py`
|
||||||
@@ -62,7 +62,6 @@
|
|||||||
- [X] Исправить некорректную работу слайдера увеличения размера карточек([Последствия регрессии после этого коммита](https://github.com/Boria138/PortProtonQt/commit/aebdd60b5537280f06a922ff80469cd4ab27bc63)
|
- [X] Исправить некорректную работу слайдера увеличения размера карточек([Последствия регрессии после этого коммита](https://github.com/Boria138/PortProtonQt/commit/aebdd60b5537280f06a922ff80469cd4ab27bc63)
|
||||||
- [X] Исправить баг с наложением карточек друг на друга при изменении фильтра отображения ([Последствия регрессии после этого коммита](https://github.com/Boria138/PortProtonQt/commit/aebdd60b5537280f06a922ff80469cd4ab27bc63))
|
- [X] Исправить баг с наложением карточек друг на друга при изменении фильтра отображения ([Последствия регрессии после этого коммита](https://github.com/Boria138/PortProtonQt/commit/aebdd60b5537280f06a922ff80469cd4ab27bc63))
|
||||||
- [X] Скопировать логику управления с D-pad на стрелки с клавиатуры
|
- [X] Скопировать логику управления с D-pad на стрелки с клавиатуры
|
||||||
- [ ] Доделать светлую тему
|
- [X] Добавить подсказки к управлению с геймпада
|
||||||
- [ ] Добавить подсказки к управлению с геймпада
|
|
||||||
- [X] Добавить миниатюры к выбору файлов в диалоге добавления игры
|
- [X] Добавить миниатюры к выбору файлов в диалоге добавления игры
|
||||||
- [X] Добавить быстрый доступ к смонтированным дискам к выбору файлов в диалоге добавления игры
|
- [X] Добавить быстрый доступ к смонтированным дискам к выбору файлов в диалоге добавления игры
|
||||||
|
@@ -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 249 |
|
||||||
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 240 |
|
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 249 |
|
||||||
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 240 of 240 |
|
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 249 of 249 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@@ -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 из 249 |
|
||||||
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 240 |
|
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 249 |
|
||||||
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 240 из 240 |
|
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 249 из 249 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@@ -259,6 +259,25 @@ def save_rumble_config(rumble_enabled):
|
|||||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||||
cp.write(configfile)
|
cp.write(configfile)
|
||||||
|
|
||||||
|
def read_gamepad_type():
|
||||||
|
"""Reads the gamepad type from the [Gamepad] section.
|
||||||
|
Returns 'xbox' if the parameter is missing.
|
||||||
|
"""
|
||||||
|
cp = read_config_safely(CONFIG_FILE)
|
||||||
|
if cp is None or not cp.has_section("Gamepad") or not cp.has_option("Gamepad", "type"):
|
||||||
|
save_gamepad_type("xbox")
|
||||||
|
return "xbox"
|
||||||
|
return cp.get("Gamepad", "type", fallback="xbox").lower()
|
||||||
|
|
||||||
|
def save_gamepad_type(gpad_type):
|
||||||
|
"""Saves the gamepad type to the [Gamepad] section."""
|
||||||
|
cp = read_config_safely(CONFIG_FILE) or configparser.ConfigParser()
|
||||||
|
if "Gamepad" not in cp:
|
||||||
|
cp["Gamepad"] = {}
|
||||||
|
cp["Gamepad"]["type"] = gpad_type
|
||||||
|
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||||
|
cp.write(configfile)
|
||||||
|
|
||||||
def ensure_default_proxy_config():
|
def ensure_default_proxy_config():
|
||||||
"""Ensures the [Proxy] section exists in the configuration file.
|
"""Ensures the [Proxy] section exists in the configuration file.
|
||||||
Creates it with empty values if missing.
|
Creates it with empty values if missing.
|
||||||
@@ -408,3 +427,22 @@ def save_favorite_folders(folders):
|
|||||||
cp["FavoritesFolders"]["folders"] = f'"{fav_str}"'
|
cp["FavoritesFolders"]["folders"] = f'"{fav_str}"'
|
||||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||||
cp.write(configfile)
|
cp.write(configfile)
|
||||||
|
|
||||||
|
def read_minimize_to_tray():
|
||||||
|
"""Reads the minimize-to-tray setting from the [Display] section.
|
||||||
|
Returns True if the parameter is missing (default: minimize to tray).
|
||||||
|
"""
|
||||||
|
cp = read_config_safely(CONFIG_FILE)
|
||||||
|
if cp is None or not cp.has_section("Display") or not cp.has_option("Display", "minimize_to_tray"):
|
||||||
|
save_minimize_to_tray(True)
|
||||||
|
return True
|
||||||
|
return cp.getboolean("Display", "minimize_to_tray", fallback=True)
|
||||||
|
|
||||||
|
def save_minimize_to_tray(minimize_to_tray):
|
||||||
|
"""Saves the minimize-to-tray setting to the [Display] section."""
|
||||||
|
cp = read_config_safely(CONFIG_FILE) or configparser.ConfigParser()
|
||||||
|
if "Display" not in cp:
|
||||||
|
cp["Display"] = {}
|
||||||
|
cp["Display"]["minimize_to_tray"] = str(minimize_to_tray)
|
||||||
|
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||||
|
cp.write(configfile)
|
||||||
|
@@ -91,6 +91,130 @@ def generate_thumbnail(inputfile, outfile, size=128, force_resize=True):
|
|||||||
logger.error(f"Ошибка при сохранении миниатюры: {e}")
|
logger.error(f"Ошибка при сохранении миниатюры: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def create_dialog_hints_widget(theme, main_window, input_manager, context='default'):
|
||||||
|
"""
|
||||||
|
Common function to create hints widget for all dialogs.
|
||||||
|
Uses main_window for get_button_icon/get_nav_icon, input_manager for gamepad detection.
|
||||||
|
"""
|
||||||
|
theme_manager = ThemeManager()
|
||||||
|
current_theme_name = read_theme_from_config()
|
||||||
|
|
||||||
|
hintsWidget = QWidget()
|
||||||
|
hintsWidget.setStyleSheet(theme.STATUS_BAR_STYLE)
|
||||||
|
hintsLayout = QHBoxLayout(hintsWidget)
|
||||||
|
hintsLayout.setContentsMargins(10, 0, 10, 0)
|
||||||
|
hintsLayout.setSpacing(20)
|
||||||
|
|
||||||
|
dialog_actions = []
|
||||||
|
|
||||||
|
# Context-specific actions (gamepad only, no keyboard)
|
||||||
|
if context == 'file_explorer':
|
||||||
|
dialog_actions = [
|
||||||
|
("confirm", _("Open")), # A / Cross
|
||||||
|
("add_game", _("Select Dir")), # X / Triangle
|
||||||
|
("prev_dir", _("Prev Dir")), # Y / Square
|
||||||
|
("back", _("Cancel")), # B / Circle
|
||||||
|
("context_menu", _("Menu")), # Start / Options
|
||||||
|
]
|
||||||
|
elif context == 'winetricks':
|
||||||
|
dialog_actions = [
|
||||||
|
("confirm", _("Toggle")), # A / Cross
|
||||||
|
("add_game", _("Install")), # X / Triangle
|
||||||
|
("prev_dir", _("Force Install")), # Y / Square
|
||||||
|
("back", _("Cancel")), # B / Circle
|
||||||
|
("prev_tab", _("Prev Tab")), # LB / L1
|
||||||
|
("next_tab", _("Next Tab")), # RB / R1
|
||||||
|
]
|
||||||
|
|
||||||
|
hints_labels = [] # Store for updates (returned for class storage)
|
||||||
|
|
||||||
|
def make_hint(icon_name, text, action=None):
|
||||||
|
container = QWidget()
|
||||||
|
hlayout = QHBoxLayout(container)
|
||||||
|
hlayout.setContentsMargins(0, 5, 0, 0)
|
||||||
|
hlayout.setSpacing(6)
|
||||||
|
|
||||||
|
icon_label = QLabel()
|
||||||
|
icon_label.setFixedSize(26, 26)
|
||||||
|
icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
|
||||||
|
pixmap = QPixmap()
|
||||||
|
icon_path = theme_manager.get_theme_image(icon_name, current_theme_name)
|
||||||
|
if icon_path:
|
||||||
|
pixmap.load(str(icon_path))
|
||||||
|
if not pixmap.isNull():
|
||||||
|
icon_label.setPixmap(pixmap.scaled(26, 26, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation))
|
||||||
|
|
||||||
|
hlayout.addWidget(icon_label)
|
||||||
|
|
||||||
|
text_label = QLabel(text)
|
||||||
|
text_label.setStyleSheet(theme.LAST_LAUNCH_VALUE_STYLE)
|
||||||
|
text_label.setAlignment(Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignLeft)
|
||||||
|
hlayout.addWidget(text_label)
|
||||||
|
|
||||||
|
# Initially hidden; show only if gamepad connected
|
||||||
|
container.setVisible(False)
|
||||||
|
hints_labels.append((container, icon_label, action))
|
||||||
|
|
||||||
|
hintsLayout.addWidget(container)
|
||||||
|
|
||||||
|
# Add gamepad hints only
|
||||||
|
for action, text in dialog_actions:
|
||||||
|
make_hint("placeholder", text, action)
|
||||||
|
|
||||||
|
hintsLayout.addStretch()
|
||||||
|
|
||||||
|
# Return widget and labels for class storage
|
||||||
|
return hintsWidget, hints_labels
|
||||||
|
|
||||||
|
def update_dialog_hints(hints_labels, main_window, input_manager, theme_manager, current_theme_name):
|
||||||
|
"""
|
||||||
|
Common function to update hints for any dialog.
|
||||||
|
"""
|
||||||
|
if not input_manager or not main_window:
|
||||||
|
# Hide all if no input_manager or main_window
|
||||||
|
for container, _, _ in hints_labels:
|
||||||
|
container.setVisible(False)
|
||||||
|
return
|
||||||
|
|
||||||
|
is_gamepad = input_manager.gamepad is not None
|
||||||
|
if not is_gamepad:
|
||||||
|
# Hide all hints if no gamepad
|
||||||
|
for container, _, _ in hints_labels:
|
||||||
|
container.setVisible(False)
|
||||||
|
return
|
||||||
|
|
||||||
|
gtype = input_manager.gamepad_type
|
||||||
|
gamepad_actions = ['confirm', 'back', 'context_menu', 'add_game', 'prev_dir', 'prev_tab', 'next_tab']
|
||||||
|
|
||||||
|
for container, icon_label, action in hints_labels:
|
||||||
|
if action and action in gamepad_actions:
|
||||||
|
container.setVisible(True)
|
||||||
|
# Update icon using main_window methods
|
||||||
|
if action in ['confirm', 'back', 'context_menu', 'add_game', 'prev_dir']:
|
||||||
|
icon_name = main_window.get_button_icon(action, gtype)
|
||||||
|
else: # only prev_tab/next_tab (treat as nav)
|
||||||
|
direction = 'left' if action == 'prev_tab' else 'right'
|
||||||
|
icon_name = main_window.get_nav_icon(direction, gtype)
|
||||||
|
icon_path = theme_manager.get_theme_image(icon_name, current_theme_name)
|
||||||
|
pixmap = QPixmap()
|
||||||
|
if icon_path:
|
||||||
|
pixmap.load(str(icon_path))
|
||||||
|
if not pixmap.isNull():
|
||||||
|
icon_label.setPixmap(pixmap.scaled(
|
||||||
|
26, 26,
|
||||||
|
Qt.AspectRatioMode.KeepAspectRatio,
|
||||||
|
Qt.TransformationMode.SmoothTransformation
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
# Fallback to placeholder
|
||||||
|
placeholder = theme_manager.get_theme_image("placeholder", current_theme_name)
|
||||||
|
if placeholder:
|
||||||
|
pixmap.load(str(placeholder))
|
||||||
|
icon_label.setPixmap(pixmap.scaled(26, 26, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation))
|
||||||
|
else:
|
||||||
|
container.setVisible(False)
|
||||||
|
|
||||||
class FileSelectedSignal(QObject):
|
class FileSelectedSignal(QObject):
|
||||||
file_selected = Signal(str) # Сигнал с путем к выбранному файлу
|
file_selected = Signal(str) # Сигнал с путем к выбранному файлу
|
||||||
|
|
||||||
@@ -185,6 +309,7 @@ class FileExplorer(QDialog):
|
|||||||
self.initial_path = initial_path # Store initial path if provided
|
self.initial_path = initial_path # Store initial path if provided
|
||||||
self.thumbnail_cache = {} # Cache for loaded thumbnails
|
self.thumbnail_cache = {} # Cache for loaded thumbnails
|
||||||
self.pending_thumbnails = set() # Track files pending thumbnail loading
|
self.pending_thumbnails = set() # Track files pending thumbnail loading
|
||||||
|
self.main_window = None # Add reference to MainWindow
|
||||||
self.setup_ui()
|
self.setup_ui()
|
||||||
|
|
||||||
# Window settings
|
# Window settings
|
||||||
@@ -198,6 +323,7 @@ class FileExplorer(QDialog):
|
|||||||
while parent:
|
while parent:
|
||||||
if hasattr(parent, 'input_manager'):
|
if hasattr(parent, 'input_manager'):
|
||||||
self.input_manager = cast("MainWindow", parent).input_manager
|
self.input_manager = cast("MainWindow", parent).input_manager
|
||||||
|
self.main_window = parent
|
||||||
if hasattr(parent, 'context_menu_manager'):
|
if hasattr(parent, 'context_menu_manager'):
|
||||||
self.context_menu_manager = cast("MainWindow", parent).context_menu_manager
|
self.context_menu_manager = cast("MainWindow", parent).context_menu_manager
|
||||||
parent = parent.parent()
|
parent = parent.parent()
|
||||||
@@ -214,6 +340,17 @@ class FileExplorer(QDialog):
|
|||||||
self.current_path = os.path.expanduser("~") # Fallback to home if initial path is invalid
|
self.current_path = os.path.expanduser("~") # Fallback to home if initial path is invalid
|
||||||
self.update_file_list()
|
self.update_file_list()
|
||||||
|
|
||||||
|
# Create hints widget using common function
|
||||||
|
self.current_theme_name = read_theme_from_config()
|
||||||
|
self.hints_widget, self.hints_labels = create_dialog_hints_widget(self.theme, self.main_window, self.input_manager, context='file_explorer')
|
||||||
|
self.main_layout.addWidget(self.hints_widget)
|
||||||
|
|
||||||
|
# Connect signals
|
||||||
|
if self.input_manager:
|
||||||
|
self.input_manager.button_event.connect(lambda *args: update_dialog_hints(self.hints_labels, self.main_window, self.input_manager, theme_manager, self.current_theme_name))
|
||||||
|
self.input_manager.dpad_moved.connect(lambda *args: update_dialog_hints(self.hints_labels, self.main_window, self.input_manager, theme_manager, self.current_theme_name))
|
||||||
|
update_dialog_hints(self.hints_labels, self.main_window, self.input_manager, theme_manager, self.current_theme_name)
|
||||||
|
|
||||||
class ThumbnailLoader(QRunnable):
|
class ThumbnailLoader(QRunnable):
|
||||||
"""Class for asynchronous thumbnail loading in a separate thread."""
|
"""Class for asynchronous thumbnail loading in a separate thread."""
|
||||||
class Signals(QObject):
|
class Signals(QObject):
|
||||||
@@ -1037,8 +1174,6 @@ Icon={icon_path}
|
|||||||
return desktop_entry, desktop_path
|
return desktop_entry, desktop_path
|
||||||
|
|
||||||
class WinetricksDialog(QDialog):
|
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):
|
def __init__(self, parent=None, theme=None, prefix_path: str | None = None, wine_use: str | None = None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.theme = theme if theme else theme_manager.apply_theme(read_theme_from_config())
|
self.theme = theme if theme else theme_manager.apply_theme(read_theme_from_config())
|
||||||
@@ -1071,6 +1206,36 @@ class WinetricksDialog(QDialog):
|
|||||||
self.setup_ui()
|
self.setup_ui()
|
||||||
self.load_lists()
|
self.load_lists()
|
||||||
|
|
||||||
|
# Find input_manager and main_window
|
||||||
|
self.input_manager = None
|
||||||
|
self.main_window = None
|
||||||
|
parent = self.parent()
|
||||||
|
while parent:
|
||||||
|
if hasattr(parent, 'input_manager'):
|
||||||
|
self.input_manager = cast("MainWindow", parent).input_manager
|
||||||
|
self.main_window = parent
|
||||||
|
parent = parent.parent()
|
||||||
|
|
||||||
|
self.current_theme_name = read_theme_from_config()
|
||||||
|
|
||||||
|
# Enable Winetricks-specific mode
|
||||||
|
if self.input_manager:
|
||||||
|
self.input_manager.enable_winetricks_mode(self)
|
||||||
|
|
||||||
|
# Create hints widget using common function
|
||||||
|
self.hints_widget, self.hints_labels = create_dialog_hints_widget(self.theme, self.main_window, self.input_manager, context='winetricks')
|
||||||
|
self.main_layout.addWidget(self.hints_widget)
|
||||||
|
|
||||||
|
# Connect signals (use self.theme_manager)
|
||||||
|
if self.input_manager:
|
||||||
|
self.input_manager.button_event.connect(
|
||||||
|
lambda *args: update_dialog_hints(self.hints_labels, self.main_window, self.input_manager, theme_manager, self.current_theme_name)
|
||||||
|
)
|
||||||
|
self.input_manager.dpad_moved.connect(
|
||||||
|
lambda *args: update_dialog_hints(self.hints_labels, self.main_window, self.input_manager, theme_manager, self.current_theme_name)
|
||||||
|
)
|
||||||
|
update_dialog_hints(self.hints_labels, self.main_window, self.input_manager, theme_manager, self.current_theme_name)
|
||||||
|
|
||||||
def update_winetricks(self):
|
def update_winetricks(self):
|
||||||
"""Update the winetricks script."""
|
"""Update the winetricks script."""
|
||||||
if not self.downloader.has_internet():
|
if not self.downloader.has_internet():
|
||||||
@@ -1143,15 +1308,15 @@ class WinetricksDialog(QDialog):
|
|||||||
|
|
||||||
def setup_ui(self):
|
def setup_ui(self):
|
||||||
"""Set up the user interface with tabs and tables."""
|
"""Set up the user interface with tabs and tables."""
|
||||||
main_layout = QVBoxLayout(self)
|
self.main_layout = QVBoxLayout(self)
|
||||||
main_layout.setContentsMargins(10, 10, 10, 10)
|
self.main_layout.setContentsMargins(10, 10, 10, 10)
|
||||||
main_layout.setSpacing(10)
|
self.main_layout.setSpacing(10)
|
||||||
|
|
||||||
# Log output
|
# Log output
|
||||||
self.log_output = QTextEdit()
|
self.log_output = QTextEdit()
|
||||||
self.log_output.setReadOnly(True)
|
self.log_output.setReadOnly(True)
|
||||||
self.log_output.setStyleSheet(self.theme.WINETRICKS_LOG_STYLE)
|
self.log_output.setStyleSheet(self.theme.WINETRICKS_LOG_STYLE)
|
||||||
main_layout.addWidget(self.log_output)
|
self.main_layout.addWidget(self.log_output)
|
||||||
|
|
||||||
# Tab widget
|
# Tab widget
|
||||||
self.tab_widget = QTabWidget()
|
self.tab_widget = QTabWidget()
|
||||||
@@ -1258,7 +1423,7 @@ class WinetricksDialog(QDialog):
|
|||||||
"settings": self.settings_container
|
"settings": self.settings_container
|
||||||
}
|
}
|
||||||
|
|
||||||
main_layout.addWidget(self.tab_widget)
|
self.main_layout.addWidget(self.tab_widget)
|
||||||
|
|
||||||
# Buttons
|
# Buttons
|
||||||
button_layout = QHBoxLayout()
|
button_layout = QHBoxLayout()
|
||||||
@@ -1272,7 +1437,7 @@ class WinetricksDialog(QDialog):
|
|||||||
button_layout.addWidget(self.cancel_button)
|
button_layout.addWidget(self.cancel_button)
|
||||||
button_layout.addWidget(self.force_button)
|
button_layout.addWidget(self.force_button)
|
||||||
button_layout.addWidget(self.install_button)
|
button_layout.addWidget(self.install_button)
|
||||||
main_layout.addLayout(button_layout)
|
self.main_layout.addLayout(button_layout)
|
||||||
|
|
||||||
self.cancel_button.clicked.connect(self.reject)
|
self.cancel_button.clicked.connect(self.reject)
|
||||||
self.force_button.clicked.connect(lambda: self.install_selected(force=True))
|
self.force_button.clicked.connect(lambda: self.install_selected(force=True))
|
||||||
@@ -1497,3 +1662,15 @@ class WinetricksDialog(QDialog):
|
|||||||
"""Добавляет в лог."""
|
"""Добавляет в лог."""
|
||||||
self.log_output.append(message)
|
self.log_output.append(message)
|
||||||
self.log_output.moveCursor(QTextCursor.MoveOperation.End)
|
self.log_output.moveCursor(QTextCursor.MoveOperation.End)
|
||||||
|
|
||||||
|
def closeEvent(self, event):
|
||||||
|
"""Disable mode on close."""
|
||||||
|
if self.input_manager:
|
||||||
|
self.input_manager.disable_winetricks_mode()
|
||||||
|
super().closeEvent(event)
|
||||||
|
|
||||||
|
def reject(self):
|
||||||
|
"""Disable mode on reject."""
|
||||||
|
if self.input_manager:
|
||||||
|
self.input_manager.disable_winetricks_mode()
|
||||||
|
super().reject()
|
||||||
|
@@ -217,6 +217,16 @@ class GameLibraryManager:
|
|||||||
else:
|
else:
|
||||||
self._update_game_grid_immediate()
|
self._update_game_grid_immediate()
|
||||||
|
|
||||||
|
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 _update_game_grid_immediate(self):
|
def _update_game_grid_immediate(self):
|
||||||
"""Updates the game grid with the provided or current game list."""
|
"""Updates the game grid with the provided or current game list."""
|
||||||
if self.gamesListLayout is None or self.gamesListWidget is None:
|
if self.gamesListLayout is None or self.gamesListWidget is None:
|
||||||
@@ -346,6 +356,8 @@ 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):
|
||||||
@@ -453,11 +465,3 @@ class GameLibraryManager:
|
|||||||
def filter_games_delayed(self):
|
def filter_games_delayed(self):
|
||||||
"""Filters games based on search text and updates the grid."""
|
"""Filters games based on search text and updates the grid."""
|
||||||
self.update_game_grid(is_filter=True)
|
self.update_game_grid(is_filter=True)
|
||||||
|
|
||||||
def calculate_columns(self, card_width: int) -> int:
|
|
||||||
"""Calculate the number of columns based on card width and assumed container width."""
|
|
||||||
# Assuming a typical container width; adjust as needed
|
|
||||||
available_width = 1200 # Example width, can be dynamic if widget access is added
|
|
||||||
spacing = 15 # Assumed spacing between cards
|
|
||||||
columns = max(1, (available_width - spacing) // (card_width + spacing))
|
|
||||||
return min(columns, 8) # Cap at reasonable max
|
|
||||||
|
@@ -5,15 +5,15 @@ 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, QTableWidget, QAbstractItemView, QTableWidgetItem
|
||||||
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
|
||||||
from portprotonqt.image_utils import FullscreenDialog
|
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, read_gamepad_type
|
||||||
from portprotonqt.dialogs import AddGameDialog, WinetricksDialog
|
from portprotonqt.dialogs import AddGameDialog
|
||||||
from portprotonqt.virtual_keyboard import VirtualKeyboard
|
from portprotonqt.virtual_keyboard import VirtualKeyboard
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
@@ -87,8 +87,13 @@ class InputManager(QObject):
|
|||||||
super().__init__(cast(QObject, main_window))
|
super().__init__(cast(QObject, main_window))
|
||||||
self._parent = main_window
|
self._parent = main_window
|
||||||
self._gamepad_handling_enabled = True
|
self._gamepad_handling_enabled = True
|
||||||
|
type_str = read_gamepad_type()
|
||||||
|
if type_str == "playstation":
|
||||||
|
self.gamepad_type = GamepadType.PLAYSTATION
|
||||||
|
elif type_str == "xbox":
|
||||||
|
self.gamepad_type = GamepadType.XBOX
|
||||||
|
else:
|
||||||
self.gamepad_type = GamepadType.UNKNOWN
|
self.gamepad_type = GamepadType.UNKNOWN
|
||||||
# Ensure attributes exist on main_window
|
|
||||||
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)
|
||||||
@@ -271,38 +276,6 @@ class InputManager(QObject):
|
|||||||
elif current_row_idx == 0:
|
elif current_row_idx == 0:
|
||||||
self._parent.tabButtons[tab_index].setFocus(Qt.FocusReason.OtherFocusReason)
|
self._parent.tabButtons[tab_index].setFocus(Qt.FocusReason.OtherFocusReason)
|
||||||
|
|
||||||
def detect_gamepad_type(self, device: InputDevice) -> GamepadType:
|
|
||||||
"""
|
|
||||||
Определяет тип геймпада по capabilities
|
|
||||||
"""
|
|
||||||
caps = device.capabilities()
|
|
||||||
keys = set(caps.get(ecodes.EV_KEY, []))
|
|
||||||
|
|
||||||
# Для EV_ABS вытаскиваем только коды (первый элемент кортежа)
|
|
||||||
abs_axes = {a if isinstance(a, int) else a[0] for a in caps.get(ecodes.EV_ABS, [])}
|
|
||||||
|
|
||||||
# Xbox layout
|
|
||||||
if {ecodes.BTN_SOUTH, ecodes.BTN_EAST, ecodes.BTN_NORTH, ecodes.BTN_WEST}.issubset(keys):
|
|
||||||
if {ecodes.ABS_X, ecodes.ABS_Y, ecodes.ABS_RX, ecodes.ABS_RY}.issubset(abs_axes):
|
|
||||||
self.gamepad_type = GamepadType.XBOX
|
|
||||||
return GamepadType.XBOX
|
|
||||||
|
|
||||||
# PlayStation layout
|
|
||||||
if ecodes.BTN_TOUCH in keys or (ecodes.BTN_DPAD_UP in keys and ecodes.BTN_EAST in keys):
|
|
||||||
self.gamepad_type = GamepadType.PLAYSTATION
|
|
||||||
logger.info(f"Detected {self.gamepad_type.value} controller: {device.name}")
|
|
||||||
return GamepadType.PLAYSTATION
|
|
||||||
|
|
||||||
# Steam Controller / Deck (трекпады)
|
|
||||||
if any(a for a in abs_axes if a >= ecodes.ABS_MT_SLOT):
|
|
||||||
self.gamepad_type = GamepadType.XBOX
|
|
||||||
logger.info(f"Detected {self.gamepad_type.value} controller: {device.name}")
|
|
||||||
return GamepadType.XBOX
|
|
||||||
|
|
||||||
# Fallback
|
|
||||||
self.gamepad_type = GamepadType.XBOX
|
|
||||||
return GamepadType.XBOX
|
|
||||||
|
|
||||||
def enable_file_explorer_mode(self, file_explorer):
|
def enable_file_explorer_mode(self, file_explorer):
|
||||||
"""Настройка обработки геймпада для FileExplorer"""
|
"""Настройка обработки геймпада для FileExplorer"""
|
||||||
try:
|
try:
|
||||||
@@ -482,6 +455,171 @@ class InputManager(QObject):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Error in FileExplorer dpad handler: %s", e)
|
logger.error("Error in FileExplorer dpad handler: %s", e)
|
||||||
|
|
||||||
|
def enable_winetricks_mode(self, winetricks_dialog):
|
||||||
|
"""Setup gamepad handling for WinetricksDialog"""
|
||||||
|
try:
|
||||||
|
self.winetricks_dialog = winetricks_dialog
|
||||||
|
self.original_button_handler = self.handle_button_slot
|
||||||
|
self.original_dpad_handler = self.handle_dpad_slot
|
||||||
|
self.original_gamepad_state = self._gamepad_handling_enabled
|
||||||
|
self.handle_button_slot = self.handle_winetricks_button
|
||||||
|
self.handle_dpad_slot = self.handle_winetricks_dpad
|
||||||
|
self._gamepad_handling_enabled = True
|
||||||
|
# Reset dpad timer for table nav
|
||||||
|
self.dpad_timer.stop()
|
||||||
|
self.current_dpad_code = None
|
||||||
|
self.current_dpad_value = 0
|
||||||
|
logger.debug("Gamepad handling successfully connected for WinetricksDialog")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error connecting gamepad handlers for Winetricks: {e}")
|
||||||
|
|
||||||
|
def disable_winetricks_mode(self):
|
||||||
|
"""Restore original main window handlers"""
|
||||||
|
try:
|
||||||
|
if self.winetricks_dialog:
|
||||||
|
self.handle_button_slot = self.original_button_handler
|
||||||
|
self.handle_dpad_slot = self.original_dpad_handler
|
||||||
|
self._gamepad_handling_enabled = self.original_gamepad_state
|
||||||
|
self.winetricks_dialog = None
|
||||||
|
self.dpad_timer.stop()
|
||||||
|
self.current_dpad_code = None
|
||||||
|
self.current_dpad_value = 0
|
||||||
|
logger.debug("Gamepad handling successfully restored from Winetricks")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error restoring gamepad handlers from Winetricks: {e}")
|
||||||
|
|
||||||
|
def handle_winetricks_button(self, button_code, value):
|
||||||
|
if self.winetricks_dialog is None:
|
||||||
|
return
|
||||||
|
if value == 0: # Ignore releases
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
# Always check for popups first, including QMessageBox
|
||||||
|
popup = QApplication.activePopupWidget()
|
||||||
|
if popup:
|
||||||
|
if isinstance(popup, QMessageBox):
|
||||||
|
if button_code in BUTTONS['confirm'] or button_code in BUTTONS['back']:
|
||||||
|
popup.accept() # Close QMessageBox with A or B
|
||||||
|
return
|
||||||
|
elif isinstance(popup, QMenu):
|
||||||
|
if button_code in BUTTONS['confirm']: # A: Select menu item
|
||||||
|
focused = popup.activeAction()
|
||||||
|
if focused:
|
||||||
|
focused.trigger()
|
||||||
|
return
|
||||||
|
elif button_code in BUTTONS['back']: # B: Close menu
|
||||||
|
popup.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Additional check for top-level QMessageBox (in case not active popup yet)
|
||||||
|
for widget in QApplication.topLevelWidgets():
|
||||||
|
if isinstance(widget, QMessageBox) and widget.isVisible():
|
||||||
|
if button_code in BUTTONS['confirm'] or button_code in BUTTONS['back']:
|
||||||
|
widget.accept()
|
||||||
|
return
|
||||||
|
|
||||||
|
focused = QApplication.focusWidget()
|
||||||
|
if button_code in BUTTONS['confirm']: # A: Toggle checkbox
|
||||||
|
if isinstance(focused, QTableWidget):
|
||||||
|
current_row = focused.currentRow()
|
||||||
|
if current_row >= 0:
|
||||||
|
checkbox_item = focused.item(current_row, 0)
|
||||||
|
if checkbox_item and isinstance(checkbox_item, QTableWidgetItem):
|
||||||
|
new_state = Qt.CheckState.Checked if checkbox_item.checkState() == Qt.CheckState.Unchecked else Qt.CheckState.Unchecked
|
||||||
|
checkbox_item.setCheckState(new_state)
|
||||||
|
return
|
||||||
|
elif button_code in BUTTONS['add_game']: # X: Install (no force)
|
||||||
|
self.winetricks_dialog.install_selected(force=False)
|
||||||
|
return
|
||||||
|
elif button_code in BUTTONS['prev_dir']: # Y: Force Install
|
||||||
|
self.winetricks_dialog.install_selected(force=True)
|
||||||
|
return
|
||||||
|
elif button_code in BUTTONS['back']: # B: Cancel
|
||||||
|
self.winetricks_dialog.reject()
|
||||||
|
return
|
||||||
|
elif button_code in BUTTONS['prev_tab']: # LB: Prev Tab
|
||||||
|
current_index = self.winetricks_dialog.tab_widget.currentIndex()
|
||||||
|
new_index = max(0, current_index - 1)
|
||||||
|
self.winetricks_dialog.tab_widget.setCurrentIndex(new_index)
|
||||||
|
self._focus_first_row_in_current_table()
|
||||||
|
return
|
||||||
|
elif button_code in BUTTONS['next_tab']: # RB: Next Tab
|
||||||
|
current_index = self.winetricks_dialog.tab_widget.currentIndex()
|
||||||
|
new_index = min(self.winetricks_dialog.tab_widget.count() - 1, current_index + 1)
|
||||||
|
self.winetricks_dialog.tab_widget.setCurrentIndex(new_index)
|
||||||
|
self._focus_first_row_in_current_table()
|
||||||
|
return
|
||||||
|
# Fallback: Activate focused widget (e.g., buttons)
|
||||||
|
self._parent.activateFocusedWidget()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in handle_winetricks_button: {e}")
|
||||||
|
|
||||||
|
def handle_winetricks_dpad(self, code, value, now):
|
||||||
|
if self.winetricks_dialog is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
if value == 0: # Release: Stop repeat
|
||||||
|
self.dpad_timer.stop()
|
||||||
|
self.current_dpad_code = None
|
||||||
|
self.current_dpad_value = 0
|
||||||
|
return
|
||||||
|
|
||||||
|
# Start/update repeat timer for hold navigation
|
||||||
|
if self.current_dpad_code != code or self.current_dpad_value != value:
|
||||||
|
self.dpad_timer.stop()
|
||||||
|
self.dpad_timer.setInterval(150 if self.dpad_timer.isActive() else 300) # Initial slower, then faster repeat
|
||||||
|
self.dpad_timer.start()
|
||||||
|
self.current_dpad_code = code
|
||||||
|
self.current_dpad_value = value
|
||||||
|
|
||||||
|
table = self._get_current_table()
|
||||||
|
if not table or table.rowCount() == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
current_row = table.currentRow()
|
||||||
|
if code == ecodes.ABS_HAT0Y: # Up/Down: Navigate rows
|
||||||
|
if value < 0: # Up
|
||||||
|
new_row = max(0, current_row - 1)
|
||||||
|
elif value > 0: # Down
|
||||||
|
new_row = min(table.rowCount() - 1, current_row + 1)
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
if new_row != current_row:
|
||||||
|
table.setCurrentCell(new_row, 0) # Focus checkbox column
|
||||||
|
table.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||||
|
elif code == ecodes.ABS_HAT0X: # Left/Right: Switch tabs
|
||||||
|
if value < 0: # Left: Prev tab
|
||||||
|
current_index = self.winetricks_dialog.tab_widget.currentIndex()
|
||||||
|
new_index = max(0, current_index - 1)
|
||||||
|
self.winetricks_dialog.tab_widget.setCurrentIndex(new_index)
|
||||||
|
elif value > 0: # Right: Next tab
|
||||||
|
current_index = self.winetricks_dialog.tab_widget.currentIndex()
|
||||||
|
new_index = min(self.winetricks_dialog.tab_widget.count() - 1, current_index + 1)
|
||||||
|
self.winetricks_dialog.tab_widget.setCurrentIndex(new_index)
|
||||||
|
self._focus_first_row_in_current_table()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in handle_winetricks_dpad: {e}")
|
||||||
|
|
||||||
|
def _get_current_table(self):
|
||||||
|
"""Get the current visible table from the tab widget's stacked container."""
|
||||||
|
if self.winetricks_dialog is None:
|
||||||
|
return None
|
||||||
|
current_container = self.winetricks_dialog.tab_widget.currentWidget()
|
||||||
|
if current_container and isinstance(current_container, QStackedWidget):
|
||||||
|
current_table = current_container.widget(1) # Table is at index 1 (after preloader)
|
||||||
|
if isinstance(current_table, QTableWidget):
|
||||||
|
return current_table
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _focus_first_row_in_current_table(self):
|
||||||
|
"""Focus the first row in the current table after tab switch."""
|
||||||
|
if self.winetricks_dialog is None:
|
||||||
|
return
|
||||||
|
table = self._get_current_table()
|
||||||
|
if table and table.rowCount() > 0:
|
||||||
|
table.setCurrentCell(0, 0)
|
||||||
|
table.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||||
|
|
||||||
def handle_navigation_repeat(self):
|
def handle_navigation_repeat(self):
|
||||||
"""Плавное повторение движения с переменной скоростью для FileExplorer"""
|
"""Плавное повторение движения с переменной скоростью для FileExplorer"""
|
||||||
try:
|
try:
|
||||||
@@ -732,39 +870,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()
|
||||||
@@ -1331,6 +1436,7 @@ class InputManager(QObject):
|
|||||||
return super().eventFilter(obj, event)
|
return super().eventFilter(obj, event)
|
||||||
|
|
||||||
def init_gamepad(self) -> None:
|
def init_gamepad(self) -> None:
|
||||||
|
self.monitor_observer = None # Добавляем атрибут для хранения observer
|
||||||
self.check_gamepad()
|
self.check_gamepad()
|
||||||
threading.Thread(target=self.run_udev_monitor, daemon=True).start()
|
threading.Thread(target=self.run_udev_monitor, daemon=True).start()
|
||||||
logger.info("Gamepad support initialized with hotplug (evdev + pyudev)")
|
logger.info("Gamepad support initialized with hotplug (evdev + pyudev)")
|
||||||
@@ -1341,9 +1447,9 @@ class InputManager(QObject):
|
|||||||
monitor = Monitor.from_netlink(context)
|
monitor = Monitor.from_netlink(context)
|
||||||
monitor.filter_by(subsystem='input')
|
monitor.filter_by(subsystem='input')
|
||||||
observer = MonitorObserver(monitor, self.handle_udev_event)
|
observer = MonitorObserver(monitor, self.handle_udev_event)
|
||||||
observer.start()
|
self.monitor_observer = observer # Сохраняем ссылку для остановки
|
||||||
while self.running:
|
observer.start() # Это блокирует поток до вызова send_stop()
|
||||||
time.sleep(1)
|
logger.info("MonitorObserver stopped gracefully")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in udev monitor: {e}", exc_info=True)
|
logger.error(f"Error in udev monitor: {e}", exc_info=True)
|
||||||
|
|
||||||
@@ -1369,8 +1475,6 @@ class InputManager(QObject):
|
|||||||
new_gamepad = self.find_gamepad()
|
new_gamepad = self.find_gamepad()
|
||||||
if new_gamepad and new_gamepad != self.gamepad:
|
if new_gamepad and new_gamepad != self.gamepad:
|
||||||
logger.info(f"Gamepad connected: {new_gamepad.name}")
|
logger.info(f"Gamepad connected: {new_gamepad.name}")
|
||||||
self.detect_gamepad_type(new_gamepad)
|
|
||||||
logger.info(f"Detected gamepad type: {self.gamepad_type.value}")
|
|
||||||
self.stop_rumble()
|
self.stop_rumble()
|
||||||
self.gamepad = new_gamepad
|
self.gamepad = new_gamepad
|
||||||
if self.gamepad_thread:
|
if self.gamepad_thread:
|
||||||
@@ -1466,11 +1570,21 @@ class InputManager(QObject):
|
|||||||
def cleanup(self) -> None:
|
def cleanup(self) -> None:
|
||||||
try:
|
try:
|
||||||
self.running = False
|
self.running = False
|
||||||
|
|
||||||
|
# Останавливаем udev monitor
|
||||||
|
if self.monitor_observer:
|
||||||
|
try:
|
||||||
|
logger.info("Stopping udev monitor...")
|
||||||
|
self.monitor_observer.send_stop()
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Error stopping monitor observer: {e}")
|
||||||
|
self.monitor_observer = None
|
||||||
|
|
||||||
self.dpad_timer.stop()
|
self.dpad_timer.stop()
|
||||||
self.nav_timer.stop()
|
self.nav_timer.stop()
|
||||||
self.stop_rumble()
|
self.stop_rumble()
|
||||||
if self.gamepad_thread:
|
if self.gamepad_thread:
|
||||||
self.gamepad_thread.join()
|
self.gamepad_thread.join(timeout=2.0) # Добавлен таймаут
|
||||||
if self.gamepad:
|
if self.gamepad:
|
||||||
self.gamepad.close()
|
self.gamepad.close()
|
||||||
self.gamepad = None
|
self.gamepad = None
|
||||||
|
Binary file not shown.
@@ -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-10-16 14:54+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"
|
||||||
@@ -252,13 +252,37 @@ msgstr ""
|
|||||||
msgid "Select All"
|
msgid "Select All"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#, python-brace-format
|
msgid "Open"
|
||||||
msgid "Launching {0}"
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Select Dir"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Prev Dir"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Toggle"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Install"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Force Install"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Prev Tab"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Next Tab"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Launching {0}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "File Explorer"
|
msgid "File Explorer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -326,12 +350,6 @@ msgstr ""
|
|||||||
msgid "Settings"
|
msgid "Settings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Force Install"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Install"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Winetricks not found. Please try again."
|
msgid "Winetricks not found. Please try again."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -579,6 +597,9 @@ msgstr ""
|
|||||||
msgid "Games Display Filter:"
|
msgid "Games Display Filter:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Gamepad Type:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Proxy URL"
|
msgid "Proxy URL"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -603,6 +624,12 @@ msgstr ""
|
|||||||
msgid "Application Fullscreen Mode:"
|
msgid "Application Fullscreen Mode:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Minimize to tray on close"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Application Close Mode:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Auto Fullscreen on Gamepad connected"
|
msgid "Auto Fullscreen on Gamepad connected"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
Binary file not shown.
@@ -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-10-16 14:54+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"
|
||||||
@@ -252,13 +252,37 @@ msgstr ""
|
|||||||
msgid "Select All"
|
msgid "Select All"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#, python-brace-format
|
msgid "Open"
|
||||||
msgid "Launching {0}"
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Select Dir"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Prev Dir"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Toggle"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Install"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Force Install"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Prev Tab"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Next Tab"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Launching {0}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "File Explorer"
|
msgid "File Explorer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -326,12 +350,6 @@ msgstr ""
|
|||||||
msgid "Settings"
|
msgid "Settings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Force Install"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Install"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Winetricks not found. Please try again."
|
msgid "Winetricks not found. Please try again."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -579,6 +597,9 @@ msgstr ""
|
|||||||
msgid "Games Display Filter:"
|
msgid "Games Display Filter:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Gamepad Type:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Proxy URL"
|
msgid "Proxy URL"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -603,6 +624,12 @@ msgstr ""
|
|||||||
msgid "Application Fullscreen Mode:"
|
msgid "Application Fullscreen Mode:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Minimize to tray on close"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Application Close Mode:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Auto Fullscreen on Gamepad connected"
|
msgid "Auto Fullscreen on Gamepad connected"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@@ -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-10-16 14:54+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"
|
||||||
@@ -250,13 +250,37 @@ msgstr ""
|
|||||||
msgid "Select All"
|
msgid "Select All"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#, python-brace-format
|
msgid "Open"
|
||||||
msgid "Launching {0}"
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Select Dir"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Prev Dir"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Toggle"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Install"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Force Install"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Prev Tab"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Next Tab"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Launching {0}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "File Explorer"
|
msgid "File Explorer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -324,12 +348,6 @@ msgstr ""
|
|||||||
msgid "Settings"
|
msgid "Settings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Force Install"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Install"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Winetricks not found. Please try again."
|
msgid "Winetricks not found. Please try again."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -577,6 +595,9 @@ msgstr ""
|
|||||||
msgid "Games Display Filter:"
|
msgid "Games Display Filter:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Gamepad Type:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Proxy URL"
|
msgid "Proxy URL"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -601,6 +622,12 @@ msgstr ""
|
|||||||
msgid "Application Fullscreen Mode:"
|
msgid "Application Fullscreen Mode:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Minimize to tray on close"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Application Close Mode:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Auto Fullscreen on Gamepad connected"
|
msgid "Auto Fullscreen on Gamepad connected"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
Binary file not shown.
@@ -9,8 +9,8 @@ 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-10-16 14:54+0500\n"
|
||||||
"PO-Revision-Date: 2025-10-12 17:13+0500\n"
|
"PO-Revision-Date: 2025-10-16 14:54+0500\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language: ru_RU\n"
|
"Language: ru_RU\n"
|
||||||
"Language-Team: ru_RU <LL@li.org>\n"
|
"Language-Team: ru_RU <LL@li.org>\n"
|
||||||
@@ -259,13 +259,37 @@ msgstr "Удалить"
|
|||||||
msgid "Select All"
|
msgid "Select All"
|
||||||
msgstr "Выбрать всё"
|
msgstr "Выбрать всё"
|
||||||
|
|
||||||
#, python-brace-format
|
msgid "Open"
|
||||||
msgid "Launching {0}"
|
msgstr "Открыть"
|
||||||
msgstr "Идёт запуск {0}"
|
|
||||||
|
msgid "Select Dir"
|
||||||
|
msgstr "Выбрать папку"
|
||||||
|
|
||||||
|
msgid "Prev Dir"
|
||||||
|
msgstr "Предыдущий каталог"
|
||||||
|
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr "Отмена"
|
msgstr "Отмена"
|
||||||
|
|
||||||
|
msgid "Toggle"
|
||||||
|
msgstr "Переключить"
|
||||||
|
|
||||||
|
msgid "Install"
|
||||||
|
msgstr "Установить"
|
||||||
|
|
||||||
|
msgid "Force Install"
|
||||||
|
msgstr "Принудительно установить"
|
||||||
|
|
||||||
|
msgid "Prev Tab"
|
||||||
|
msgstr "Предыдущая вкладка"
|
||||||
|
|
||||||
|
msgid "Next Tab"
|
||||||
|
msgstr "Следующая вкладка"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Launching {0}"
|
||||||
|
msgstr "Идёт запуск {0}"
|
||||||
|
|
||||||
msgid "File Explorer"
|
msgid "File Explorer"
|
||||||
msgstr "Проводник"
|
msgstr "Проводник"
|
||||||
|
|
||||||
@@ -333,12 +357,6 @@ msgstr "Шрифты"
|
|||||||
msgid "Settings"
|
msgid "Settings"
|
||||||
msgstr "Настройки"
|
msgstr "Настройки"
|
||||||
|
|
||||||
msgid "Force Install"
|
|
||||||
msgstr "Принудительно установить"
|
|
||||||
|
|
||||||
msgid "Install"
|
|
||||||
msgstr "Установить"
|
|
||||||
|
|
||||||
msgid "Winetricks not found. Please try again."
|
msgid "Winetricks not found. Please try again."
|
||||||
msgstr "Winetricks не найден. Повторите попытку."
|
msgstr "Winetricks не найден. Повторите попытку."
|
||||||
|
|
||||||
@@ -588,6 +606,9 @@ msgstr "все"
|
|||||||
msgid "Games Display Filter:"
|
msgid "Games Display Filter:"
|
||||||
msgstr "Фильтр игр:"
|
msgstr "Фильтр игр:"
|
||||||
|
|
||||||
|
msgid "Gamepad Type:"
|
||||||
|
msgstr "Тип геймпада:"
|
||||||
|
|
||||||
msgid "Proxy URL"
|
msgid "Proxy URL"
|
||||||
msgstr "Адрес прокси"
|
msgstr "Адрес прокси"
|
||||||
|
|
||||||
@@ -612,6 +633,12 @@ msgstr "Запуск приложения в полноэкранном режи
|
|||||||
msgid "Application Fullscreen Mode:"
|
msgid "Application Fullscreen Mode:"
|
||||||
msgstr "Режим полноэкранного отображения приложения:"
|
msgstr "Режим полноэкранного отображения приложения:"
|
||||||
|
|
||||||
|
msgid "Minimize to tray on close"
|
||||||
|
msgstr "Сворачивать в трей при закрытии"
|
||||||
|
|
||||||
|
msgid "Application Close Mode:"
|
||||||
|
msgstr "Режим закрытия приложения:"
|
||||||
|
|
||||||
msgid "Auto Fullscreen on Gamepad connected"
|
msgid "Auto Fullscreen on Gamepad connected"
|
||||||
msgstr "Режим полноэкранного отображения приложения при подключении геймпада"
|
msgstr "Режим полноэкранного отображения приложения при подключении геймпада"
|
||||||
|
|
||||||
|
@@ -29,7 +29,7 @@ from portprotonqt.config_utils import (
|
|||||||
read_display_filter, read_favorites, save_favorites, save_time_config, save_sort_method,
|
read_display_filter, read_favorites, save_favorites, save_time_config, save_sort_method,
|
||||||
save_display_filter, save_proxy_config, read_proxy_config, read_fullscreen_config,
|
save_display_filter, save_proxy_config, read_proxy_config, read_fullscreen_config,
|
||||||
save_fullscreen_config, read_window_geometry, save_window_geometry, reset_config,
|
save_fullscreen_config, read_window_geometry, save_window_geometry, reset_config,
|
||||||
clear_cache, read_auto_fullscreen_gamepad, save_auto_fullscreen_gamepad, read_rumble_config, save_rumble_config
|
clear_cache, read_auto_fullscreen_gamepad, save_auto_fullscreen_gamepad, read_rumble_config, save_rumble_config, read_gamepad_type, save_gamepad_type, read_minimize_to_tray, save_minimize_to_tray
|
||||||
)
|
)
|
||||||
from portprotonqt.localization import _, get_egs_language, read_metadata_translations
|
from portprotonqt.localization import _, get_egs_language, read_metadata_translations
|
||||||
from portprotonqt.howlongtobeat_api import HowLongToBeat
|
from portprotonqt.howlongtobeat_api import HowLongToBeat
|
||||||
@@ -260,6 +260,10 @@ class MainWindow(QMainWindow):
|
|||||||
GamepadType.XBOX: "xbox_y",
|
GamepadType.XBOX: "xbox_y",
|
||||||
GamepadType.PLAYSTATION: "ps_square",
|
GamepadType.PLAYSTATION: "ps_square",
|
||||||
},
|
},
|
||||||
|
'prev_dir': {
|
||||||
|
GamepadType.XBOX: "xbox_y",
|
||||||
|
GamepadType.PLAYSTATION: "ps_square",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
return mappings.get(action, {}).get(gtype, "placeholder")
|
return mappings.get(action, {}).get(gtype, "placeholder")
|
||||||
|
|
||||||
@@ -516,12 +520,26 @@ class MainWindow(QMainWindow):
|
|||||||
self.install_monitor_timer = None
|
self.install_monitor_timer = None
|
||||||
self.progress_bar.setRange(0, 100)
|
self.progress_bar.setRange(0, 100)
|
||||||
self.progress_bar.setValue(100)
|
self.progress_bar.setValue(100)
|
||||||
|
|
||||||
if exit_code == 0:
|
if exit_code == 0:
|
||||||
self.update_status_message.emit(_("Installation completed successfully."), 5000)
|
self.update_status_message.emit(_("Installation completed successfully."), 5000)
|
||||||
QTimer.singleShot(500, lambda: self.restart_application())
|
|
||||||
|
desktop_dir = self.portproton_location or ""
|
||||||
|
new_desktops = [e.path for e in os.scandir(desktop_dir) if e.name.endswith(".desktop")]
|
||||||
|
if new_desktops:
|
||||||
|
latest = max(new_desktops, key=os.path.getmtime)
|
||||||
|
self._process_desktop_file_async(
|
||||||
|
latest,
|
||||||
|
lambda result: (
|
||||||
|
self.game_library_manager.add_game_incremental(result)
|
||||||
|
if result else None
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.update_status_message.emit(_("Installation failed."), 5000)
|
self.update_status_message.emit(_("Installation failed."), 5000)
|
||||||
QMessageBox.warning(self, _("Error"), f"Installation failed (code: {exit_code}).")
|
QMessageBox.warning(self, _("Error"), f"Installation failed (code: {exit_code}).")
|
||||||
|
|
||||||
self.progress_bar.setVisible(False)
|
self.progress_bar.setVisible(False)
|
||||||
self.current_install_script = None
|
self.current_install_script = None
|
||||||
if self.install_process:
|
if self.install_process:
|
||||||
@@ -820,6 +838,25 @@ class MainWindow(QMainWindow):
|
|||||||
for i, btn in self.tabButtons.items():
|
for i, btn in self.tabButtons.items():
|
||||||
btn.setChecked(i == index)
|
btn.setChecked(i == index)
|
||||||
self.stackedWidget.setCurrentIndex(index)
|
self.stackedWidget.setCurrentIndex(index)
|
||||||
|
if hasattr(self, "game_library_manager"):
|
||||||
|
mgr = self.game_library_manager
|
||||||
|
if mgr.gamesListWidget and mgr.gamesListLayout:
|
||||||
|
games_layout = mgr.gamesListLayout
|
||||||
|
games_widget = mgr.gamesListWidget
|
||||||
|
QTimer.singleShot(0, lambda: (
|
||||||
|
games_layout.invalidate(),
|
||||||
|
games_widget.adjustSize(),
|
||||||
|
games_widget.updateGeometry()
|
||||||
|
))
|
||||||
|
if hasattr(self, "autoInstallContainer") and hasattr(self, "autoInstallContainerLayout"):
|
||||||
|
auto_layout = self.autoInstallContainerLayout
|
||||||
|
auto_widget = self.autoInstallContainer
|
||||||
|
QTimer.singleShot(0, lambda: (
|
||||||
|
auto_layout.invalidate(),
|
||||||
|
auto_widget.adjustSize(),
|
||||||
|
auto_widget.updateGeometry()
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
def openSystemOverlay(self):
|
def openSystemOverlay(self):
|
||||||
"""Opens the system overlay dialog."""
|
"""Opens the system overlay dialog."""
|
||||||
@@ -1765,7 +1802,22 @@ class MainWindow(QMainWindow):
|
|||||||
self.gamesDisplayCombo.setCurrentIndex(idx)
|
self.gamesDisplayCombo.setCurrentIndex(idx)
|
||||||
formLayout.addRow(self.gamesDisplayTitle, self.gamesDisplayCombo)
|
formLayout.addRow(self.gamesDisplayTitle, self.gamesDisplayCombo)
|
||||||
|
|
||||||
# 4. Proxy settings
|
# 4 Gamepad Type
|
||||||
|
self.gamepadTypeCombo = QComboBox()
|
||||||
|
self.gamepadTypeCombo.addItems(["Xbox", "PlayStation"])
|
||||||
|
self.gamepadTypeCombo.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||||
|
self.gamepadTypeCombo.setStyleSheet(self.theme.SETTINGS_COMBO_STYLE)
|
||||||
|
self.gamepadTypeTitle = QLabel(_("Gamepad Type:"))
|
||||||
|
self.gamepadTypeTitle.setStyleSheet(self.theme.PARAMS_TITLE_STYLE)
|
||||||
|
self.gamepadTypeTitle.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||||
|
current_type_str = read_gamepad_type()
|
||||||
|
if current_type_str == "playstation":
|
||||||
|
self.gamepadTypeCombo.setCurrentText("PlayStation")
|
||||||
|
else:
|
||||||
|
self.gamepadTypeCombo.setCurrentText("Xbox")
|
||||||
|
formLayout.addRow(self.gamepadTypeTitle, self.gamepadTypeCombo)
|
||||||
|
|
||||||
|
# 5. Proxy settings
|
||||||
self.proxyUrlEdit = CustomLineEdit(self, theme=self.theme)
|
self.proxyUrlEdit = CustomLineEdit(self, theme=self.theme)
|
||||||
self.proxyUrlEdit.setPlaceholderText(_("Proxy URL"))
|
self.proxyUrlEdit.setPlaceholderText(_("Proxy URL"))
|
||||||
self.proxyUrlEdit.setStyleSheet(self.theme.PROXY_INPUT_STYLE)
|
self.proxyUrlEdit.setStyleSheet(self.theme.PROXY_INPUT_STYLE)
|
||||||
@@ -1797,7 +1849,7 @@ class MainWindow(QMainWindow):
|
|||||||
self.proxyPasswordTitle.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
self.proxyPasswordTitle.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||||
formLayout.addRow(self.proxyPasswordTitle, self.proxyPasswordEdit)
|
formLayout.addRow(self.proxyPasswordTitle, self.proxyPasswordEdit)
|
||||||
|
|
||||||
# 5. Fullscreen setting for application
|
# 6. Fullscreen setting for application
|
||||||
self.fullscreenCheckBox = QCheckBox(_("Launch Application in Fullscreen"))
|
self.fullscreenCheckBox = QCheckBox(_("Launch Application in Fullscreen"))
|
||||||
self.fullscreenCheckBox.setStyleSheet(self.theme.SETTINGS_CHECKBOX_STYLE)
|
self.fullscreenCheckBox.setStyleSheet(self.theme.SETTINGS_CHECKBOX_STYLE)
|
||||||
self.fullscreenCheckBox.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
self.fullscreenCheckBox.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||||
@@ -1808,7 +1860,19 @@ class MainWindow(QMainWindow):
|
|||||||
self.fullscreenCheckBox.setChecked(current_fullscreen)
|
self.fullscreenCheckBox.setChecked(current_fullscreen)
|
||||||
formLayout.addRow(self.fullscreenTitle, self.fullscreenCheckBox)
|
formLayout.addRow(self.fullscreenTitle, self.fullscreenCheckBox)
|
||||||
|
|
||||||
# 6. Automatic fullscreen on gamepad connection
|
# 7. Minimize to tray setting
|
||||||
|
self.minimizeToTrayCheckBox = QCheckBox(_("Minimize to tray on close"))
|
||||||
|
self.minimizeToTrayCheckBox.setStyleSheet(self.theme.SETTINGS_CHECKBOX_STYLE)
|
||||||
|
self.minimizeToTrayCheckBox.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||||
|
self.minimizeToTrayTitle = QLabel(_("Application Close Mode:"))
|
||||||
|
self.minimizeToTrayTitle.setStyleSheet(self.theme.PARAMS_TITLE_STYLE)
|
||||||
|
self.minimizeToTrayTitle.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||||
|
current_minimize_to_tray = read_minimize_to_tray()
|
||||||
|
self.minimizeToTrayCheckBox.setChecked(current_minimize_to_tray)
|
||||||
|
self.minimizeToTrayCheckBox.toggled.connect(lambda checked: save_minimize_to_tray(checked))
|
||||||
|
formLayout.addRow(self.minimizeToTrayTitle, self.minimizeToTrayCheckBox)
|
||||||
|
|
||||||
|
# 8. Automatic fullscreen on gamepad connection
|
||||||
self.autoFullscreenGamepadCheckBox = QCheckBox(_("Auto Fullscreen on Gamepad connected"))
|
self.autoFullscreenGamepadCheckBox = QCheckBox(_("Auto Fullscreen on Gamepad connected"))
|
||||||
self.autoFullscreenGamepadCheckBox.setStyleSheet(self.theme.SETTINGS_CHECKBOX_STYLE)
|
self.autoFullscreenGamepadCheckBox.setStyleSheet(self.theme.SETTINGS_CHECKBOX_STYLE)
|
||||||
self.autoFullscreenGamepadCheckBox.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
self.autoFullscreenGamepadCheckBox.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||||
@@ -1820,7 +1884,7 @@ class MainWindow(QMainWindow):
|
|||||||
self.autoFullscreenGamepadCheckBox.setChecked(current_auto_fullscreen)
|
self.autoFullscreenGamepadCheckBox.setChecked(current_auto_fullscreen)
|
||||||
formLayout.addRow(self.autoFullscreenGamepadTitle, self.autoFullscreenGamepadCheckBox)
|
formLayout.addRow(self.autoFullscreenGamepadTitle, self.autoFullscreenGamepadCheckBox)
|
||||||
|
|
||||||
# 7. Gamepad haptic feedback config
|
# 9. Gamepad haptic feedback config
|
||||||
self.gamepadRumbleCheckBox = QCheckBox(_("Gamepad haptic feedback"))
|
self.gamepadRumbleCheckBox = QCheckBox(_("Gamepad haptic feedback"))
|
||||||
self.gamepadRumbleCheckBox.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
self.gamepadRumbleCheckBox.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||||
self.gamepadRumbleCheckBox.setStyleSheet(self.theme.SETTINGS_CHECKBOX_STYLE)
|
self.gamepadRumbleCheckBox.setStyleSheet(self.theme.SETTINGS_CHECKBOX_STYLE)
|
||||||
@@ -1831,7 +1895,7 @@ class MainWindow(QMainWindow):
|
|||||||
self.gamepadRumbleCheckBox.setChecked(current_rumble_state)
|
self.gamepadRumbleCheckBox.setChecked(current_rumble_state)
|
||||||
formLayout.addRow(self.gamepadRumbleTitle, self.gamepadRumbleCheckBox)
|
formLayout.addRow(self.gamepadRumbleTitle, self.gamepadRumbleCheckBox)
|
||||||
|
|
||||||
# # 8. Legendary Authentication
|
# # 9. Legendary Authentication
|
||||||
# self.legendaryAuthButton = AutoSizeButton(
|
# self.legendaryAuthButton = AutoSizeButton(
|
||||||
# _("Open Legendary Login"),
|
# _("Open Legendary Login"),
|
||||||
# icon=self.theme_manager.get_icon("login")self.theme_manager.get_icon("login")
|
# icon=self.theme_manager.get_icon("login")self.theme_manager.get_icon("login")
|
||||||
@@ -2011,6 +2075,19 @@ class MainWindow(QMainWindow):
|
|||||||
rumble_enabled = self.gamepadRumbleCheckBox.isChecked()
|
rumble_enabled = self.gamepadRumbleCheckBox.isChecked()
|
||||||
save_rumble_config(rumble_enabled)
|
save_rumble_config(rumble_enabled)
|
||||||
|
|
||||||
|
gamepad_type_text = self.gamepadTypeCombo.currentText()
|
||||||
|
gpad_type = "playstation" if gamepad_type_text == "PlayStation" else "xbox"
|
||||||
|
save_gamepad_type(gpad_type)
|
||||||
|
|
||||||
|
if hasattr(self, 'input_manager'):
|
||||||
|
if gpad_type == "playstation":
|
||||||
|
self.input_manager.gamepad_type = GamepadType.PLAYSTATION
|
||||||
|
elif gpad_type == "xbox":
|
||||||
|
self.input_manager.gamepad_type = GamepadType.XBOX
|
||||||
|
else:
|
||||||
|
self.input_manager.gamepad_type = GamepadType.UNKNOWN
|
||||||
|
self.updateControlHints()
|
||||||
|
|
||||||
for card in self.game_library_manager.game_card_cache.values():
|
for card in self.game_library_manager.game_card_cache.values():
|
||||||
card.update_badge_visibility(filter_key)
|
card.update_badge_visibility(filter_key)
|
||||||
|
|
||||||
@@ -2943,10 +3020,12 @@ class MainWindow(QMainWindow):
|
|||||||
logger.error(f"Failed to launch game {exe_name}: {e}")
|
logger.error(f"Failed to launch game {exe_name}: {e}")
|
||||||
QMessageBox.warning(self, _("Error"), _("Failed to launch game: {0}").format(str(e)))
|
QMessageBox.warning(self, _("Error"), _("Failed to launch game: {0}").format(str(e)))
|
||||||
|
|
||||||
|
|
||||||
def closeEvent(self, event):
|
def closeEvent(self, event):
|
||||||
"""Обработчик закрытия окна: сворачивает приложение в трей, если не требуется принудительный выход."""
|
"""Обработчик закрытия окна: проверяет настройку minimize_to_tray.
|
||||||
if hasattr(self, 'is_exiting') and self.is_exiting:
|
Если True — сворачиваем в трей (по умолчанию). Иначе — полностью закрываем.
|
||||||
|
"""
|
||||||
|
minimize_to_tray = read_minimize_to_tray() # Импорт read_minimize_to_tray из config_utils
|
||||||
|
if hasattr(self, 'is_exiting') and self.is_exiting or not minimize_to_tray:
|
||||||
# Принудительное закрытие: завершаем процессы и приложение
|
# Принудительное закрытие: завершаем процессы и приложение
|
||||||
for proc in self.game_processes:
|
for proc in self.game_processes:
|
||||||
try:
|
try:
|
||||||
|
Reference in New Issue
Block a user