5 Commits

Author SHA1 Message Date
802d5a2ba1 chore(metainfo): sync screenshots with standart theme
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-14 00:07:42 +05:00
1d47caf4aa chore(readme): update todo
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-14 00:01:21 +05:00
502664438c chore: update screenshots in standart theme
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-14 00:00:02 +05:00
f4e155dade chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-13 23:49:25 +05:00
74400d1389 feat: align keyboard arrow key navigation with D-pad logic
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-13 23:34:11 +05:00
10 changed files with 140 additions and 237 deletions

View File

@@ -10,24 +10,24 @@
- Бейдж PortProton - Бейдж PortProton
- Зависимость от `xdg-utils` - Зависимость от `xdg-utils`
- Интеграция статуса WeAntiCheatYet в карточку - Интеграция статуса WeAntiCheatYet в карточку
- Стили в AddGameDialog
- Переключение полноэкранного режима через F11 или кнопку Select на геймпаде - Переключение полноэкранного режима через F11 или кнопку Select на геймпаде
- Выбор QCheckBox через Enter или кнопку A на геймпаде - Выбор состояния `QCheckBox` через Enter или кнопку A на геймпаде
- Закрытие диалога добавления игры через ESC или кнопку B на геймпаде - Закрытие диалога добавления игры через ESC или кнопку B на геймпаде
- Закрытие окна приложения по комбинации клавиш Ctrl+Q - Закрытие окна приложения по комбинации клавиш Ctrl+Q
- Сохранение и восстановление размера окна при перезапуске - Сохранение и восстановление размера окна при перезапуске
- Переключатель полноэкранного режима приложения - Переключатель полноэкранного режима приложения
- Пункт в контекстном меню «Открыть папку игры» - Пункт в контекстном меню «Открыть папку игры»
- Пункты в контекстном меню «Добавить в Steam» и «Удалить из Steam» - Пункты в контекстном меню «Добавить в Steam» и «Удалить из Steam»
- Пункты в контекстном меню «Добавить в Избранное» и «Удалить из Избранного» для переключения статуса избранного через геймпад - Пункты в контекстном меню «Добавить в Избранное» и «Удалить из Избранного»
- Метод сортировки «Сначала избранное» - Метод сортировки «Сначала избранное»
- Настройка автоматического перехода в полноэкранный режим при подключении геймпада (по умолчанию отключена) - Настройка автоматического перехода в полноэкранный режим при подключении геймпада (по умолчанию отключена)
- Обработчики для QMenu и QComboBox при управлении геймпадом - Поддержка управления геймпадом в `QMenu` и `QComboBox`
- Аргумент `--fullscreen` для запуска приложения в полноэкранном режиме - Аргумент `--fullscreen` для запуска приложения в полноэкранном режиме
- Оверлей на кнопку Insert или кнопку Xbox/PS на геймпаде для закрытия приложения, выключения, перезагрузки и перехода в спящий режим или между сессиями - Оверлей на кнопку Insert или кнопку Xbox/PS на геймпаде для закрытия приложения, выключения, перезагрузки и перехода в спящий режим или переключения между сессиями
- [Gamescope сессия](https://git.linux-gaming.ru/Boria138/gamescope-session-portprotonqt) - [Gamescope сессия](https://git.linux-gaming.ru/Boria138/gamescope-session-portprotonqt)
- Мапинги управления для Dualshock 4 и DualSense - Пресеты управления для DualShock 4 и DualSense
- Настройка тактильной обратной связи на геймпаде при запуске игры (по умолчанию отключена) - Настройка тактильной отдачи на геймпаде при запуске игры (по умолчанию выключена)
- Переводы пунктов настроек
### Changed ### Changed
- Обновлены все иконки - Обновлены все иконки
@@ -36,29 +36,29 @@
- Логика контекстного меню вынесена в `ContextMenuManager` - Логика контекстного меню вынесена в `ContextMenuManager`
- Бейдж Steam теперь открывает Steam Community - Бейдж Steam теперь открывает Steam Community
- Изменена лицензия с MIT на GPL-3.0 для совместимости с кодом от legendary - Изменена лицензия с MIT на GPL-3.0 для совместимости с кодом от legendary
- Оптимизирована генерация карточек для предотвращения задержек при поиске и изменении размера окна - Оптимизирована генерация карточек для плавной работы при поиске и изменении размера окна
- Бейджи с карточек теперь отображаются также на странице с деталями, а не только в библиотеке - Бейджи с карточек теперь отображаются также на странице с деталями, а не только в библиотеке
- Установлена ширина бейджа в две трети ширины карточки - Установлена ширина бейджа в две трети ширины карточки
- Бейджи источников (`Steam`, `EGS`, `PortProton`) теперь отображаются только при активном фильтре `all` или `favorites` - Бейджи источников (`Steam`, `EGS`, `PortProton`) теперь отображаются только при активном фильтре `all` или `favorites`
- Карточки теперь фокусируются в направлении движения стрелок или D-pad: например, при нажатии D-pad вниз фокус переходит на карточку в следующей колонке, а не по порядку - Карточки теперь фокусируются в направлении движения стрелок или D-pad:
- Теперь D-pad можно зажимать для переключения карточек - Поддерживается удержание D-pad для непрерывного переключения карточек
- D-pad больше не переключает вкладки, только RB и LB - Объединён обработчик управления стрелками клавиатуры и D-pad для консистентности
- D-pad больше не переключает вкладки (только кнопки RB/LB)
- Кнопка добавления игры больше не фокусируется - Кнопка добавления игры больше не фокусируется
- Диалог добавления игры теперь открывается только в библиотеке - Диалог добавления игры теперь открывается только в библиотеке
- Удалены все упоминания PortProtonQT из кода и заменены на PortProtonQt - Удалены все упоминания PortProtonQT из кода и заменены на PortProtonQt
### Fixed ### Fixed
- Обработка несуществующей темы с возвратом к «standard» - Возврат к теме «standard» при выборе несуществующей темы
- Открытие контекстного меню - Корректное открытие контекстного меню
- Запуск при отсутствии exiftool - Запуск приложения при отсутствии `exiftool`
- Переводы пунктов настроек - Предотвращено бесконечное обращение к `get_portproton_location`
- Бесконечное обращение к `get_portproton_location` - Обновлены ссылки на документацию в README
- Ссылки на документацию в README - Устранён traceback при отсутствии обложек (placeholder)
- Traceback при загрузке placeholder при отсутствии обложек - Устранены утечки памяти при загрузке обложек
- Утечки памяти при загрузке обложек - Исправлены ошибки при подключении геймпада
- Ошибки при подключении геймпада из-за работы в разных потоках - Предотвращено многократное открытие диалога добавления игры через геймпад
- Многократное открытие диалога добавления игры при использовании геймпада - Корректная обработка событий геймпада во время игры
- Перехват событий геймпада во время работы игры
--- ---

View File

@@ -65,7 +65,7 @@
- [X] Добавить виброотдачу на геймпаде при запуске игры - [X] Добавить виброотдачу на геймпаде при запуске игры
- [ ] Исправить некорректную работу слайдера увеличения размера карточек([Последствия регрессии после этого коммита](https://github.com/Boria138/PortProtonQt/commit/aebdd60b5537280f06a922ff80469cd4ab27bc63) - [ ] Исправить некорректную работу слайдера увеличения размера карточек([Последствия регрессии после этого коммита](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) ### Установка (devel)

View File

@@ -49,6 +49,16 @@
<caption>Settings</caption> <caption>Settings</caption>
<caption xml:lang="ru">Настройки</caption> <caption xml:lang="ru">Настройки</caption>
</screenshot> </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> </screenshots>
<keywords> <keywords>
<keyword translate="no">wine</keyword> <keyword translate="no">wine</keyword>

View File

@@ -523,240 +523,133 @@ class InputManager(QObject):
if not app: if not app:
return super().eventFilter(obj, event) return super().eventFilter(obj, event)
# Handle only key press events # Handle key press and release events
if not (isinstance(event, QKeyEvent) and event.type() == QEvent.Type.KeyPress): if not isinstance(event, QKeyEvent):
return super().eventFilter(obj, event) return super().eventFilter(obj, event)
key = event.key() key = event.key()
modifiers = event.modifiers() modifiers = event.modifiers()
focused = QApplication.focusWidget() focused = QApplication.focusWidget()
popup = QApplication.activePopupWidget() 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() 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 # Handle key press events
if self._parent.currentDetailPage and key in (Qt.Key.Key_Return, Qt.Key.Key_Enter): if event.type() == QEvent.Type.KeyPress:
if self._parent.current_exec_line: # Open system overlay with Insert
self._parent.toggleGame(self._parent.current_exec_line, None) if key == Qt.Key.Key_Insert:
return True if not popup and not isinstance(active_win, QDialog):
self._parent.openSystemOverlay()
# 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:
return True return True
# If no focused widget or not a GameCard, focus the first card # Close application with Ctrl+Q
if not isinstance(focused, GameCard) or focused not in game_cards: if key == Qt.Key.Key_Q and modifiers & Qt.KeyboardModifier.ControlModifier:
game_cards[0].setFocus() app.quit()
if scroll_area: return True
scroll_area.ensureWidgetVisible(game_cards[0], 50, 50)
# 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 return True
# Group cards by rows based on y-coordinate # Map arrow keys to D-pad press events for other contexts
rows = {} if key in (Qt.Key.Key_Up, Qt.Key.Key_Down, Qt.Key.Key_Left, Qt.Key.Key_Right):
for card in game_cards: now = time.time()
y = card.pos().y() dpad_code = None
if y not in rows: dpad_value = 0
rows[y] = [] if key == Qt.Key.Key_Up:
rows[y].append(card) dpad_code = ecodes.ABS_HAT0Y
# Sort cards in each row by x-coordinate dpad_value = -1
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
elif key == Qt.Key.Key_Down: elif key == Qt.Key.Key_Down:
next_row_idx = current_row_idx + 1 dpad_code = ecodes.ABS_HAT0Y
if next_row_idx < len(sorted_rows): dpad_value = 1
next_row = sorted_rows[next_row_idx][1] elif key == Qt.Key.Key_Left:
target_x = focused.pos().x() dpad_code = ecodes.ABS_HAT0X
next_card = min( dpad_value = -1
next_row, elif key == Qt.Key.Key_Right:
key=lambda c: abs(c.pos().x() - target_x), dpad_code = ecodes.ABS_HAT0X
default=None dpad_value = 1
)
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
# Navigate down into tab content if dpad_code is not None:
if key == Qt.Key.Key_Down: self.dpad_moved.emit(dpad_code, dpad_value, now)
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()
return True 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 return True
# Navigate up through tab content elif key in (Qt.Key.Key_Escape, Qt.Key.Key_Backspace):
if key == Qt.Key.Key_Up: if isinstance(focused, QLineEdit):
if isinstance(focused, NavLabel): return False
self._parent.goBackDetailPage(self._parent.currentDetailPage)
return True return True
if focused is not None: elif key == Qt.Key.Key_E:
focused.focusPreviousChild() 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 return True
# General actions: Activate, Back, Add # Handle key release events for arrow keys
if key in (Qt.Key.Key_Return, Qt.Key.Key_Enter): elif event.type() == QEvent.Type.KeyRelease:
self._parent.activateFocusedWidget() if key in (Qt.Key.Key_Up, Qt.Key.Key_Down, Qt.Key.Key_Left, Qt.Key.Key_Right):
return True now = time.time()
elif key in (Qt.Key.Key_Escape, Qt.Key.Key_Backspace): dpad_code = None
if isinstance(focused, QLineEdit): if key in (Qt.Key.Key_Up, Qt.Key.Key_Down):
return False dpad_code = ecodes.ABS_HAT0Y
self._parent.goBackDetailPage(self._parent.currentDetailPage) elif key in (Qt.Key.Key_Left, Qt.Key.Key_Right):
return True dpad_code = ecodes.ABS_HAT0X
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 dpad_code is not None:
if key == Qt.Key.Key_F11: # Emit release event with value 0 to stop continuous movement
self.toggle_fullscreen.emit(not self._is_fullscreen) self.dpad_moved.emit(dpad_code, 0, now)
return True return True
return super().eventFilter(obj, event) return super().eventFilter(obj, event)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 621 KiB

After

Width:  |  Height:  |  Size: 562 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 445 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB