12 Commits

Author SHA1 Message Date
3d2d5a6243 chore(input_manager): clean mapping of actions to evdev button codes
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-06 13:36:16 +05:00
565dc49f36 fix(input_manager): prevent game launch when AddGameDialog is open
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-06 13:23:55 +05:00
c460737bed fix(input_manager): Prioritize tab switching over game card navigation on left arrow key press
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-06 13:09:52 +05:00
636ab73580 chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-06 11:39:25 +05:00
93954abf0d feat(input_manager): directional D-pad navigation for game cards
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-06 11:35:26 +05:00
9ab0adf676 fix(input_manager): disable D-pad tab switching, restrict to LB/RB buttons
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-06 10:29:13 +05:00
c08e4fb38d feat: disable focus for addGameButton
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-05 22:00:13 +05:00
c25589ac96 feat: add focus styling to ACTION_BUTTON_STYLE
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-05 21:50:11 +05:00
60d6f0734d chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-05 19:47:00 +05:00
57d499fab2 feat(input_manager): close AddGameDialog with B or Esc
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-05 19:43:47 +05:00
bc91b03843 fix(main_window): prevent multiple AddGameDialog openings on gamepad
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-05 19:37:57 +05:00
aabf8cb30f fix(input_manager): prevent gamepad input handling during game execution
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-05 19:33:08 +05:00
4 changed files with 360 additions and 167 deletions

View File

@@ -15,6 +15,7 @@
- Стили в AddGameDialog - Стили в AddGameDialog
- Переключение полноэкранного режима через F11 - Переключение полноэкранного режима через F11
- Выбор QCheckBox через Enter или кнопку A геймпада - Выбор QCheckBox через Enter или кнопку A геймпада
- Закрытие диалога добавления игры через ESC или кнопку B геймпада
- Закрытие окна приложения по комбинации клавиш Ctrl+Q - Закрытие окна приложения по комбинации клавиш Ctrl+Q
- Сохранение и восстановление размера при рестарте - Сохранение и восстановление размера при рестарте
- Переключатель полноэкранного режима приложения - Переключатель полноэкранного режима приложения
@@ -36,6 +37,9 @@
- Бейджи с карточек так же теперь дублируются и на странице с деталями, а не только в библиотеке - Бейджи с карточек так же теперь дублируются и на странице с деталями, а не только в библиотеке
- Установка ширины бейджа в две трети ширины карточки - Установка ширины бейджа в две трети ширины карточки
- Бейджи источников (`Steam`, `EGS`, `PortProton`) теперь отображаются только при активном фильтре `all` или `favorites` - Бейджи источников (`Steam`, `EGS`, `PortProton`) теперь отображаются только при активном фильтре `all` или `favorites`
- Карточки теперь фокусируются в направлении движения стрелок или D-pad, например если нажать D-pad вниз то перейдёшь на карточку со следующей колонки, а не по порядку
- D-pad больше не переключает вкладки только RB и LB
- Кнопка добавления игры больше не фокусируется
### Fixed ### Fixed
- Обработка несуществующей темы с возвратом к “standart” - Обработка несуществующей темы с возвратом к “standart”
@@ -47,6 +51,8 @@
- traceback при загрузке placeholder при отсутствии обложек - traceback при загрузке placeholder при отсутствии обложек
- Утечки памяти при загрузке обложек - Утечки памяти при загрузке обложек
- Ошибки при подключении геймпада из-за работы в разных потоках - Ошибки при подключении геймпада из-за работы в разных потоках
- Множественное открытие диалога добавления игры на геймпаде
- Перехват событий геймпада во время работы игры
--- ---

View File

@@ -3,7 +3,7 @@ import threading
from typing import Protocol, cast from typing import Protocol, cast
from evdev import InputDevice, ecodes, list_devices from evdev import InputDevice, ecodes, list_devices
import pyudev import pyudev
from PySide6.QtWidgets import QWidget, QStackedWidget, QApplication, QScrollArea, QLineEdit from PySide6.QtWidgets import QWidget, QStackedWidget, QApplication, QScrollArea, QLineEdit, QDialog
from PySide6.QtCore import Qt, QObject, QEvent, QPoint, Signal, Slot from PySide6.QtCore import Qt, QObject, QEvent, QPoint, Signal, Slot
from PySide6.QtGui import QKeyEvent from PySide6.QtGui import QKeyEvent
from portprotonqt.logger import get_logger from portprotonqt.logger import get_logger
@@ -30,14 +30,15 @@ class MainWindowProtocol(Protocol):
gamesListWidget: QWidget gamesListWidget: QWidget
currentDetailPage: QWidget | None currentDetailPage: QWidget | None
current_exec_line: str | None current_exec_line: str | None
current_add_game_dialog: QDialog | None
# Mapping of actions to evdev button codes, includes PlayStation, Xbox, and Switch controllers # Mapping of actions to evdev button codes, includes PlayStation, Xbox, and Switch controllers
BUTTONS = { BUTTONS = {
'confirm': {ecodes.BTN_A}, 'confirm': {ecodes.BTN_A},
'back': {ecodes.BTN_B}, 'back': {ecodes.BTN_B},
'add_game': {ecodes.BTN_Y}, 'add_game': {ecodes.BTN_Y},
'prev_tab': {ecodes.BTN_TL, ecodes.BTN_TL2}, 'prev_tab': {ecodes.BTN_TL, ecodes.BTN_TRIGGER_HAPPY7},
'next_tab': {ecodes.BTN_TR, ecodes.BTN_TR2}, 'next_tab': {ecodes.BTN_TR, ecodes.BTN_TRIGGER_HAPPY5},
'confirm_stick': {ecodes.BTN_THUMBL, ecodes.BTN_THUMBR}, 'confirm_stick': {ecodes.BTN_THUMBL, ecodes.BTN_THUMBR},
'context_menu': {ecodes.BTN_START}, 'context_menu': {ecodes.BTN_START},
'menu': {ecodes.BTN_SELECT, ecodes.BTN_MODE}, 'menu': {ecodes.BTN_SELECT, ecodes.BTN_MODE},
@@ -67,6 +68,7 @@ class InputManager(QObject):
# Ensure attributes exist on main_window # 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.axis_deadzone = axis_deadzone self.axis_deadzone = axis_deadzone
self.initial_axis_move_delay = initial_axis_move_delay self.initial_axis_move_delay = initial_axis_move_delay
@@ -115,6 +117,225 @@ class InputManager(QObject):
except Exception as e: except Exception as e:
logger.error(f"Error in handle_fullscreen_slot: {e}", exc_info=True) logger.error(f"Error in handle_fullscreen_slot: {e}", exc_info=True)
@Slot(int)
def handle_button_slot(self, button_code: int) -> None:
try:
# Игнорировать события геймпада, если игра запущена
if getattr(self._parent, '_gameLaunched', False):
return
app = QApplication.instance()
if not app:
return
active = QApplication.activeWindow()
focused = QApplication.focusWidget()
# Закрытие AddGameDialog на кнопку B
if button_code in BUTTONS['back'] and isinstance(active, QDialog):
active.reject() # Закрываем диалог
return
# FullscreenDialog
if isinstance(active, FullscreenDialog):
if button_code in BUTTONS['prev_tab']:
active.show_prev()
elif button_code in BUTTONS['next_tab']:
active.show_next()
elif button_code in BUTTONS['back']:
active.close()
return
# Context menu for GameCard
if isinstance(focused, GameCard):
if button_code in BUTTONS['context_menu']:
pos = QPoint(focused.width() // 2, focused.height() // 2)
focused._show_context_menu(pos)
return
# Game launch on detail page
if (button_code in BUTTONS['confirm'] or button_code in BUTTONS['confirm_stick']) and self._parent.currentDetailPage is not None and self._parent.current_add_game_dialog is None:
if self._parent.current_exec_line:
self._parent.toggleGame(self._parent.current_exec_line, None)
return
# Standard navigation
if button_code in BUTTONS['confirm'] or button_code in BUTTONS['confirm_stick']:
self._parent.activateFocusedWidget()
elif button_code in BUTTONS['back'] or button_code in BUTTONS['menu']:
self._parent.goBackDetailPage(getattr(self._parent, 'currentDetailPage', None))
elif button_code in BUTTONS['add_game']:
self._parent.openAddGameDialog()
elif button_code in BUTTONS['prev_tab']:
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['next_tab']:
idx = (self._parent.stackedWidget.currentIndex() + 1) % len(self._parent.tabButtons)
self._parent.switchTab(idx)
self._parent.tabButtons[idx].setFocus(Qt.FocusReason.OtherFocusReason)
except Exception as e:
logger.error(f"Error in handle_button_slot: {e}", exc_info=True)
@Slot(int, int, float)
def handle_dpad_slot(self, code: int, value: int, current_time: float) -> None:
try:
# Игнорировать события геймпада, если игра запущена
if getattr(self._parent, '_gameLaunched', False):
return
app = QApplication.instance()
if not app:
return
active = QApplication.activeWindow()
# Fullscreen horizontal navigation
if isinstance(active, FullscreenDialog) and code == ecodes.ABS_HAT0X:
if value < 0:
active.show_prev()
elif value > 0:
active.show_next()
return
# Handle repeated D-pad movement
if value != 0:
if not self.axis_moving:
self.axis_moving = True
elif (current_time - self.last_move_time) < self.current_axis_delay:
return
self.last_move_time = current_time
self.current_axis_delay = self.repeat_axis_move_delay
else:
self.axis_moving = False
self.current_axis_delay = self.initial_axis_move_delay
return
# Library tab navigation (index 0)
if self._parent.stackedWidget.currentIndex() == 0 and code in (ecodes.ABS_HAT0X, ecodes.ABS_HAT0Y):
focused = QApplication.focusWidget()
game_cards = self._parent.gamesListWidget.findChildren(GameCard)
if not game_cards:
return
scroll_area = self._parent.gamesListWidget.parentWidget()
while scroll_area and not isinstance(scroll_area, QScrollArea):
scroll_area = scroll_area.parentWidget()
# If no focused widget or not a GameCard, focus the first card
if not isinstance(focused, GameCard) or focused not in game_cards:
game_cards[0].setFocus()
if scroll_area:
scroll_area.ensureWidgetVisible(game_cards[0], 50, 50)
return
# 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 code == ecodes.ABS_HAT0X and value != 0: # Left/Right
if value < 0: # 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)
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)
elif value > 0: # 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)
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)
elif code == ecodes.ABS_HAT0Y and value != 0: # Up/Down
if value > 0: # Down
next_row_idx = current_row_idx + 1
if next_row_idx < len(sorted_rows):
next_row = sorted_rows[next_row_idx][1]
# Find card in same column or closest
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)
elif value < 0: # Up
next_row_idx = current_row_idx - 1
if next_row_idx >= 0:
next_row = sorted_rows[next_row_idx][1]
# Find card in same column or closest
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)
elif current_row_idx == 0:
self._parent.tabButtons[0].setFocus(Qt.FocusReason.OtherFocusReason)
# Vertical navigation in other tabs
elif code == ecodes.ABS_HAT0Y and value != 0:
focused = QApplication.focusWidget()
page = self._parent.stackedWidget.currentWidget()
if value > 0: # 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
elif focused:
focused.focusNextChild()
return
elif value < 0 and focused: # Up
focused.focusPreviousChild()
return
except Exception as e:
logger.error(f"Error in handle_dpad_slot: {e}", exc_info=True)
def eventFilter(self, obj: QObject, event: QEvent) -> bool: def eventFilter(self, obj: QObject, event: QEvent) -> bool:
app = QApplication.instance() app = QApplication.instance()
if not app: if not app:
@@ -134,6 +355,11 @@ class InputManager(QObject):
app.quit() app.quit()
return True 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 # Skip navigation keys if a popup is open
if popup: if popup:
return False return False
@@ -164,59 +390,125 @@ class InputManager(QObject):
focused._show_context_menu(pos) focused._show_context_menu(pos)
return True return True
# Navigation in Library tab # 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: if self._parent.stackedWidget.currentIndex() == 0:
game_cards = self._parent.gamesListWidget.findChildren(GameCard) game_cards = self._parent.gamesListWidget.findChildren(GameCard)
scroll_area = self._parent.gamesListWidget.parentWidget() scroll_area = self._parent.gamesListWidget.parentWidget()
while scroll_area and not isinstance(scroll_area, QScrollArea): while scroll_area and not isinstance(scroll_area, QScrollArea):
scroll_area = scroll_area.parentWidget() scroll_area = scroll_area.parentWidget()
if isinstance(focused, GameCard): if key in (Qt.Key.Key_Left, Qt.Key.Key_Right, Qt.Key.Key_Up, Qt.Key.Key_Down):
current_index = game_cards.index(focused) if focused in game_cards else -1 if not game_cards:
if key == Qt.Key.Key_Down: return True
if current_index >= 0 and current_index + 1 < len(game_cards):
next_card = game_cards[current_index + 1] # If no focused widget or not a GameCard, focus the first card
if not isinstance(focused, GameCard) or focused not in game_cards:
game_cards[0].setFocus()
if scroll_area:
scroll_area.ensureWidgetVisible(game_cards[0], 50, 50)
return 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
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() next_card.setFocus()
if scroll_area: if scroll_area:
scroll_area.ensureWidgetVisible(next_card, 50, 50) scroll_area.ensureWidgetVisible(next_card, 50, 50)
return True return True
elif key == Qt.Key.Key_Up: elif key == Qt.Key.Key_Up:
if current_index > 0: next_row_idx = current_row_idx - 1
prev_card = game_cards[current_index - 1] if next_row_idx >= 0:
prev_card.setFocus() next_row = sorted_rows[next_row_idx][1]
if scroll_area: target_x = focused.pos().x()
scroll_area.ensureWidgetVisible(prev_card, 50, 50) next_card = min(
return True next_row,
elif current_index == 0: key=lambda c: abs(c.pos().x() - target_x),
self._parent.tabButtons[0].setFocus() default=None
return True )
elif key == Qt.Key.Key_Left: if next_card:
if current_index > 0:
prev_card = game_cards[current_index - 1]
prev_card.setFocus()
if scroll_area:
scroll_area.ensureWidgetVisible(prev_card, 50, 50)
return True
elif key == Qt.Key.Key_Right:
if current_index >= 0 and current_index + 1 < len(game_cards):
next_card = game_cards[current_index + 1]
next_card.setFocus() next_card.setFocus()
if scroll_area: if scroll_area:
scroll_area.ensureWidgetVisible(next_card, 50, 50) scroll_area.ensureWidgetVisible(next_card, 50, 50)
return True return True
elif current_row_idx == 0:
# Tab switching with Left/Right keys self._parent.tabButtons[0].setFocus()
idx = self._parent.stackedWidget.currentIndex()
total = len(self._parent.tabButtons)
if key == Qt.Key.Key_Left and not isinstance(focused, GameCard):
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):
new = (idx + 1) % total
self._parent.switchTab(new)
self._parent.tabButtons[new].setFocus()
return True return True
# Navigate down into tab content # Navigate down into tab content
@@ -228,11 +520,6 @@ class InputManager(QObject):
if focusables: if focusables:
focusables[0].setFocus() focusables[0].setFocus()
return True return True
else:
if focused is not None:
focused.focusNextChild()
return True
# Navigate up through tab content # Navigate up through tab content
if key == Qt.Key.Key_Up: if key == Qt.Key.Key_Up:
if isinstance(focused, NavLabel): if isinstance(focused, NavLabel):
@@ -354,124 +641,6 @@ class InputManager(QObject):
pass pass
self.gamepad = None self.gamepad = None
@Slot(int)
def handle_button_slot(self, button_code: int) -> None:
try:
app = QApplication.instance()
if not app:
return
active = QApplication.activeWindow()
focused = QApplication.focusWidget()
# FullscreenDialog
if isinstance(active, FullscreenDialog):
if button_code in BUTTONS['prev_tab']:
active.show_prev()
elif button_code in BUTTONS['next_tab']:
active.show_next()
elif button_code in BUTTONS['back']:
active.close()
return
# Context menu for GameCard
if isinstance(focused, GameCard):
if button_code in BUTTONS['context_menu']:
pos = QPoint(focused.width() // 2, focused.height() // 2)
focused._show_context_menu(pos)
return
# Game launch on detail page
if (button_code in BUTTONS['confirm'] or button_code in BUTTONS['confirm_stick']) and self._parent.currentDetailPage is not None:
if self._parent.current_exec_line:
self._parent.toggleGame(self._parent.current_exec_line, None)
return
# Standard navigation
if button_code in BUTTONS['confirm'] or button_code in BUTTONS['confirm_stick']:
self._parent.activateFocusedWidget()
elif button_code in BUTTONS['back'] or button_code in BUTTONS['menu']:
self._parent.goBackDetailPage(getattr(self._parent, 'currentDetailPage', None))
elif button_code in BUTTONS['add_game']:
self._parent.openAddGameDialog()
elif button_code in BUTTONS['prev_tab']:
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['next_tab']:
idx = (self._parent.stackedWidget.currentIndex() + 1) % len(self._parent.tabButtons)
self._parent.switchTab(idx)
self._parent.tabButtons[idx].setFocus(Qt.FocusReason.OtherFocusReason)
except Exception as e:
logger.error(f"Error in handle_button_slot: {e}", exc_info=True)
@Slot(int, int, float)
def handle_dpad_slot(self, code: int, value: int, current_time: float) -> None:
try:
app = QApplication.instance()
if not app:
return
active = QApplication.activeWindow()
# Fullscreen horizontal
if isinstance(active, FullscreenDialog) and code == ecodes.ABS_HAT0X:
if value < 0:
active.show_prev()
elif value > 0:
active.show_next()
return
# Vertical navigation (DPAD up/down)
if code == ecodes.ABS_HAT0Y:
if value == 0:
return
focused = QApplication.focusWidget()
page = self._parent.stackedWidget.currentWidget()
if value > 0:
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
elif focused:
focused.focusNextChild()
return
elif value < 0 and focused:
focused.focusPreviousChild()
return
# Horizontal wrap navigation repeat logic
if code != ecodes.ABS_HAT0X:
return
if value == 0:
self.axis_moving = False
self.current_axis_delay = self.initial_axis_move_delay
return
if not self.axis_moving:
self.trigger_dpad_movement(code, value)
self.last_move_time = current_time
self.axis_moving = True
elif current_time - self.last_move_time >= self.current_axis_delay:
self.trigger_dpad_movement(code, value)
self.last_move_time = current_time
self.current_axis_delay = self.repeat_axis_move_delay
except Exception as e:
logger.error(f"Error in handle_dpad_slot: {e}", exc_info=True)
def trigger_dpad_movement(self, code: int, value: int) -> None:
try:
if code != ecodes.ABS_HAT0X:
return
idx = self._parent.stackedWidget.currentIndex()
if value < 0:
new = (idx - 1) % len(self._parent.tabButtons)
else:
new = (idx + 1) % len(self._parent.tabButtons)
self._parent.switchTab(new)
self._parent.tabButtons[new].setFocus(Qt.FocusReason.OtherFocusReason)
except Exception as e:
logger.error(f"Error in trigger_dpad_movement: {e}", exc_info=True)
def cleanup(self) -> None: def cleanup(self) -> None:
try: try:
self.running = False self.running = False

View File

@@ -60,6 +60,7 @@ class MainWindow(QMainWindow):
self.games_load_timer.setSingleShot(True) self.games_load_timer.setSingleShot(True)
self.games_load_timer.timeout.connect(self.finalize_game_loading) self.games_load_timer.timeout.connect(self.finalize_game_loading)
self.games_loaded.connect(self.on_games_loaded) self.games_loaded.connect(self.on_games_loaded)
self.current_add_game_dialog = None
# Добавляем таймер для дебаунсинга сохранения настроек # Добавляем таймер для дебаунсинга сохранения настроек
self.settingsDebounceTimer = QTimer(self) self.settingsDebounceTimer = QTimer(self)
@@ -509,6 +510,7 @@ class MainWindow(QMainWindow):
self.addGameButton = AutoSizeButton(_("Add Game"), icon=self.theme_manager.get_icon("addgame")) self.addGameButton = AutoSizeButton(_("Add Game"), icon=self.theme_manager.get_icon("addgame"))
self.addGameButton.setStyleSheet(self.theme.ADDGAME_BACK_BUTTON_STYLE) self.addGameButton.setStyleSheet(self.theme.ADDGAME_BACK_BUTTON_STYLE)
self.addGameButton.setFocusPolicy(Qt.FocusPolicy.NoFocus)
self.addGameButton.clicked.connect(self.openAddGameDialog) self.addGameButton.clicked.connect(self.openAddGameDialog)
layout.addWidget(self.addGameButton, alignment=Qt.AlignmentFlag.AlignRight) layout.addWidget(self.addGameButton, alignment=Qt.AlignmentFlag.AlignRight)
@@ -730,7 +732,14 @@ class MainWindow(QMainWindow):
def openAddGameDialog(self, exe_path=None): def openAddGameDialog(self, exe_path=None):
"""Открывает диалоговое окно 'Add Game' с текущей темой.""" """Открывает диалоговое окно 'Add Game' с текущей темой."""
# Проверяем, открыт ли уже диалог
if self.current_add_game_dialog is not None and self.current_add_game_dialog.isVisible():
self.current_add_game_dialog.activateWindow() # Активируем существующий диалог
self.current_add_game_dialog.raise_() # Поднимаем окно
return
dialog = AddGameDialog(self, self.theme) dialog = AddGameDialog(self, self.theme)
self.current_add_game_dialog = dialog # Сохраняем ссылку на диалог
# Предзаполняем путь к .exe при drag-and-drop # Предзаполняем путь к .exe при drag-and-drop
if exe_path: if exe_path:
@@ -738,6 +747,12 @@ class MainWindow(QMainWindow):
dialog.nameEdit.setText(os.path.splitext(os.path.basename(exe_path))[0]) dialog.nameEdit.setText(os.path.splitext(os.path.basename(exe_path))[0])
dialog.updatePreview() dialog.updatePreview()
# Обработчик закрытия диалога
def on_dialog_finished():
self.current_add_game_dialog = None # Сбрасываем ссылку при закрытии
dialog.finished.connect(on_dialog_finished)
if dialog.exec() == QDialog.DialogCode.Accepted: if dialog.exec() == QDialog.DialogCode.Accepted:
name = dialog.nameEdit.text().strip() name = dialog.nameEdit.text().strip()
exe_path = dialog.exeEdit.text().strip() exe_path = dialog.exeEdit.text().strip()
@@ -774,7 +789,6 @@ class MainWindow(QMainWindow):
self.games = self.loadGames() self.games = self.loadGames()
self.updateGameGrid() self.updateGameGrid()
def createAutoInstallTab(self): def createAutoInstallTab(self):
"""Вкладка 'Auto Install'.""" """Вкладка 'Auto Install'."""
self.autoInstallWidget = QWidget() self.autoInstallWidget = QWidget()

View File

@@ -200,6 +200,10 @@ ACTION_BUTTON_STYLE = """
QPushButton:pressed { QPushButton:pressed {
background: #282a33; background: #282a33;
} }
QPushButton:focus {
border: 2px solid #409EFF;
background-color: #404554;
}
""" """
# ТЕКСТОВЫЕ СТИЛИ: ЗАГОЛОВКИ И ОСНОВНОЙ КОНТЕНТ # ТЕКСТОВЫЕ СТИЛИ: ЗАГОЛОВКИ И ОСНОВНОЙ КОНТЕНТ