Compare commits
14 Commits
Author | SHA1 | Date | |
---|---|---|---|
f765b5e840
|
|||
c54c3273a0
|
|||
502b5b5256
|
|||
0b45ba963a
|
|||
7becbf5de2
|
|||
66b4b82d49
|
|||
dbf3a30119
|
|||
4c2e2a9c8d
|
|||
802d5a2ba1
|
|||
1d47caf4aa
|
|||
502664438c
|
|||
f4e155dade
|
|||
74400d1389
|
|||
2a46cf7a2f
|
46
CHANGELOG.md
@ -10,24 +10,24 @@
|
||||
- Бейдж PortProton
|
||||
- Зависимость от `xdg-utils`
|
||||
- Интеграция статуса WeAntiCheatYet в карточку
|
||||
- Стили в AddGameDialog
|
||||
- Переключение полноэкранного режима через F11 или кнопку Select на геймпаде
|
||||
- Выбор QCheckBox через Enter или кнопку A на геймпаде
|
||||
- Выбор состояния `QCheckBox` через Enter или кнопку A на геймпаде
|
||||
- Закрытие диалога добавления игры через ESC или кнопку B на геймпаде
|
||||
- Закрытие окна приложения по комбинации клавиш Ctrl+Q
|
||||
- Сохранение и восстановление размера окна при перезапуске
|
||||
- Переключатель полноэкранного режима приложения
|
||||
- Пункт в контекстном меню «Открыть папку игры»
|
||||
- Пункты в контекстном меню «Добавить в Steam» и «Удалить из Steam»
|
||||
- Пункты в контекстном меню «Добавить в Избранное» и «Удалить из Избранного» для переключения статуса избранного через геймпад
|
||||
- Пункты в контекстном меню «Добавить в Избранное» и «Удалить из Избранного»
|
||||
- Метод сортировки «Сначала избранное»
|
||||
- Настройка автоматического перехода в полноэкранный режим при подключении геймпада (по умолчанию отключена)
|
||||
- Обработчики для QMenu и QComboBox при управлении геймпадом
|
||||
- Поддержка управления геймпадом в `QMenu` и `QComboBox`
|
||||
- Аргумент `--fullscreen` для запуска приложения в полноэкранном режиме
|
||||
- Оверлей на кнопку Insert или кнопку Xbox/PS на геймпаде для закрытия приложения, выключения, перезагрузки и перехода в спящий режим или между сессиями
|
||||
- Оверлей на кнопку Insert или кнопку Xbox/PS на геймпаде для закрытия приложения, выключения, перезагрузки и перехода в спящий режим или переключения между сессиями
|
||||
- [Gamescope сессия](https://git.linux-gaming.ru/Boria138/gamescope-session-portprotonqt)
|
||||
- Мапинги управления для Dualshock 4 и DualSense
|
||||
- Настройка тактильной обратной связи на геймпаде при запуске игры (по умолчанию отключена)
|
||||
- Пресеты управления для DualShock 4 и DualSense
|
||||
- Настройка тактильной отдачи на геймпаде при запуске игры (по умолчанию выключена)
|
||||
- Переводы пунктов настроек
|
||||
|
||||
### Changed
|
||||
- Обновлены все иконки
|
||||
@ -36,29 +36,31 @@
|
||||
- Логика контекстного меню вынесена в `ContextMenuManager`
|
||||
- Бейдж Steam теперь открывает Steam Community
|
||||
- Изменена лицензия с MIT на GPL-3.0 для совместимости с кодом от legendary
|
||||
- Оптимизирована генерация карточек для предотвращения задержек при поиске и изменении размера окна
|
||||
- Оптимизирована генерация карточек для плавной работы при поиске и изменении размера окна
|
||||
- Бейджи с карточек теперь отображаются также на странице с деталями, а не только в библиотеке
|
||||
- Установлена ширина бейджа в две трети ширины карточки
|
||||
- Бейджи источников (`Steam`, `EGS`, `PortProton`) теперь отображаются только при активном фильтре `all` или `favorites`
|
||||
- Карточки теперь фокусируются в направлении движения стрелок или D-pad: например, при нажатии D-pad вниз фокус переходит на карточку в следующей колонке, а не по порядку
|
||||
- Теперь D-pad можно зажимать для переключения карточек
|
||||
- D-pad больше не переключает вкладки, только RB и LB
|
||||
- Карточки теперь фокусируются в направлении движения стрелок или D-pad:
|
||||
- Поддерживается удержание D-pad для непрерывного переключения карточек
|
||||
- Объединён обработчик управления стрелками клавиатуры и D-pad для консистентности
|
||||
- D-pad больше не переключает вкладки (только кнопки RB/LB)
|
||||
- Кнопка добавления игры больше не фокусируется
|
||||
- Диалог добавления игры теперь открывается только в библиотеке
|
||||
- Удалены все упоминания PortProtonQT из кода и заменены на PortProtonQt
|
||||
- Размер карточек теперь меняется только при отпускании слайдера
|
||||
- Слайдер теперь управляется через тригеры на геймпаде
|
||||
|
||||
### Fixed
|
||||
- Обработка несуществующей темы с возвратом к «standard»
|
||||
- Открытие контекстного меню
|
||||
- Запуск при отсутствии exiftool
|
||||
- Переводы пунктов настроек
|
||||
- Бесконечное обращение к `get_portproton_location`
|
||||
- Ссылки на документацию в README
|
||||
- Traceback при загрузке placeholder при отсутствии обложек
|
||||
- Утечки памяти при загрузке обложек
|
||||
- Ошибки при подключении геймпада из-за работы в разных потоках
|
||||
- Многократное открытие диалога добавления игры при использовании геймпада
|
||||
- Перехват событий геймпада во время работы игры
|
||||
- Возврат к теме «standard» при выборе несуществующей темы
|
||||
- Корректное открытие контекстного меню
|
||||
- Запуск приложения при отсутствии `exiftool`
|
||||
- Предотвращено бесконечное обращение к `get_portproton_location`
|
||||
- Обновлены ссылки на документацию в README
|
||||
- Устранён traceback при отсутствии обложек (placeholder)
|
||||
- Устранены утечки памяти при загрузке обложек
|
||||
- Исправлены ошибки при подключении геймпада
|
||||
- Предотвращено многократное открытие диалога добавления игры через геймпад
|
||||
- Корректная обработка событий геймпада во время игры
|
||||
|
||||
---
|
||||
|
||||
|
@ -63,9 +63,9 @@
|
||||
- [X] Определиться с названием (PortProtonQt или PortProtonQT или вообще третий вариант)
|
||||
- [ ] Добавить данные с HowLongToBeat на страницу с деталями игры (?)
|
||||
- [X] Добавить виброотдачу на геймпаде при запуске игры
|
||||
- [ ] Исправить некорректную работу слайдера увеличения размера карточек([Последствия регрессии после этого коммита](https://github.com/Boria138/PortProtonQt/commit/aebdd60b5537280f06a922ff80469cd4ab27bc63)
|
||||
- [X] Исправить некорректную работу слайдера увеличения размера карточек([Последствия регрессии после этого коммита](https://github.com/Boria138/PortProtonQt/commit/aebdd60b5537280f06a922ff80469cd4ab27bc63)
|
||||
- [X] Исправить баг с наложением карточек друг на друга при изменении фильтра отображения ([Последствия регрессии после этого коммита](https://github.com/Boria138/PortProtonQt/commit/aebdd60b5537280f06a922ff80469cd4ab27bc63))
|
||||
- [ ] Скопировать логику управления с D-pad на стрелки с клавиатуры
|
||||
- [X] Скопировать логику управления с D-pad на стрелки с клавиатуры
|
||||
|
||||
### Установка (devel)
|
||||
|
||||
|
@ -49,6 +49,16 @@
|
||||
<caption>Settings</caption>
|
||||
<caption xml:lang="ru">Настройки</caption>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/portprotonqt/themes/standart/images/screenshots/%D0%9A%D0%BE%D0%BD%D1%82%D0%B5%D0%BA%D1%81%D1%82%D0%BD%D0%BE%D0%B5%20%D0%BC%D0%B5%D0%BD%D1%8E.png</image>
|
||||
<caption>Context Menu</caption>
|
||||
<caption xml:lang="ru">Контекстное меню</caption>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://git.linux-gaming.ru/Boria138/PortProtonQt/src/branch/main/portprotonqt/themes/standart/images/screenshots/%D0%9E%D0%B2%D0%B5%D1%80%D0%BB%D0%B5%D0%B9.png</image>
|
||||
<caption>Overlay</caption>
|
||||
<caption xml:lang="ru">Оверлей</caption>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<keywords>
|
||||
<keyword translate="no">wine</keyword>
|
||||
|
@ -20,9 +20,9 @@ Current translation status:
|
||||
|
||||
| Locale | Progress | Translated |
|
||||
| :----- | -------: | ---------: |
|
||||
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 162 |
|
||||
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 162 |
|
||||
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 162 of 162 |
|
||||
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 161 |
|
||||
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 161 |
|
||||
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 161 of 161 |
|
||||
|
||||
---
|
||||
|
||||
|
@ -20,9 +20,9 @@
|
||||
|
||||
| Локаль | Прогресс | Переведено |
|
||||
| :----- | -------: | ---------: |
|
||||
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 162 |
|
||||
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 162 |
|
||||
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 162 из 162 |
|
||||
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 161 |
|
||||
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 161 |
|
||||
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 161 из 161 |
|
||||
|
||||
---
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import numpy as np
|
||||
from PySide6.QtWidgets import QLabel, QPushButton, QWidget, QLayout, QStyleOption, QLayoutItem
|
||||
from PySide6.QtWidgets import QLabel, QPushButton, QWidget, QLayout, QLayoutItem
|
||||
from PySide6.QtCore import Qt, Signal, QRect, QPoint, QSize
|
||||
from PySide6.QtGui import QFont, QFontMetrics, QPainter
|
||||
|
||||
@ -133,18 +133,7 @@ class FlowLayout(QLayout):
|
||||
class ClickableLabel(QLabel):
|
||||
clicked = Signal()
|
||||
|
||||
def __init__(self, *args, icon=None, icon_size=16, icon_space=5, change_cursor=True, **kwargs):
|
||||
"""
|
||||
Поддерживаются вызовы:
|
||||
- ClickableLabel("текст", parent=...) – первый аргумент строка,
|
||||
- ClickableLabel(parent, text="...") – если первым аргументом передается родитель.
|
||||
|
||||
Аргументы:
|
||||
icon: QIcon или None – иконка, которая будет отрисована вместе с текстом.
|
||||
icon_size: int – размер иконки (ширина и высота).
|
||||
icon_space: int – отступ между иконкой и текстом.
|
||||
change_cursor: bool – изменять ли курсор на PointingHandCursor при наведении (по умолчанию True).
|
||||
"""
|
||||
def __init__(self, *args, icon=None, icon_size=16, icon_space=5, change_cursor=True, font_scale_factor=0.06, **kwargs):
|
||||
if args and isinstance(args[0], str):
|
||||
text = args[0]
|
||||
parent = kwargs.get("parent", None)
|
||||
@ -162,20 +151,38 @@ class ClickableLabel(QLabel):
|
||||
self._icon = icon
|
||||
self._icon_size = icon_size
|
||||
self._icon_space = icon_space
|
||||
self._font_scale_factor = font_scale_factor
|
||||
self._card_width = 250 # Значение по умолчанию
|
||||
if change_cursor:
|
||||
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.updateFontSize()
|
||||
|
||||
def setIcon(self, icon):
|
||||
"""Устанавливает иконку и перерисовывает виджет."""
|
||||
self._icon = icon
|
||||
self.update()
|
||||
|
||||
def icon(self):
|
||||
"""Возвращает текущую иконку."""
|
||||
return self._icon
|
||||
|
||||
def setIconSize(self, icon_size: int, icon_space: int):
|
||||
self._icon_size = icon_size
|
||||
self._icon_space = icon_space
|
||||
self.update()
|
||||
|
||||
def setCardWidth(self, card_width: int):
|
||||
"""Обновляет ширину карточки и пересчитывает размер шрифта."""
|
||||
self._card_width = card_width
|
||||
self.updateFontSize()
|
||||
|
||||
def updateFontSize(self):
|
||||
"""Обновляет размер шрифта на основе card_width и font_scale_factor."""
|
||||
font = self.font()
|
||||
font_size = int(self._card_width * self._font_scale_factor)
|
||||
font.setPointSize(max(8, font_size)) # Минимальный размер шрифта 8
|
||||
self.setFont(font)
|
||||
self.update()
|
||||
|
||||
def paintEvent(self, event):
|
||||
"""Переопределяем отрисовку: рисуем иконку и текст в одном лейбле."""
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
|
||||
@ -190,7 +197,6 @@ class ClickableLabel(QLabel):
|
||||
text = self.text()
|
||||
|
||||
if self._icon:
|
||||
# Получаем QPixmap нужного размера
|
||||
pixmap = self._icon.pixmap(icon_size, icon_size)
|
||||
icon_rect = QRect(0, 0, icon_size, icon_size)
|
||||
icon_rect.moveTop(rect.top() + (rect.height() - icon_size) // 2)
|
||||
@ -214,13 +220,11 @@ class ClickableLabel(QLabel):
|
||||
if pixmap:
|
||||
icon_rect.moveLeft(x)
|
||||
text_rect = QRect(x + icon_size + spacing, y, text_width, text_height)
|
||||
painter.drawPixmap(icon_rect, pixmap)
|
||||
else:
|
||||
# Устанавливаем text_rect для меток без иконки (например, favoriteLabel)
|
||||
text_rect = QRect(x, y, text_width, text_height)
|
||||
|
||||
option = QStyleOption()
|
||||
option.initFrom(self)
|
||||
if pixmap:
|
||||
painter.drawPixmap(icon_rect, pixmap)
|
||||
self.style().drawItemText(
|
||||
painter,
|
||||
text_rect,
|
||||
|
@ -43,6 +43,7 @@ class GameCard(QFrame):
|
||||
self.game_source = game_source
|
||||
self.last_launch_ts = last_launch_ts
|
||||
self.playtime_seconds = playtime_seconds
|
||||
self.card_width = card_width
|
||||
|
||||
self.select_callback = select_callback
|
||||
self.context_menu_manager = context_menu_manager
|
||||
@ -54,6 +55,10 @@ class GameCard(QFrame):
|
||||
self.display_filter = read_display_filter()
|
||||
self.current_theme_name = read_theme_from_config()
|
||||
|
||||
self.steam_visible = (str(game_source).lower() == "steam" and self.display_filter in ("all", "favorites"))
|
||||
self.egs_visible = (str(game_source).lower() == "epic" and self.display_filter in ("all", "favorites"))
|
||||
self.portproton_visible = (str(game_source).lower() == "portproton" and self.display_filter in ("all", "favorites"))
|
||||
|
||||
# Дополнительное пространство для анимации
|
||||
extra_margin = 20
|
||||
self.setFixedSize(card_width + extra_margin, int(card_width * 1.6) + extra_margin)
|
||||
@ -121,9 +126,11 @@ class GameCard(QFrame):
|
||||
self.update_favorite_icon()
|
||||
self.favoriteLabel.raise_()
|
||||
|
||||
steam_visible = (str(game_source).lower() == "steam" and self.display_filter in ("all", "favorites"))
|
||||
egs_visible = (str(game_source).lower() == "epic" and self.display_filter in ("all", "favorites"))
|
||||
portproton_visible = (str(game_source).lower() == "portproton" and self.display_filter in ("all", "favorites"))
|
||||
# Определяем общие параметры для бейджей
|
||||
badge_width = int(card_width * 2/3)
|
||||
icon_size = int(card_width * 0.06) # 6% от ширины карточки
|
||||
icon_space = int(card_width * 0.012) # 1.2% от ширины карточки
|
||||
font_scale_factor = 0.06 # Шрифт будет 6% от card_width
|
||||
|
||||
# ProtonDB бейдж
|
||||
tier_text = self.getProtonDBText(protondb_tier)
|
||||
@ -134,17 +141,17 @@ class GameCard(QFrame):
|
||||
tier_text,
|
||||
icon=icon,
|
||||
parent=coverWidget,
|
||||
icon_size=16,
|
||||
icon_space=3,
|
||||
icon_size=icon_size,
|
||||
icon_space=icon_space,
|
||||
font_scale_factor=font_scale_factor
|
||||
)
|
||||
self.protondbLabel.setStyleSheet(self.theme.get_protondb_badge_style(protondb_tier))
|
||||
self.protondbLabel.setFixedWidth(int(card_width * 2/3))
|
||||
protondb_visible = True
|
||||
self.protondbLabel.setFixedWidth(badge_width)
|
||||
self.protondbLabel.setCardWidth(card_width)
|
||||
else:
|
||||
self.protondbLabel = ClickableLabel("", parent=coverWidget, icon_size=16, icon_space=3)
|
||||
self.protondbLabel.setFixedWidth(int(card_width * 2/3))
|
||||
self.protondbLabel = ClickableLabel("", parent=coverWidget, icon_size=icon_size, icon_space=icon_space)
|
||||
self.protondbLabel.setFixedWidth(badge_width)
|
||||
self.protondbLabel.setVisible(False)
|
||||
protondb_visible = False
|
||||
|
||||
# Steam бейдж
|
||||
steam_icon = self.theme_manager.get_icon("steam")
|
||||
@ -152,12 +159,14 @@ class GameCard(QFrame):
|
||||
"Steam",
|
||||
icon=steam_icon,
|
||||
parent=coverWidget,
|
||||
icon_size=16,
|
||||
icon_space=5,
|
||||
icon_size=icon_size,
|
||||
icon_space=icon_space,
|
||||
font_scale_factor=font_scale_factor
|
||||
)
|
||||
self.steamLabel.setStyleSheet(self.theme.STEAM_BADGE_STYLE)
|
||||
self.steamLabel.setFixedWidth(int(card_width * 2/3))
|
||||
self.steamLabel.setVisible(steam_visible)
|
||||
self.steamLabel.setFixedWidth(badge_width)
|
||||
self.steamLabel.setCardWidth(card_width)
|
||||
self.steamLabel.setVisible(self.steam_visible)
|
||||
|
||||
# Epic Games Store бейдж
|
||||
egs_icon = self.theme_manager.get_icon("steam")
|
||||
@ -165,27 +174,31 @@ class GameCard(QFrame):
|
||||
"Epic Games",
|
||||
icon=egs_icon,
|
||||
parent=coverWidget,
|
||||
icon_size=16,
|
||||
icon_space=5,
|
||||
icon_size=icon_size,
|
||||
icon_space=icon_space,
|
||||
font_scale_factor=font_scale_factor,
|
||||
change_cursor=False
|
||||
)
|
||||
self.egsLabel.setStyleSheet(self.theme.STEAM_BADGE_STYLE)
|
||||
self.egsLabel.setFixedWidth(int(card_width * 2/3))
|
||||
self.egsLabel.setVisible(egs_visible)
|
||||
self.egsLabel.setFixedWidth(badge_width)
|
||||
self.egsLabel.setCardWidth(card_width)
|
||||
self.egsLabel.setVisible(self.egs_visible)
|
||||
|
||||
# PortProton badge
|
||||
# PortProton бейдж
|
||||
portproton_icon = self.theme_manager.get_icon("ppqt-tray")
|
||||
self.portprotonLabel = ClickableLabel(
|
||||
"PortProton",
|
||||
icon=portproton_icon,
|
||||
parent=coverWidget,
|
||||
icon_size=16,
|
||||
icon_space=5,
|
||||
icon_size=icon_size,
|
||||
icon_space=icon_space,
|
||||
font_scale_factor=font_scale_factor,
|
||||
change_cursor=False
|
||||
)
|
||||
self.portprotonLabel.setStyleSheet(self.theme.STEAM_BADGE_STYLE)
|
||||
self.portprotonLabel.setFixedWidth(int(card_width * 2/3))
|
||||
self.portprotonLabel.setVisible(portproton_visible)
|
||||
self.portprotonLabel.setFixedWidth(badge_width)
|
||||
self.portprotonLabel.setCardWidth(card_width)
|
||||
self.portprotonLabel.setVisible(self.portproton_visible)
|
||||
|
||||
# WeAntiCheatYet бейдж
|
||||
anticheat_text = self.getAntiCheatText(anticheat_status)
|
||||
@ -196,53 +209,20 @@ class GameCard(QFrame):
|
||||
anticheat_text,
|
||||
icon=icon,
|
||||
parent=coverWidget,
|
||||
icon_size=16,
|
||||
icon_space=3,
|
||||
icon_size=icon_size,
|
||||
icon_space=icon_space,
|
||||
font_scale_factor=font_scale_factor
|
||||
)
|
||||
self.anticheatLabel.setStyleSheet(self.theme.get_anticheat_badge_style(anticheat_status))
|
||||
self.anticheatLabel.setFixedWidth(int(card_width * 2/3))
|
||||
anticheat_visible = True
|
||||
self.anticheatLabel.setFixedWidth(badge_width)
|
||||
self.anticheatLabel.setCardWidth(card_width)
|
||||
else:
|
||||
self.anticheatLabel = ClickableLabel("", parent=coverWidget, icon_size=16, icon_space=3)
|
||||
self.anticheatLabel.setFixedWidth(int(card_width * 2/3))
|
||||
self.anticheatLabel = ClickableLabel("", parent=coverWidget, icon_size=icon_size, icon_space=icon_space)
|
||||
self.anticheatLabel.setFixedWidth(badge_width)
|
||||
self.anticheatLabel.setVisible(False)
|
||||
anticheat_visible = False
|
||||
|
||||
# Расположение бейджей
|
||||
right_margin = 8
|
||||
badge_spacing = 5
|
||||
top_y = 10
|
||||
badge_y_positions = []
|
||||
badge_width = int(card_width * 2/3)
|
||||
if steam_visible:
|
||||
steam_x = card_width - badge_width - right_margin
|
||||
self.steamLabel.move(steam_x, top_y)
|
||||
badge_y_positions.append(top_y + self.steamLabel.height())
|
||||
if egs_visible:
|
||||
egs_x = card_width - badge_width - right_margin
|
||||
egs_y = badge_y_positions[-1] + badge_spacing if badge_y_positions else top_y
|
||||
self.egsLabel.move(egs_x, egs_y)
|
||||
badge_y_positions.append(egs_y + self.egsLabel.height())
|
||||
if portproton_visible:
|
||||
portproton_x = card_width - badge_width - right_margin
|
||||
portproton_y = badge_y_positions[-1] + badge_spacing if badge_y_positions else top_y
|
||||
self.portprotonLabel.move(portproton_x, portproton_y)
|
||||
badge_y_positions.append(portproton_y + self.portprotonLabel.height())
|
||||
if protondb_visible:
|
||||
protondb_x = card_width - badge_width - right_margin
|
||||
protondb_y = badge_y_positions[-1] + badge_spacing if badge_y_positions else top_y
|
||||
self.protondbLabel.move(protondb_x, protondb_y)
|
||||
badge_y_positions.append(protondb_y + self.protondbLabel.height())
|
||||
if anticheat_visible:
|
||||
anticheat_x = card_width - badge_width - right_margin
|
||||
anticheat_y = badge_y_positions[-1] + badge_spacing if badge_y_positions else top_y
|
||||
self.anticheatLabel.move(anticheat_x, anticheat_y)
|
||||
|
||||
self.anticheatLabel.raise_()
|
||||
self.protondbLabel.raise_()
|
||||
self.portprotonLabel.raise_()
|
||||
self.egsLabel.raise_()
|
||||
self.steamLabel.raise_()
|
||||
self._position_badges(card_width)
|
||||
self.protondbLabel.clicked.connect(self.open_protondb_report)
|
||||
self.steamLabel.clicked.connect(self.open_steam_page)
|
||||
self.anticheatLabel.clicked.connect(self.open_weanticheatyet_page)
|
||||
@ -255,8 +235,79 @@ class GameCard(QFrame):
|
||||
nameLabel.setStyleSheet(self.theme.GAME_CARD_NAME_LABEL_STYLE)
|
||||
layout.addWidget(nameLabel)
|
||||
|
||||
def _position_badges(self, card_width):
|
||||
"""Позиционирует бейджи на основе ширины карточки."""
|
||||
right_margin = 8
|
||||
badge_spacing = int(card_width * 0.02) # 2% от ширины карточки
|
||||
top_y = 10
|
||||
badge_y_positions = []
|
||||
badge_width = int(card_width * 2/3)
|
||||
|
||||
badges = [
|
||||
(self.steam_visible, self.steamLabel),
|
||||
(self.egs_visible, self.egsLabel),
|
||||
(self.portproton_visible, self.portprotonLabel),
|
||||
(bool(self.getProtonDBText(self.protondb_tier)), self.protondbLabel),
|
||||
(bool(self.getAntiCheatText(self.anticheat_status)), self.anticheatLabel),
|
||||
]
|
||||
|
||||
for is_visible, badge in badges:
|
||||
if is_visible:
|
||||
badge_x = card_width - badge_width - right_margin
|
||||
badge_y = badge_y_positions[-1] + badge_spacing if badge_y_positions else top_y
|
||||
badge.move(badge_x, badge_y)
|
||||
badge_y_positions.append(badge_y + badge.height())
|
||||
|
||||
# Поднимаем бейджи в правильном порядке (от нижнего к верхнему)
|
||||
self.anticheatLabel.raise_()
|
||||
self.protondbLabel.raise_()
|
||||
self.portprotonLabel.raise_()
|
||||
self.egsLabel.raise_()
|
||||
self.steamLabel.raise_()
|
||||
|
||||
def update_card_size(self, new_width: int):
|
||||
"""Обновляет размер карточки, обложки и бейджей."""
|
||||
self.card_width = new_width
|
||||
extra_margin = 20
|
||||
self.setFixedSize(new_width + extra_margin, int(new_width * 1.6) + extra_margin)
|
||||
|
||||
if self.coverLabel is None:
|
||||
return
|
||||
|
||||
coverWidget = self.coverLabel.parentWidget()
|
||||
if coverWidget is None:
|
||||
return
|
||||
|
||||
coverWidget.setFixedSize(new_width, int(new_width * 1.2))
|
||||
self.coverLabel.setFixedSize(new_width, int(new_width * 1.2))
|
||||
|
||||
label_ref = weakref.ref(self.coverLabel)
|
||||
def on_cover_loaded(pixmap):
|
||||
label = label_ref()
|
||||
if label:
|
||||
scaled_pixmap = pixmap.scaled(new_width, int(new_width * 1.2), Qt.AspectRatioMode.KeepAspectRatioByExpanding, Qt.TransformationMode.SmoothTransformation)
|
||||
rounded_pixmap = round_corners(scaled_pixmap, 15)
|
||||
label.setPixmap(rounded_pixmap)
|
||||
|
||||
load_pixmap_async(self.cover_path or "", new_width, int(new_width * 1.2), on_cover_loaded)
|
||||
|
||||
# Обновляем размеры и шрифты бейджей
|
||||
badge_width = int(new_width * 2/3)
|
||||
icon_size = int(new_width * 0.06)
|
||||
icon_space = int(new_width * 0.012)
|
||||
for label in [self.steamLabel, self.egsLabel, self.portprotonLabel, self.protondbLabel, self.anticheatLabel]:
|
||||
if label is not None:
|
||||
label.setFixedWidth(badge_width)
|
||||
label.setIconSize(icon_size, icon_space)
|
||||
label.setCardWidth(new_width) # Пересчитываем размер шрифта
|
||||
|
||||
# Перепозиционируем бейджи
|
||||
self._position_badges(new_width)
|
||||
|
||||
self.update()
|
||||
|
||||
def update_badge_visibility(self, display_filter: str):
|
||||
"""Update badge visibility based on the provided display_filter."""
|
||||
"""Обновляет видимость бейджей на основе display_filter."""
|
||||
self.display_filter = display_filter
|
||||
self.steam_visible = (str(self.game_source).lower() == "steam" and display_filter in ("all", "favorites"))
|
||||
self.egs_visible = (str(self.game_source).lower() == "epic" and display_filter in ("all", "favorites"))
|
||||
@ -271,35 +322,8 @@ class GameCard(QFrame):
|
||||
self.protondbLabel.setVisible(protondb_visible)
|
||||
self.anticheatLabel.setVisible(anticheat_visible)
|
||||
|
||||
# Подготавливаем список всех бейджей с их текущей видимостью
|
||||
badges = [
|
||||
(self.steam_visible, self.steamLabel),
|
||||
(self.egs_visible, self.egsLabel),
|
||||
(self.portproton_visible, self.portprotonLabel),
|
||||
(protondb_visible, self.protondbLabel),
|
||||
(anticheat_visible, self.anticheatLabel),
|
||||
]
|
||||
|
||||
# Пересчитываем позиции бейджей
|
||||
right_margin = 8
|
||||
badge_spacing = 5
|
||||
top_y = 10
|
||||
badge_y_positions = []
|
||||
badge_width = int(self.coverLabel.width() * 2/3)
|
||||
|
||||
for is_visible, badge in badges:
|
||||
if is_visible:
|
||||
badge_x = self.coverLabel.width() - badge_width - right_margin
|
||||
badge_y = badge_y_positions[-1] + badge_spacing if badge_y_positions else top_y
|
||||
badge.move(badge_x, badge_y)
|
||||
badge_y_positions.append(badge_y + badge.height())
|
||||
|
||||
# Поднимаем бейджи в правильном порядке (от нижнего к верхнему)
|
||||
self.anticheatLabel.raise_()
|
||||
self.protondbLabel.raise_()
|
||||
self.portprotonLabel.raise_()
|
||||
self.egsLabel.raise_()
|
||||
self.steamLabel.raise_()
|
||||
# Перепозиционируем бейджи
|
||||
self._position_badges(self.card_width)
|
||||
|
||||
def _show_context_menu(self, pos):
|
||||
"""Delegate context menu display to ContextMenuManager."""
|
||||
|
@ -26,7 +26,9 @@ class MainWindowProtocol(Protocol):
|
||||
def toggleGame(self, exec_line: str | None, button: QWidget | None = None) -> None:
|
||||
...
|
||||
def openSystemOverlay(self) -> None:
|
||||
...
|
||||
...
|
||||
def on_slider_released(self) -> None:
|
||||
...
|
||||
stackedWidget: QStackedWidget
|
||||
tabButtons: dict[int, QWidget]
|
||||
gamesListWidget: QWidget
|
||||
@ -34,18 +36,20 @@ class MainWindowProtocol(Protocol):
|
||||
current_exec_line: str | None
|
||||
current_add_game_dialog: QDialog | None
|
||||
|
||||
# Mapping of actions to evdev button codes, includes Xbox and Playstation controllers
|
||||
# Mapping of actions to evdev button codes, includes Xbox and PlayStation controllers
|
||||
# https://github.com/torvalds/linux/blob/master/drivers/hid/hid-playstation.c
|
||||
# https://github.com/torvalds/linux/blob/master/drivers/input/joystick/xpad.c
|
||||
BUTTONS = {
|
||||
'confirm': {ecodes.BTN_A, ecodes.BTN_SOUTH}, # A / Cross
|
||||
'back': {ecodes.BTN_B, ecodes.BTN_EAST}, # B / Circle
|
||||
'add_game': {ecodes.BTN_Y, ecodes.BTN_NORTH}, # Y / Triangle
|
||||
'prev_tab': {ecodes.BTN_TL}, # LB / L1
|
||||
'next_tab': {ecodes.BTN_TR}, # RB / R1
|
||||
'context_menu': {ecodes.BTN_START}, # Start / Options
|
||||
'menu': {ecodes.BTN_SELECT}, # Select / Share
|
||||
'guide': {ecodes.BTN_MODE}, # Xbox / PS Home
|
||||
'confirm': {ecodes.BTN_A, ecodes.BTN_SOUTH}, # A (Xbox) / Cross (PS)
|
||||
'back': {ecodes.BTN_B, ecodes.BTN_EAST}, # B (Xbox) / Circle (PS)
|
||||
'add_game': {ecodes.BTN_Y, ecodes.BTN_NORTH}, # Y (Xbox) / Triangle (PS)
|
||||
'prev_tab': {ecodes.BTN_TL}, # LB (Xbox) / L1 (PS)
|
||||
'next_tab': {ecodes.BTN_TR}, # RB (Xbox) / R1 (PS)
|
||||
'context_menu': {ecodes.BTN_START}, # Start (Xbox) / Options (PS)
|
||||
'menu': {ecodes.BTN_SELECT}, # Select (Xbox) / Share (PS)
|
||||
'guide': {ecodes.BTN_MODE}, # Xbox Button / PS Button
|
||||
'increase_size': {ecodes.ABS_RZ}, # RT (Xbox) / R2 (PS)
|
||||
'decrease_size': {ecodes.ABS_Z}, # LT (Xbox) / L2 (PS)
|
||||
}
|
||||
|
||||
class InputManager(QObject):
|
||||
@ -83,6 +87,10 @@ class InputManager(QObject):
|
||||
self.running = True
|
||||
self._is_fullscreen = read_fullscreen_config()
|
||||
self.rumble_effect_id: int | None = None # Store the rumble effect ID
|
||||
self.lt_pressed = False
|
||||
self.rt_pressed = False
|
||||
self.last_trigger_time = 0.0
|
||||
self.trigger_cooldown = 0.2
|
||||
|
||||
# Add variables for continuous D-pad movement
|
||||
self.dpad_timer = QTimer(self)
|
||||
@ -106,8 +114,6 @@ class InputManager(QObject):
|
||||
@Slot(bool)
|
||||
def handle_fullscreen_slot(self, enable: bool) -> None:
|
||||
try:
|
||||
if read_fullscreen_config():
|
||||
return
|
||||
window = self._parent
|
||||
if not isinstance(window, QWidget):
|
||||
return
|
||||
@ -171,7 +177,7 @@ class InputManager(QObject):
|
||||
@Slot(int)
|
||||
def handle_button_slot(self, button_code: int) -> None:
|
||||
try:
|
||||
# Игнорировать события геймпада, если игра запущена
|
||||
# Ignore gamepad events if a game is launched
|
||||
if getattr(self._parent, '_gameLaunched', False):
|
||||
return
|
||||
|
||||
@ -237,7 +243,7 @@ class InputManager(QObject):
|
||||
focused.clearSelection()
|
||||
focused.hide()
|
||||
|
||||
# Закрытие AddGameDialog на кнопку B
|
||||
# Close AddGameDialog on B button
|
||||
if button_code in BUTTONS['back'] and isinstance(active, QDialog):
|
||||
active.reject()
|
||||
return
|
||||
@ -284,6 +290,20 @@ class InputManager(QObject):
|
||||
idx = (self._parent.stackedWidget.currentIndex() + 1) % len(self._parent.tabButtons)
|
||||
self._parent.switchTab(idx)
|
||||
self._parent.tabButtons[idx].setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
elif button_code in BUTTONS['increase_size'] and self._parent.stackedWidget.currentIndex() == 0:
|
||||
# Increase card size with RT (Xbox) / R2 (PS)
|
||||
size_slider = getattr(self._parent, 'sizeSlider', None)
|
||||
if size_slider:
|
||||
new_value = min(size_slider.value() + 10, size_slider.maximum())
|
||||
size_slider.setValue(new_value)
|
||||
self._parent.on_slider_released()
|
||||
elif button_code in BUTTONS['decrease_size'] and self._parent.stackedWidget.currentIndex() == 0:
|
||||
# Decrease card size with LT (Xbox) / L2 (PS)
|
||||
size_slider = getattr(self._parent, 'sizeSlider', None)
|
||||
if size_slider:
|
||||
new_value = max(size_slider.value() - 10, size_slider.minimum())
|
||||
size_slider.setValue(new_value)
|
||||
self._parent.on_slider_released()
|
||||
except Exception as e:
|
||||
logger.error(f"Error in handle_button_slot: {e}", exc_info=True)
|
||||
|
||||
@ -299,7 +319,7 @@ class InputManager(QObject):
|
||||
@Slot(int, int, float)
|
||||
def handle_dpad_slot(self, code: int, value: int, current_time: float) -> None:
|
||||
try:
|
||||
# Игнорировать события геймпада, если игра запущена
|
||||
# Ignore gamepad events if a game is launched
|
||||
if getattr(self._parent, '_gameLaunched', False):
|
||||
return
|
||||
|
||||
@ -525,242 +545,133 @@ class InputManager(QObject):
|
||||
if not app:
|
||||
return super().eventFilter(obj, event)
|
||||
|
||||
# Handle only key press events
|
||||
if not (isinstance(event, QKeyEvent) and event.type() == QEvent.Type.KeyPress):
|
||||
# Handle key press and release events
|
||||
if not isinstance(event, QKeyEvent):
|
||||
return super().eventFilter(obj, event)
|
||||
|
||||
key = event.key()
|
||||
modifiers = event.modifiers()
|
||||
focused = QApplication.focusWidget()
|
||||
popup = QApplication.activePopupWidget()
|
||||
|
||||
# Open system overlay with Insert
|
||||
if key == Qt.Key.Key_Insert:
|
||||
if not popup and not isinstance(QApplication.activeWindow(), QDialog):
|
||||
self._parent.openSystemOverlay()
|
||||
return True
|
||||
|
||||
# Close application with Ctrl+Q
|
||||
if key == Qt.Key.Key_Q and modifiers & Qt.KeyboardModifier.ControlModifier:
|
||||
app.quit()
|
||||
return True
|
||||
|
||||
# Закрытие AddGameDialog на Esc
|
||||
if key == Qt.Key.Key_Escape and isinstance(popup, QDialog):
|
||||
popup.reject() # Закрываем диалог
|
||||
return True
|
||||
|
||||
# Skip navigation keys if a popup is open
|
||||
if popup:
|
||||
return False
|
||||
|
||||
# FullscreenDialog navigation
|
||||
active_win = QApplication.activeWindow()
|
||||
if isinstance(active_win, FullscreenDialog):
|
||||
if key == Qt.Key.Key_Right:
|
||||
active_win.show_next()
|
||||
return True
|
||||
if key == Qt.Key.Key_Left:
|
||||
active_win.show_prev()
|
||||
return True
|
||||
if key in (Qt.Key.Key_Escape, Qt.Key.Key_Return, Qt.Key.Key_Enter, Qt.Key.Key_Backspace):
|
||||
active_win.close()
|
||||
return True
|
||||
|
||||
# Launch/stop game on detail page
|
||||
if self._parent.currentDetailPage and key in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
|
||||
if self._parent.current_exec_line:
|
||||
self._parent.toggleGame(self._parent.current_exec_line, None)
|
||||
return True
|
||||
|
||||
# Context menu for GameCard
|
||||
if isinstance(focused, GameCard):
|
||||
if key == Qt.Key.Key_F10 and Qt.KeyboardModifier.ShiftModifier:
|
||||
pos = QPoint(focused.width() // 2, focused.height() // 2)
|
||||
focused._show_context_menu(pos)
|
||||
return True
|
||||
|
||||
# Handle Up/Down keys for non-GameCard tabs
|
||||
if key in (Qt.Key.Key_Up, Qt.Key.Key_Down) and not isinstance(focused, GameCard):
|
||||
page = self._parent.stackedWidget.currentWidget()
|
||||
if key == Qt.Key.Key_Down:
|
||||
if isinstance(focused, NavLabel):
|
||||
focusables = page.findChildren(QWidget, options=Qt.FindChildOption.FindChildrenRecursively)
|
||||
focusables = [w for w in focusables if w.focusPolicy() & Qt.FocusPolicy.StrongFocus]
|
||||
if focusables:
|
||||
focusables[0].setFocus()
|
||||
return True
|
||||
elif focused:
|
||||
focused.focusNextChild()
|
||||
return True
|
||||
elif key == Qt.Key.Key_Up and focused:
|
||||
focused.focusPreviousChild()
|
||||
return True
|
||||
|
||||
# Tab switching with Left/Right keys (non-GameCard focus or no focus)
|
||||
idx = self._parent.stackedWidget.currentIndex()
|
||||
total = len(self._parent.tabButtons)
|
||||
if key == Qt.Key.Key_Left and (not isinstance(focused, GameCard) or focused is None):
|
||||
new = (idx - 1) % total
|
||||
self._parent.switchTab(new)
|
||||
self._parent.tabButtons[new].setFocus()
|
||||
return True
|
||||
if key == Qt.Key.Key_Right and (not isinstance(focused, GameCard) or focused is None):
|
||||
new = (idx + 1) % total
|
||||
self._parent.switchTab(new)
|
||||
self._parent.tabButtons[new].setFocus()
|
||||
return True
|
||||
|
||||
# Library tab navigation
|
||||
if self._parent.stackedWidget.currentIndex() == 0:
|
||||
game_cards = self._parent.gamesListWidget.findChildren(GameCard)
|
||||
scroll_area = self._parent.gamesListWidget.parentWidget()
|
||||
while scroll_area and not isinstance(scroll_area, QScrollArea):
|
||||
scroll_area = scroll_area.parentWidget()
|
||||
|
||||
if key in (Qt.Key.Key_Left, Qt.Key.Key_Right, Qt.Key.Key_Up, Qt.Key.Key_Down):
|
||||
if not game_cards:
|
||||
# Handle key press events
|
||||
if event.type() == QEvent.Type.KeyPress:
|
||||
# Open system overlay with Insert
|
||||
if key == Qt.Key.Key_Insert:
|
||||
if not popup and not isinstance(active_win, QDialog):
|
||||
self._parent.openSystemOverlay()
|
||||
return True
|
||||
|
||||
# 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)
|
||||
# Close application with Ctrl+Q
|
||||
if key == Qt.Key.Key_Q and modifiers & Qt.KeyboardModifier.ControlModifier:
|
||||
app.quit()
|
||||
return True
|
||||
|
||||
# Close AddGameDialog with Escape
|
||||
if key == Qt.Key.Key_Escape and isinstance(popup, QDialog):
|
||||
popup.reject()
|
||||
return True
|
||||
|
||||
# FullscreenDialog navigation
|
||||
if isinstance(active_win, FullscreenDialog):
|
||||
if key in (Qt.Key.Key_Escape, Qt.Key.Key_Return, Qt.Key.Key_Enter, Qt.Key.Key_Backspace):
|
||||
active_win.close()
|
||||
return True
|
||||
elif key in (Qt.Key.Key_Left, Qt.Key.Key_Right):
|
||||
# Navigate screenshots in FullscreenDialog
|
||||
if key == Qt.Key.Key_Left:
|
||||
active_win.show_prev()
|
||||
elif key == Qt.Key.Key_Right:
|
||||
active_win.show_next()
|
||||
return True # Consume event to prevent tab switching
|
||||
|
||||
# Handle tab switching with Left/Right arrow keys when not in GameCard focus
|
||||
if key in (Qt.Key.Key_Left, Qt.Key.Key_Right) and (not isinstance(focused, GameCard) or focused is None):
|
||||
idx = self._parent.stackedWidget.currentIndex()
|
||||
total = len(self._parent.tabButtons)
|
||||
if key == Qt.Key.Key_Left:
|
||||
new_idx = (idx - 1) % total
|
||||
self._parent.switchTab(new_idx)
|
||||
self._parent.tabButtons[new_idx].setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
return True
|
||||
elif key == Qt.Key.Key_Right:
|
||||
new_idx = (idx + 1) % total
|
||||
self._parent.switchTab(new_idx)
|
||||
self._parent.tabButtons[new_idx].setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
return True
|
||||
|
||||
# Group cards by rows based on y-coordinate
|
||||
rows = {}
|
||||
for card in game_cards:
|
||||
y = card.pos().y()
|
||||
if y not in rows:
|
||||
rows[y] = []
|
||||
rows[y].append(card)
|
||||
# Sort cards in each row by x-coordinate
|
||||
for y in rows:
|
||||
rows[y].sort(key=lambda c: c.pos().x())
|
||||
# Sort rows by y-coordinate
|
||||
sorted_rows = sorted(rows.items(), key=lambda x: x[0])
|
||||
|
||||
# Find current row and column
|
||||
current_y = focused.pos().y()
|
||||
current_row_idx = next(i for i, (y, _) in enumerate(sorted_rows) if y == current_y)
|
||||
current_row = sorted_rows[current_row_idx][1]
|
||||
current_col_idx = current_row.index(focused)
|
||||
|
||||
if key == Qt.Key.Key_Right:
|
||||
next_col_idx = current_col_idx + 1
|
||||
if next_col_idx < len(current_row):
|
||||
next_card = current_row[next_col_idx]
|
||||
next_card.setFocus()
|
||||
if scroll_area:
|
||||
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
||||
return True
|
||||
else:
|
||||
# Move to the first card of the next row if available
|
||||
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()
|
||||
if scroll_area:
|
||||
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
||||
return True
|
||||
elif key == Qt.Key.Key_Left:
|
||||
next_col_idx = current_col_idx - 1
|
||||
if next_col_idx >= 0:
|
||||
next_card = current_row[next_col_idx]
|
||||
next_card.setFocus()
|
||||
if scroll_area:
|
||||
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
||||
return True
|
||||
else:
|
||||
# Move to the last card of the previous row if available
|
||||
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()
|
||||
if scroll_area:
|
||||
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
||||
return True
|
||||
# Map arrow keys to D-pad press events for other contexts
|
||||
if key in (Qt.Key.Key_Up, Qt.Key.Key_Down, Qt.Key.Key_Left, Qt.Key.Key_Right):
|
||||
now = time.time()
|
||||
dpad_code = None
|
||||
dpad_value = 0
|
||||
if key == Qt.Key.Key_Up:
|
||||
dpad_code = ecodes.ABS_HAT0Y
|
||||
dpad_value = -1
|
||||
elif key == Qt.Key.Key_Down:
|
||||
next_row_idx = current_row_idx + 1
|
||||
if next_row_idx < len(sorted_rows):
|
||||
next_row = sorted_rows[next_row_idx][1]
|
||||
target_x = focused.pos().x()
|
||||
next_card = min(
|
||||
next_row,
|
||||
key=lambda c: abs(c.pos().x() - target_x),
|
||||
default=None
|
||||
)
|
||||
if next_card:
|
||||
next_card.setFocus()
|
||||
if scroll_area:
|
||||
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
||||
return True
|
||||
elif key == Qt.Key.Key_Up:
|
||||
next_row_idx = current_row_idx - 1
|
||||
if next_row_idx >= 0:
|
||||
next_row = sorted_rows[next_row_idx][1]
|
||||
target_x = focused.pos().x()
|
||||
next_card = min(
|
||||
next_row,
|
||||
key=lambda c: abs(c.pos().x() - target_x),
|
||||
default=None
|
||||
)
|
||||
if next_card:
|
||||
next_card.setFocus()
|
||||
if scroll_area:
|
||||
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
||||
return True
|
||||
elif current_row_idx == 0:
|
||||
self._parent.tabButtons[0].setFocus()
|
||||
return True
|
||||
dpad_code = ecodes.ABS_HAT0Y
|
||||
dpad_value = 1
|
||||
elif key == Qt.Key.Key_Left:
|
||||
dpad_code = ecodes.ABS_HAT0X
|
||||
dpad_value = -1
|
||||
elif key == Qt.Key.Key_Right:
|
||||
dpad_code = ecodes.ABS_HAT0X
|
||||
dpad_value = 1
|
||||
|
||||
# Navigate down into tab content
|
||||
if key == Qt.Key.Key_Down:
|
||||
if isinstance(focused, NavLabel):
|
||||
page = self._parent.stackedWidget.currentWidget()
|
||||
focusables = page.findChildren(QWidget, options=Qt.FindChildOption.FindChildrenRecursively)
|
||||
focusables = [w for w in focusables if w.focusPolicy() & Qt.FocusPolicy.StrongFocus]
|
||||
if focusables:
|
||||
focusables[0].setFocus()
|
||||
if dpad_code is not None:
|
||||
self.dpad_moved.emit(dpad_code, dpad_value, now)
|
||||
return True
|
||||
elif focused:
|
||||
focused.focusNextChild()
|
||||
|
||||
# Launch/stop game on detail page
|
||||
if self._parent.currentDetailPage and key in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
|
||||
if self._parent.current_exec_line:
|
||||
self._parent.toggleGame(self._parent.current_exec_line, None)
|
||||
return True
|
||||
|
||||
# Context menu for GameCard
|
||||
if isinstance(focused, GameCard):
|
||||
if key == Qt.Key.Key_F10 and modifiers & Qt.KeyboardModifier.ShiftModifier:
|
||||
pos = QPoint(focused.width() // 2, focused.height() // 2)
|
||||
focused._show_context_menu(pos)
|
||||
return True
|
||||
|
||||
# General actions: Activate, Back, Add
|
||||
if key in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
|
||||
self._parent.activateFocusedWidget()
|
||||
return True
|
||||
# Navigate up through tab content
|
||||
if key == Qt.Key.Key_Up:
|
||||
if isinstance(focused, NavLabel):
|
||||
elif key in (Qt.Key.Key_Escape, Qt.Key.Key_Backspace):
|
||||
if isinstance(focused, QLineEdit):
|
||||
return False
|
||||
self._parent.goBackDetailPage(self._parent.currentDetailPage)
|
||||
return True
|
||||
if focused is not None:
|
||||
focused.focusPreviousChild()
|
||||
elif key == Qt.Key.Key_E:
|
||||
if isinstance(focused, QLineEdit):
|
||||
return False
|
||||
# Only open AddGameDialog if in library tab (index 0)
|
||||
if self._parent.stackedWidget.currentIndex() == 0:
|
||||
self._parent.openAddGameDialog()
|
||||
return True
|
||||
|
||||
# Toggle fullscreen with F11
|
||||
if key == Qt.Key.Key_F11:
|
||||
self.toggle_fullscreen.emit(not self._is_fullscreen)
|
||||
return True
|
||||
|
||||
# General actions: Activate, Back, Add
|
||||
if key in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
|
||||
self._parent.activateFocusedWidget()
|
||||
return True
|
||||
elif key in (Qt.Key.Key_Escape, Qt.Key.Key_Backspace):
|
||||
if isinstance(focused, QLineEdit):
|
||||
return False
|
||||
self._parent.goBackDetailPage(self._parent.currentDetailPage)
|
||||
return True
|
||||
elif key == Qt.Key.Key_E:
|
||||
if isinstance(focused, QLineEdit):
|
||||
return False
|
||||
# Only open AddGameDialog if in library tab (index 0)
|
||||
if self._parent.stackedWidget.currentIndex() == 0:
|
||||
self._parent.openAddGameDialog()
|
||||
return True
|
||||
# Handle key release events for arrow keys
|
||||
elif event.type() == QEvent.Type.KeyRelease:
|
||||
if key in (Qt.Key.Key_Up, Qt.Key.Key_Down, Qt.Key.Key_Left, Qt.Key.Key_Right):
|
||||
now = time.time()
|
||||
dpad_code = None
|
||||
if key in (Qt.Key.Key_Up, Qt.Key.Key_Down):
|
||||
dpad_code = ecodes.ABS_HAT0Y
|
||||
elif key in (Qt.Key.Key_Left, Qt.Key.Key_Right):
|
||||
dpad_code = ecodes.ABS_HAT0X
|
||||
|
||||
# Toggle fullscreen with F11
|
||||
if key == Qt.Key.Key_F11:
|
||||
if read_fullscreen_config():
|
||||
return True
|
||||
self.toggle_fullscreen.emit(not self._is_fullscreen)
|
||||
return True
|
||||
if dpad_code is not None:
|
||||
# Emit release event with value 0 to stop continuous movement
|
||||
self.dpad_moved.emit(dpad_code, 0, now)
|
||||
return True
|
||||
|
||||
return super().eventFilter(obj, event)
|
||||
|
||||
@ -809,9 +720,9 @@ class InputManager(QObject):
|
||||
self.gamepad_thread.join()
|
||||
self.gamepad_thread = threading.Thread(target=self.monitor_gamepad, daemon=True)
|
||||
self.gamepad_thread.start()
|
||||
# Отправляем сигнал для полноэкранного режима только если:
|
||||
# 1. auto_fullscreen_gamepad включено
|
||||
# 2. fullscreen выключено (чтобы не конфликтовать с основной настройкой)
|
||||
# Send signal for fullscreen mode only if:
|
||||
# 1. auto_fullscreen_gamepad is enabled
|
||||
# 2. fullscreen is not already enabled (to avoid conflict)
|
||||
if read_auto_fullscreen_gamepad() and not read_fullscreen_config():
|
||||
self.toggle_fullscreen.emit(True)
|
||||
except Exception as e:
|
||||
@ -845,7 +756,26 @@ class InputManager(QObject):
|
||||
else:
|
||||
self.button_pressed.emit(event.code)
|
||||
elif event.type == ecodes.EV_ABS:
|
||||
self.dpad_moved.emit(event.code, event.value, now)
|
||||
if event.code in {ecodes.ABS_Z, ecodes.ABS_RZ}:
|
||||
# Проверяем, достаточно ли времени прошло с последнего срабатывания
|
||||
if now - self.last_trigger_time < self.trigger_cooldown:
|
||||
continue
|
||||
if event.code == ecodes.ABS_Z: # LT/L2
|
||||
if event.value > 128 and not self.lt_pressed:
|
||||
self.lt_pressed = True
|
||||
self.button_pressed.emit(event.code)
|
||||
self.last_trigger_time = now
|
||||
elif event.value <= 128 and self.lt_pressed:
|
||||
self.lt_pressed = False
|
||||
elif event.code == ecodes.ABS_RZ: # RT/R2
|
||||
if event.value > 128 and not self.rt_pressed:
|
||||
self.rt_pressed = True
|
||||
self.button_pressed.emit(event.code)
|
||||
self.last_trigger_time = now
|
||||
elif event.value <= 128 and self.rt_pressed:
|
||||
self.rt_pressed = False
|
||||
else:
|
||||
self.dpad_moved.emit(event.code, event.value, now)
|
||||
except OSError as e:
|
||||
if e.errno == 19: # ENODEV: No such device
|
||||
logger.info("Gamepad disconnected during event loop")
|
||||
|
@ -9,7 +9,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2025-06-11 23:15+0500\n"
|
||||
"POT-Creation-Date: 2025-06-14 10:37+0500\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: de_DE\n"
|
||||
@ -494,9 +494,6 @@ msgstr ""
|
||||
msgid "Launching"
|
||||
msgstr ""
|
||||
|
||||
msgid "System Overlay"
|
||||
msgstr ""
|
||||
|
||||
msgid "Reboot"
|
||||
msgstr ""
|
||||
|
||||
|
@ -9,7 +9,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2025-06-11 23:15+0500\n"
|
||||
"POT-Creation-Date: 2025-06-14 10:37+0500\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: es_ES\n"
|
||||
@ -494,9 +494,6 @@ msgstr ""
|
||||
msgid "Launching"
|
||||
msgstr ""
|
||||
|
||||
msgid "System Overlay"
|
||||
msgstr ""
|
||||
|
||||
msgid "Reboot"
|
||||
msgstr ""
|
||||
|
||||
|
@ -9,7 +9,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PortProtonQt 0.1.1\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2025-06-11 23:15+0500\n"
|
||||
"POT-Creation-Date: 2025-06-14 10:37+0500\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@ -492,9 +492,6 @@ msgstr ""
|
||||
msgid "Launching"
|
||||
msgstr ""
|
||||
|
||||
msgid "System Overlay"
|
||||
msgstr ""
|
||||
|
||||
msgid "Reboot"
|
||||
msgstr ""
|
||||
|
||||
|
@ -9,8 +9,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2025-06-11 23:15+0500\n"
|
||||
"PO-Revision-Date: 2025-06-11 23:15+0500\n"
|
||||
"POT-Creation-Date: 2025-06-14 10:37+0500\n"
|
||||
"PO-Revision-Date: 2025-06-14 10:37+0500\n"
|
||||
"Last-Translator: \n"
|
||||
"Language: ru_RU\n"
|
||||
"Language-Team: ru_RU <LL@li.org>\n"
|
||||
@ -384,10 +384,10 @@ msgid "Auto Fullscreen on Gamepad connected:"
|
||||
msgstr "Режим полноэкранного отображения приложения при подключении геймпада:"
|
||||
|
||||
msgid "Gamepad haptic feedback"
|
||||
msgstr "Тактильная обратная связь на геймпаде"
|
||||
msgstr "Тактильная отдача на геймпаде"
|
||||
|
||||
msgid "Gamepad haptic feedback:"
|
||||
msgstr "Тактильная обратная связь на геймпаде:"
|
||||
msgstr "Тактильная отдача на геймпаде:"
|
||||
|
||||
msgid "Save Settings"
|
||||
msgstr "Сохранить настройки"
|
||||
@ -503,9 +503,6 @@ msgstr "Невозможно запустить игру пока запущен
|
||||
msgid "Launching"
|
||||
msgstr "Идёт запуск"
|
||||
|
||||
msgid "System Overlay"
|
||||
msgstr "Системный оверлей"
|
||||
|
||||
msgid "Reboot"
|
||||
msgstr "Перезагрузить"
|
||||
|
||||
|
@ -535,11 +535,13 @@ class MainWindow(QMainWindow):
|
||||
def startSearchDebounce(self, text):
|
||||
self.searchDebounceTimer.start()
|
||||
|
||||
def on_slider_value_changed(self, value: int):
|
||||
self.card_width = value
|
||||
self.sizeSlider.setToolTip(f"{value} px")
|
||||
save_card_size(value)
|
||||
self.updateGameGrid()
|
||||
def on_slider_released(self):
|
||||
self.card_width = self.sizeSlider.value()
|
||||
self.sizeSlider.setToolTip(f"{self.card_width} px")
|
||||
save_card_size(self.card_width)
|
||||
for card in self.game_card_cache.values():
|
||||
card.update_card_size(self.card_width)
|
||||
self.updateGameGrid()
|
||||
|
||||
def filterGamesDelayed(self):
|
||||
"""Filters games based on search text and updates the grid."""
|
||||
@ -581,7 +583,7 @@ class MainWindow(QMainWindow):
|
||||
self.sizeSlider.setFixedWidth(150)
|
||||
self.sizeSlider.setToolTip(f"{self.card_width} px")
|
||||
self.sizeSlider.setStyleSheet(self.theme.SLIDER_SIZE_STYLE)
|
||||
self.sizeSlider.valueChanged.connect(self.on_slider_value_changed)
|
||||
self.sizeSlider.sliderReleased.connect(self.on_slider_released)
|
||||
sliderLayout.addWidget(self.sizeSlider)
|
||||
layout.addLayout(sliderLayout)
|
||||
|
||||
|
@ -14,7 +14,7 @@ class SystemOverlay(QDialog):
|
||||
def __init__(self, parent, theme):
|
||||
super().__init__(parent)
|
||||
self.theme = theme
|
||||
self.setWindowTitle(_("System Overlay"))
|
||||
self.setWindowTitle("System Overlay")
|
||||
self.setModal(True)
|
||||
self.setFixedSize(400, 300)
|
||||
self.theme_manager = ThemeManager()
|
||||
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.4 MiB |
Before Width: | Height: | Size: 621 KiB After Width: | Height: | Size: 562 KiB |
After Width: | Height: | Size: 445 KiB |
After Width: | Height: | Size: 1.4 MiB |
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 106 KiB |
BIN
portprotonqt/themes/standart/images/screenshots/Оверлей.png
Normal file
After Width: | Height: | Size: 1.1 MiB |