fix(input_manager): fix keyboard and dpad navigation
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
This commit is contained in:
@@ -556,174 +556,95 @@ class InputManager(QObject):
|
|||||||
|
|
||||||
@Slot(int, int, float)
|
@Slot(int, int, float)
|
||||||
def handle_dpad_slot(self, code: int, value: int, current_time: float) -> None:
|
def handle_dpad_slot(self, code: int, value: int, current_time: float) -> None:
|
||||||
if not self._gamepad_handling_enabled:
|
|
||||||
return
|
|
||||||
try:
|
try:
|
||||||
# Ignore gamepad events if a game is launched
|
if not self._gamepad_handling_enabled:
|
||||||
|
return
|
||||||
if getattr(self._parent, '_gameLaunched', False):
|
if getattr(self._parent, '_gameLaunched', False):
|
||||||
return
|
return
|
||||||
|
|
||||||
app = QApplication.instance()
|
|
||||||
if not app:
|
|
||||||
return
|
|
||||||
active = QApplication.activeWindow()
|
|
||||||
focused = QApplication.focusWidget()
|
|
||||||
popup = QApplication.activePopupWidget()
|
popup = QApplication.activePopupWidget()
|
||||||
|
|
||||||
# Update D-pad state
|
|
||||||
if value != 0:
|
|
||||||
self.current_dpad_code = code
|
|
||||||
self.current_dpad_value = value
|
|
||||||
if not self.axis_moving:
|
|
||||||
self.axis_moving = True
|
|
||||||
self.last_move_time = current_time
|
|
||||||
self.current_axis_delay = self.initial_axis_move_delay
|
|
||||||
self.dpad_timer.start(int(self.repeat_axis_move_delay * 1000)) # Start timer (in milliseconds)
|
|
||||||
else:
|
|
||||||
self.current_dpad_code = None
|
|
||||||
self.current_dpad_value = 0
|
|
||||||
self.axis_moving = False
|
|
||||||
self.current_axis_delay = self.initial_axis_move_delay
|
|
||||||
self.dpad_timer.stop() # Stop timer when D-pad is released
|
|
||||||
return
|
|
||||||
|
|
||||||
# Handle SystemOverlay, AddGameDialog, or QMessageBox navigation with D-pad
|
|
||||||
if isinstance(active, QDialog) and code == ecodes.ABS_HAT0X and value != 0:
|
|
||||||
if isinstance(active, QMessageBox): # Specific handling for QMessageBox
|
|
||||||
if not focused or not active.focusWidget():
|
|
||||||
# If no widget is focused, focus the first focusable widget
|
|
||||||
focusables = active.findChildren(QWidget, options=Qt.FindChildOption.FindChildrenRecursively)
|
|
||||||
focusables = [w for w in focusables if w.focusPolicy() & Qt.FocusPolicy.StrongFocus]
|
|
||||||
if focusables:
|
|
||||||
focusables[0].setFocus(Qt.FocusReason.OtherFocusReason)
|
|
||||||
return
|
|
||||||
if value > 0: # Right
|
|
||||||
active.focusNextChild()
|
|
||||||
elif value < 0: # Left
|
|
||||||
active.focusPreviousChild()
|
|
||||||
return
|
|
||||||
elif isinstance(active, QDialog) and code == ecodes.ABS_HAT0Y and value != 0: # Keep up/down for other dialogs
|
|
||||||
if not focused or not active.focusWidget():
|
|
||||||
# If no widget is focused, focus the first focusable widget
|
|
||||||
focusables = active.findChildren(QWidget, options=Qt.FindChildOption.FindChildrenRecursively)
|
|
||||||
focusables = [w for w in focusables if w.focusPolicy() & Qt.FocusPolicy.StrongFocus]
|
|
||||||
if focusables:
|
|
||||||
focusables[0].setFocus(Qt.FocusReason.OtherFocusReason)
|
|
||||||
return
|
|
||||||
if value > 0: # Down
|
|
||||||
active.focusNextChild()
|
|
||||||
elif value < 0: # Up
|
|
||||||
active.focusPreviousChild()
|
|
||||||
return
|
|
||||||
|
|
||||||
# Handle QMenu navigation with D-pad
|
|
||||||
if isinstance(popup, QMenu):
|
if isinstance(popup, QMenu):
|
||||||
if code == ecodes.ABS_HAT0Y and value != 0:
|
if code == ecodes.ABS_HAT0Y and value != 0:
|
||||||
actions = popup.actions()
|
actions = popup.actions()
|
||||||
if actions:
|
if not actions:
|
||||||
current_idx = actions.index(popup.activeAction()) if popup.activeAction() in actions else 0
|
|
||||||
if value < 0: # Up
|
|
||||||
next_idx = (current_idx - 1) % len(actions)
|
|
||||||
popup.setActiveAction(actions[next_idx])
|
|
||||||
elif value > 0: # Down
|
|
||||||
next_idx = (current_idx + 1) % len(actions)
|
|
||||||
popup.setActiveAction(actions[next_idx])
|
|
||||||
return
|
|
||||||
return
|
|
||||||
|
|
||||||
# Handle QListView navigation with D-pad
|
|
||||||
if isinstance(focused, QListView) and code == ecodes.ABS_HAT0Y and value != 0:
|
|
||||||
model = focused.model()
|
|
||||||
current_index = focused.currentIndex()
|
|
||||||
if model and current_index.isValid():
|
|
||||||
row_count = model.rowCount()
|
|
||||||
current_row = current_index.row()
|
|
||||||
if value > 0: # Down
|
|
||||||
next_row = min(current_row + 1, row_count - 1)
|
|
||||||
focused.setCurrentIndex(model.index(next_row, current_index.column()))
|
|
||||||
elif value < 0: # Up
|
|
||||||
prev_row = max(current_row - 1, 0)
|
|
||||||
focused.setCurrentIndex(model.index(prev_row, current_index.column()))
|
|
||||||
focused.scrollTo(focused.currentIndex(), QListView.ScrollHint.PositionAtCenter)
|
|
||||||
return
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# 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 = [card for card in self._parent.gamesListWidget.findChildren(GameCard) if card.isVisible()]
|
|
||||||
if not game_cards:
|
|
||||||
logger.debug("No visible GameCards found")
|
|
||||||
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(Qt.FocusReason.OtherFocusReason)
|
|
||||||
if scroll_area:
|
|
||||||
scroll_area.ensureWidgetVisible(game_cards[0], 50, 50)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Group cards by rows based on normalized y-coordinate
|
|
||||||
rows = {}
|
|
||||||
tolerance = 50 # Increased tolerance for scaling effects
|
|
||||||
for card in game_cards:
|
|
||||||
y = card.pos().y() / card.getScale() # Normalize y-coordinate
|
|
||||||
found = False
|
|
||||||
for row_y in rows:
|
|
||||||
if abs(y - row_y) < tolerance:
|
|
||||||
rows[row_y].append(card)
|
|
||||||
found = True
|
|
||||||
break
|
|
||||||
if not found:
|
|
||||||
rows[y] = [card]
|
|
||||||
# Sort cards in each row by normalized x-coordinate
|
|
||||||
for y in rows:
|
|
||||||
rows[y].sort(key=lambda c: c.pos().x() / c.getScale())
|
|
||||||
# Sort rows by y-coordinate
|
|
||||||
sorted_rows = sorted(rows.items(), key=lambda x: x[0])
|
|
||||||
|
|
||||||
# Find current row with normalized y-coordinate
|
|
||||||
current_y = focused.pos().y() / focused.getScale()
|
|
||||||
current_row_idx = None
|
|
||||||
min_diff = float('inf')
|
|
||||||
for i, (y, _) in enumerate(sorted_rows):
|
|
||||||
diff = abs(y - current_y)
|
|
||||||
if diff < tolerance and diff < min_diff:
|
|
||||||
current_row_idx = i
|
|
||||||
min_diff = diff
|
|
||||||
|
|
||||||
if current_row_idx is None:
|
|
||||||
logger.warning("No row found for current_y: %s, falling back to closest row", current_y)
|
|
||||||
if sorted_rows:
|
|
||||||
current_row_idx = min(range(len(sorted_rows)), key=lambda i: abs(sorted_rows[i][0] - current_y))
|
|
||||||
else:
|
|
||||||
logger.error("No rows available")
|
|
||||||
return
|
return
|
||||||
|
current_action = popup.activeAction()
|
||||||
|
current_idx = actions.index(current_action) if current_action in actions else -1
|
||||||
|
if value > 0: # Down
|
||||||
|
next_idx = (current_idx + 1) % len(actions) if current_idx != -1 else 0
|
||||||
|
popup.setActiveAction(actions[next_idx])
|
||||||
|
elif value < 0: # Up
|
||||||
|
next_idx = (current_idx - 1) % len(actions) if current_idx != -1 else len(actions) - 1
|
||||||
|
popup.setActiveAction(actions[next_idx])
|
||||||
|
return
|
||||||
|
return # Skip other handling if menu is open
|
||||||
|
|
||||||
|
# Update dpad state for repeat navigation
|
||||||
|
if code in (ecodes.ABS_HAT0X, ecodes.ABS_HAT0Y):
|
||||||
|
self.current_dpad_code = code
|
||||||
|
self.current_dpad_value = value
|
||||||
|
if value != 0 and not self.dpad_timer.isActive():
|
||||||
|
self.dpad_timer.start(int(self.initial_axis_move_delay * 1000))
|
||||||
|
elif value == 0:
|
||||||
|
self.dpad_timer.stop()
|
||||||
|
|
||||||
|
focused = QApplication.focusWidget()
|
||||||
|
if self._parent.stackedWidget.currentIndex() == 0 and isinstance(focused, GameCard):
|
||||||
|
scroll_area = None
|
||||||
|
parent = focused
|
||||||
|
while parent and not isinstance(parent, QScrollArea):
|
||||||
|
parent = parent.parentWidget()
|
||||||
|
if isinstance(parent, QScrollArea):
|
||||||
|
scroll_area = parent
|
||||||
|
cards = self._parent.gamesListWidget.findChildren(GameCard, options=Qt.FindChildOption.FindChildrenRecursively)
|
||||||
|
if not cards:
|
||||||
|
return
|
||||||
|
# Group cards by rows with tolerance for y-position
|
||||||
|
rows = {}
|
||||||
|
y_tolerance = 10 # Allow slight variations in y-position
|
||||||
|
for card in cards:
|
||||||
|
y = card.pos().y()
|
||||||
|
matched = False
|
||||||
|
for row_y in rows:
|
||||||
|
if abs(y - row_y) <= y_tolerance:
|
||||||
|
rows[row_y].append(card)
|
||||||
|
matched = True
|
||||||
|
break
|
||||||
|
if not matched:
|
||||||
|
rows[y] = [card]
|
||||||
|
sorted_rows = sorted(rows.items(), key=lambda x: x[0])
|
||||||
|
if not sorted_rows:
|
||||||
|
return
|
||||||
|
current_row_idx = None
|
||||||
|
current_col_idx = None
|
||||||
|
for row_idx, (_y, row_cards) in enumerate(sorted_rows):
|
||||||
|
for idx, card in enumerate(row_cards):
|
||||||
|
if card == focused:
|
||||||
|
current_row_idx = row_idx
|
||||||
|
current_col_idx = idx
|
||||||
|
break
|
||||||
|
if current_row_idx is not None:
|
||||||
|
break
|
||||||
|
# Fallback: if focused card not found, select closest row by y-position
|
||||||
|
if current_row_idx is None:
|
||||||
|
focused_y = focused.pos().y()
|
||||||
|
current_row_idx = min(range(len(sorted_rows)), key=lambda i: abs(sorted_rows[i][0] - focused_y), default=0)
|
||||||
|
current_row = sorted_rows[current_row_idx][1]
|
||||||
|
focused_x = focused.pos().x() + focused.width() / 2
|
||||||
|
current_col_idx = min(range(len(current_row)), key=lambda i: abs((current_row[i].pos().x() + current_row[i].width() / 2) - focused_x), default=0)
|
||||||
|
|
||||||
|
# Add null checks before using current_row_idx and current_col_idx
|
||||||
|
if current_row_idx is None or current_col_idx is None:
|
||||||
|
return
|
||||||
|
|
||||||
current_row = sorted_rows[current_row_idx][1]
|
current_row = sorted_rows[current_row_idx][1]
|
||||||
current_col_idx = current_row.index(focused)
|
if code == ecodes.ABS_HAT0X and value != 0:
|
||||||
|
|
||||||
if code == ecodes.ABS_HAT0X and value != 0: # Left/Right
|
|
||||||
if value < 0: # Left
|
if value < 0: # Left
|
||||||
next_col_idx = current_col_idx - 1
|
if current_col_idx > 0:
|
||||||
if next_col_idx >= 0:
|
next_card = current_row[current_col_idx - 1]
|
||||||
next_card = current_row[next_col_idx]
|
|
||||||
next_card.setFocus(Qt.FocusReason.OtherFocusReason)
|
next_card.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||||
if scroll_area:
|
if scroll_area:
|
||||||
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
||||||
else:
|
else:
|
||||||
# Move to the last card of the previous row if available
|
|
||||||
if current_row_idx > 0:
|
if current_row_idx > 0:
|
||||||
prev_row = sorted_rows[current_row_idx - 1][1]
|
prev_row = sorted_rows[current_row_idx - 1][1]
|
||||||
next_card = prev_row[-1] if prev_row else None
|
next_card = prev_row[-1] if prev_row else None
|
||||||
@@ -732,14 +653,12 @@ class InputManager(QObject):
|
|||||||
if scroll_area:
|
if scroll_area:
|
||||||
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
||||||
elif value > 0: # Right
|
elif value > 0: # Right
|
||||||
next_col_idx = current_col_idx + 1
|
if current_col_idx < len(current_row) - 1:
|
||||||
if next_col_idx < len(current_row):
|
next_card = current_row[current_col_idx + 1]
|
||||||
next_card = current_row[next_col_idx]
|
|
||||||
next_card.setFocus(Qt.FocusReason.OtherFocusReason)
|
next_card.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||||
if scroll_area:
|
if scroll_area:
|
||||||
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
||||||
else:
|
else:
|
||||||
# Move to the first card of the next row if available
|
|
||||||
if current_row_idx < len(sorted_rows) - 1:
|
if current_row_idx < len(sorted_rows) - 1:
|
||||||
next_row = sorted_rows[current_row_idx + 1][1]
|
next_row = sorted_rows[current_row_idx + 1][1]
|
||||||
next_card = next_row[0] if next_row else None
|
next_card = next_row[0] if next_row else None
|
||||||
@@ -747,16 +666,14 @@ class InputManager(QObject):
|
|||||||
next_card.setFocus(Qt.FocusReason.OtherFocusReason)
|
next_card.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||||
if scroll_area:
|
if scroll_area:
|
||||||
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
||||||
elif code == ecodes.ABS_HAT0Y and value != 0: # Up/Down
|
elif code == ecodes.ABS_HAT0Y and value != 0:
|
||||||
if value > 0: # Down
|
if value > 0: # Down
|
||||||
next_row_idx = current_row_idx + 1
|
if current_row_idx < len(sorted_rows) - 1:
|
||||||
if next_row_idx < len(sorted_rows):
|
next_row = sorted_rows[current_row_idx + 1][1]
|
||||||
next_row = sorted_rows[next_row_idx][1]
|
current_x = focused.pos().x() + focused.width() / 2
|
||||||
# Find card in same column or closest based on normalized x
|
|
||||||
target_x = focused.pos().x() / focused.getScale()
|
|
||||||
next_card = min(
|
next_card = min(
|
||||||
next_row,
|
next_row,
|
||||||
key=lambda c: abs(c.pos().x() / c.getScale() - target_x),
|
key=lambda c: abs((c.pos().x() + c.width() / 2) - current_x),
|
||||||
default=None
|
default=None
|
||||||
)
|
)
|
||||||
if next_card:
|
if next_card:
|
||||||
@@ -764,14 +681,12 @@ class InputManager(QObject):
|
|||||||
if scroll_area:
|
if scroll_area:
|
||||||
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
||||||
elif value < 0: # Up
|
elif value < 0: # Up
|
||||||
next_row_idx = current_row_idx - 1
|
if current_row_idx > 0:
|
||||||
if next_row_idx >= 0:
|
prev_row = sorted_rows[current_row_idx - 1][1]
|
||||||
next_row = sorted_rows[next_row_idx][1]
|
current_x = focused.pos().x() + focused.width() / 2
|
||||||
# Find card in same column or closest based on normalized x
|
|
||||||
target_x = focused.pos().x() / focused.getScale()
|
|
||||||
next_card = min(
|
next_card = min(
|
||||||
next_row,
|
prev_row,
|
||||||
key=lambda c: abs(c.pos().x() / c.getScale() - target_x),
|
key=lambda c: abs((c.pos().x() + c.width() / 2) - current_x),
|
||||||
default=None
|
default=None
|
||||||
)
|
)
|
||||||
if next_card:
|
if next_card:
|
||||||
@@ -780,25 +695,33 @@ class InputManager(QObject):
|
|||||||
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
||||||
elif current_row_idx == 0:
|
elif current_row_idx == 0:
|
||||||
self._parent.tabButtons[0].setFocus(Qt.FocusReason.OtherFocusReason)
|
self._parent.tabButtons[0].setFocus(Qt.FocusReason.OtherFocusReason)
|
||||||
|
|
||||||
# Vertical navigation in other tabs
|
|
||||||
elif code == ecodes.ABS_HAT0Y and value != 0:
|
elif code == ecodes.ABS_HAT0Y and value != 0:
|
||||||
focused = QApplication.focusWidget()
|
focused = QApplication.focusWidget()
|
||||||
page = self._parent.stackedWidget.currentWidget()
|
|
||||||
if value > 0: # Down
|
if value > 0: # Down
|
||||||
if isinstance(focused, NavLabel):
|
if isinstance(focused, NavLabel) and self._parent.stackedWidget.currentIndex() == 0:
|
||||||
focusables = page.findChildren(QWidget, options=Qt.FindChildOption.FindChildrenRecursively)
|
# Directly move to the first GameCard in gamesListWidget
|
||||||
focusables = [w for w in focusables if w.focusPolicy() & Qt.FocusPolicy.StrongFocus]
|
cards = self._parent.gamesListWidget.findChildren(GameCard, options=Qt.FindChildOption.FindChildrenRecursively)
|
||||||
if focusables:
|
if cards:
|
||||||
focusables[0].setFocus(Qt.FocusReason.OtherFocusReason)
|
first_card = min(cards, key=lambda c: (c.pos().y(), c.pos().x()), default=None)
|
||||||
return
|
if first_card:
|
||||||
elif focused:
|
first_card.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||||
focused.focusNextChild()
|
scroll_area = None
|
||||||
return
|
parent = first_card
|
||||||
|
while parent and not isinstance(parent, QScrollArea):
|
||||||
|
parent = parent.parentWidget()
|
||||||
|
if isinstance(parent, QScrollArea):
|
||||||
|
scroll_area = parent
|
||||||
|
scroll_area.ensureWidgetVisible(first_card, 50, 50)
|
||||||
|
return
|
||||||
|
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(Qt.FocusReason.OtherFocusReason)
|
||||||
|
return
|
||||||
elif value < 0 and focused: # Up
|
elif value < 0 and focused: # Up
|
||||||
focused.focusPreviousChild()
|
focused.focusPreviousChild()
|
||||||
return
|
return
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in handle_dpad_slot: {e}", exc_info=True)
|
logger.error(f"Error in handle_dpad_slot: {e}", exc_info=True)
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user