forked from Boria138/PortProtonQt
Compare commits
12 Commits
a3d7351e16
...
3d2d5a6243
Author | SHA1 | Date | |
---|---|---|---|
3d2d5a6243
|
|||
565dc49f36
|
|||
c460737bed
|
|||
636ab73580
|
|||
93954abf0d
|
|||
9ab0adf676
|
|||
c08e4fb38d
|
|||
c25589ac96
|
|||
60d6f0734d
|
|||
57d499fab2
|
|||
bc91b03843
|
|||
aabf8cb30f
|
@@ -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 при отсутствии обложек
|
||||||
- Утечки памяти при загрузке обложек
|
- Утечки памяти при загрузке обложек
|
||||||
- Ошибки при подключении геймпада из-за работы в разных потоках
|
- Ошибки при подключении геймпада из-за работы в разных потоках
|
||||||
|
- Множественное открытие диалога добавления игры на геймпаде
|
||||||
|
- Перехват событий геймпада во время работы игры
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
@@ -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()
|
||||||
|
@@ -200,6 +200,10 @@ ACTION_BUTTON_STYLE = """
|
|||||||
QPushButton:pressed {
|
QPushButton:pressed {
|
||||||
background: #282a33;
|
background: #282a33;
|
||||||
}
|
}
|
||||||
|
QPushButton:focus {
|
||||||
|
border: 2px solid #409EFF;
|
||||||
|
background-color: #404554;
|
||||||
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# ТЕКСТОВЫЕ СТИЛИ: ЗАГОЛОВКИ И ОСНОВНОЙ КОНТЕНТ
|
# ТЕКСТОВЫЕ СТИЛИ: ЗАГОЛОВКИ И ОСНОВНОЙ КОНТЕНТ
|
||||||
|
Reference in New Issue
Block a user