forked from Boria138/PortProtonQt
fix(input-manager): resolve threading error in gamepad events
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
This commit is contained in:
parent
b317e4760b
commit
fe208f0783
@ -4,13 +4,13 @@ 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
|
||||||
from PySide6.QtCore import Qt, QObject, QEvent, QPoint
|
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
|
||||||
from portprotonqt.image_utils import FullscreenDialog
|
from portprotonqt.image_utils import FullscreenDialog
|
||||||
from portprotonqt.custom_widgets import NavLabel
|
from portprotonqt.custom_widgets import NavLabel
|
||||||
from portprotonqt.game_card import GameCard
|
from portprotonqt.game_card import GameCard
|
||||||
from portprotonqt.config_utils import read_fullscreen_config
|
from portprotonqt.config_utils import read_fullscreen_config, read_window_geometry, save_window_geometry
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@ -31,23 +31,15 @@ class MainWindowProtocol(Protocol):
|
|||||||
currentDetailPage: QWidget | None
|
currentDetailPage: QWidget | None
|
||||||
current_exec_line: str | None
|
current_exec_line: str | None
|
||||||
|
|
||||||
# Mapping of actions to evdev button codes, includes PlayStation, Xbox, and Switch controllers (https://www.kernel.org/doc/html/v4.12/input/gamepad.html)
|
# Mapping of actions to evdev button codes, includes PlayStation, Xbox, and Switch controllers
|
||||||
BUTTONS = {
|
BUTTONS = {
|
||||||
# South button: X (PlayStation), A (Xbox), B (Switch Joy-Con south)
|
'confirm': {ecodes.BTN_A},
|
||||||
'confirm': {ecodes.BTN_SOUTH, ecodes.BTN_A},
|
'back': {ecodes.BTN_B},
|
||||||
# East button: Circle (PS), B (Xbox), A (Switch Joy-Con east)
|
'add_game': {ecodes.BTN_Y},
|
||||||
'back': {ecodes.BTN_EAST, ecodes.BTN_B},
|
|
||||||
# North button: Triangle (PS), Y (Xbox), X (Switch Joy-Con north)
|
|
||||||
'add_game': {ecodes.BTN_NORTH, ecodes.BTN_Y},
|
|
||||||
# Shoulder buttons: L1/L2 (PS), LB (Xbox), L (Switch): BTN_TL, BTN_TL2
|
|
||||||
'prev_tab': {ecodes.BTN_TL, ecodes.BTN_TL2},
|
'prev_tab': {ecodes.BTN_TL, ecodes.BTN_TL2},
|
||||||
# Shoulder buttons: R1/R2 (PS), RB (Xbox), R (Switch): BTN_TR, BTN_TR2
|
|
||||||
'next_tab': {ecodes.BTN_TR, ecodes.BTN_TR2},
|
'next_tab': {ecodes.BTN_TR, ecodes.BTN_TR2},
|
||||||
# Optional: stick presses on Switch Joy-Con
|
|
||||||
'confirm_stick': {ecodes.BTN_THUMBL, ecodes.BTN_THUMBR},
|
'confirm_stick': {ecodes.BTN_THUMBL, ecodes.BTN_THUMBR},
|
||||||
# Start button for context menu
|
|
||||||
'context_menu': {ecodes.BTN_START},
|
'context_menu': {ecodes.BTN_START},
|
||||||
# Select/home for back/menu
|
|
||||||
'menu': {ecodes.BTN_SELECT, ecodes.BTN_MODE},
|
'menu': {ecodes.BTN_SELECT, ecodes.BTN_MODE},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,8 +47,14 @@ class InputManager(QObject):
|
|||||||
"""
|
"""
|
||||||
Manages input from gamepads and keyboards for navigating the application interface.
|
Manages input from gamepads and keyboards for navigating the application interface.
|
||||||
Supports gamepad hotplugging, button and axis events, and keyboard event filtering
|
Supports gamepad hotplugging, button and axis events, and keyboard event filtering
|
||||||
for seamless UI interaction.
|
for seamless UI interaction. Enables fullscreen mode when a gamepad is connected
|
||||||
|
and restores normal mode when disconnected.
|
||||||
"""
|
"""
|
||||||
|
# Signals for gamepad events
|
||||||
|
button_pressed = Signal(int) # Signal for button presses
|
||||||
|
dpad_moved = Signal(int, int, float) # Signal for D-pad movements
|
||||||
|
toggle_fullscreen = Signal(bool) # Signal for toggling fullscreen mode (True for fullscreen, False for normal)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
main_window: MainWindowProtocol,
|
main_window: MainWindowProtocol,
|
||||||
@ -81,22 +79,48 @@ class InputManager(QObject):
|
|||||||
self.running = True
|
self.running = True
|
||||||
self._is_fullscreen = read_fullscreen_config()
|
self._is_fullscreen = read_fullscreen_config()
|
||||||
|
|
||||||
|
# Connect signals to slots
|
||||||
|
self.button_pressed.connect(self.handle_button_slot)
|
||||||
|
self.dpad_moved.connect(self.handle_dpad_slot)
|
||||||
|
self.toggle_fullscreen.connect(self.handle_fullscreen_slot)
|
||||||
|
|
||||||
# Install keyboard event filter
|
# Install keyboard event filter
|
||||||
app = QApplication.instance()
|
app = QApplication.instance()
|
||||||
if app is not None:
|
if app is not None:
|
||||||
app.installEventFilter(self)
|
app.installEventFilter(self)
|
||||||
else:
|
|
||||||
logger.error("QApplication instance is None, cannot install event filter")
|
|
||||||
|
|
||||||
# Initialize evdev + hotplug
|
# Initialize evdev + hotplug
|
||||||
self.init_gamepad()
|
self.init_gamepad()
|
||||||
|
|
||||||
|
@Slot(bool)
|
||||||
|
def handle_fullscreen_slot(self, enable: bool) -> None:
|
||||||
|
try:
|
||||||
|
if read_fullscreen_config():
|
||||||
|
return
|
||||||
|
window = self._parent
|
||||||
|
if not isinstance(window, QWidget):
|
||||||
|
return
|
||||||
|
if enable and not self._is_fullscreen:
|
||||||
|
if not window.isFullScreen():
|
||||||
|
save_window_geometry(window.width(), window.height())
|
||||||
|
window.showFullScreen()
|
||||||
|
self._is_fullscreen = True
|
||||||
|
elif not enable and self._is_fullscreen:
|
||||||
|
window.showNormal()
|
||||||
|
width, height = read_window_geometry()
|
||||||
|
if width > 0 and height > 0:
|
||||||
|
window.resize(width, height)
|
||||||
|
self._is_fullscreen = False
|
||||||
|
save_window_geometry(width, height)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in handle_fullscreen_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:
|
||||||
return super().eventFilter(obj, event)
|
return super().eventFilter(obj, event)
|
||||||
|
|
||||||
# 1) Интересуют только нажатия клавиш
|
# Handle only key press events
|
||||||
if not (isinstance(event, QKeyEvent) and event.type() == QEvent.Type.KeyPress):
|
if not (isinstance(event, QKeyEvent) and event.type() == QEvent.Type.KeyPress):
|
||||||
return super().eventFilter(obj, event)
|
return super().eventFilter(obj, event)
|
||||||
|
|
||||||
@ -105,17 +129,16 @@ class InputManager(QObject):
|
|||||||
focused = QApplication.focusWidget()
|
focused = QApplication.focusWidget()
|
||||||
popup = QApplication.activePopupWidget()
|
popup = QApplication.activePopupWidget()
|
||||||
|
|
||||||
# 2) Закрытие приложения по Ctrl+Q
|
# Close application with Ctrl+Q
|
||||||
if key == Qt.Key.Key_Q and modifiers & Qt.KeyboardModifier.ControlModifier:
|
if key == Qt.Key.Key_Q and modifiers & Qt.KeyboardModifier.ControlModifier:
|
||||||
app.quit()
|
app.quit()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# 3) Если открыт любой popup — не перехватываем ENTER, ESC и стрелки
|
# Skip navigation keys if a popup is open
|
||||||
if popup:
|
if popup:
|
||||||
# возвращаем False, чтобы событие пошло дальше в Qt и закрыло popup как нужно
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 4) Навигация в полноэкранном просмотре
|
# FullscreenDialog navigation
|
||||||
active_win = QApplication.activeWindow()
|
active_win = QApplication.activeWindow()
|
||||||
if isinstance(active_win, FullscreenDialog):
|
if isinstance(active_win, FullscreenDialog):
|
||||||
if key == Qt.Key.Key_Right:
|
if key == Qt.Key.Key_Right:
|
||||||
@ -128,27 +151,25 @@ class InputManager(QObject):
|
|||||||
active_win.close()
|
active_win.close()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# 5) На странице деталей Enter запускает/останавливает игру
|
# Launch/stop game on detail page
|
||||||
if self._parent.currentDetailPage and key in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
|
if self._parent.currentDetailPage and key in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
|
||||||
if self._parent.current_exec_line:
|
if self._parent.current_exec_line:
|
||||||
self._parent.toggleGame(self._parent.current_exec_line, None)
|
self._parent.toggleGame(self._parent.current_exec_line, None)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# 6) Открытие контекстного меню для GameCard
|
# Context menu for GameCard
|
||||||
if isinstance(focused, GameCard):
|
if isinstance(focused, GameCard):
|
||||||
if key == Qt.Key.Key_F10 and Qt.KeyboardModifier.ShiftModifier:
|
if key == Qt.Key.Key_F10 and Qt.KeyboardModifier.ShiftModifier:
|
||||||
pos = QPoint(focused.width() // 2, focused.height() // 2)
|
pos = QPoint(focused.width() // 2, focused.height() // 2)
|
||||||
focused._show_context_menu(pos)
|
focused._show_context_menu(pos)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# 7) Навигация по карточкам в Library
|
# Navigation in Library tab
|
||||||
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 not scroll_area:
|
|
||||||
logger.warning("No QScrollArea found for gamesListWidget")
|
|
||||||
|
|
||||||
if isinstance(focused, GameCard):
|
if isinstance(focused, GameCard):
|
||||||
current_index = game_cards.index(focused) if focused in game_cards else -1
|
current_index = game_cards.index(focused) if focused in game_cards else -1
|
||||||
@ -184,7 +205,7 @@ class InputManager(QObject):
|
|||||||
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# 8) Переключение вкладок ←/→
|
# Tab switching with Left/Right keys
|
||||||
idx = self._parent.stackedWidget.currentIndex()
|
idx = self._parent.stackedWidget.currentIndex()
|
||||||
total = len(self._parent.tabButtons)
|
total = len(self._parent.tabButtons)
|
||||||
if key == Qt.Key.Key_Left and not isinstance(focused, GameCard):
|
if key == Qt.Key.Key_Left and not isinstance(focused, GameCard):
|
||||||
@ -198,7 +219,7 @@ class InputManager(QObject):
|
|||||||
self._parent.tabButtons[new].setFocus()
|
self._parent.tabButtons[new].setFocus()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# 9) Спуск в содержимое вкладки ↓
|
# Navigate down into tab content
|
||||||
if key == Qt.Key.Key_Down:
|
if key == Qt.Key.Key_Down:
|
||||||
if isinstance(focused, NavLabel):
|
if isinstance(focused, NavLabel):
|
||||||
page = self._parent.stackedWidget.currentWidget()
|
page = self._parent.stackedWidget.currentWidget()
|
||||||
@ -212,15 +233,15 @@ class InputManager(QObject):
|
|||||||
focused.focusNextChild()
|
focused.focusNextChild()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# 10) Подъём по содержимому вкладки ↑
|
# 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):
|
||||||
return True # Не даём уйти выше NavLabel
|
return True
|
||||||
if focused is not None:
|
if focused is not None:
|
||||||
focused.focusPreviousChild()
|
focused.focusPreviousChild()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# 11) Общие: Activate, Back, Add
|
# General actions: Activate, Back, Add
|
||||||
if key in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
|
if key in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
|
||||||
self._parent.activateFocusedWidget()
|
self._parent.activateFocusedWidget()
|
||||||
return True
|
return True
|
||||||
@ -235,18 +256,11 @@ class InputManager(QObject):
|
|||||||
self._parent.openAddGameDialog()
|
self._parent.openAddGameDialog()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# 12) Переключение полноэкранного режима по F11
|
# Toggle fullscreen with F11
|
||||||
if key == Qt.Key.Key_F11:
|
if key == Qt.Key.Key_F11:
|
||||||
if read_fullscreen_config():
|
if read_fullscreen_config():
|
||||||
return True
|
return True
|
||||||
window = self._parent
|
self.toggle_fullscreen.emit(not self._is_fullscreen)
|
||||||
if isinstance(window, QWidget):
|
|
||||||
if self._is_fullscreen:
|
|
||||||
window.showNormal()
|
|
||||||
self._is_fullscreen = False
|
|
||||||
else:
|
|
||||||
window.showFullScreen()
|
|
||||||
self._is_fullscreen = True
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return super().eventFilter(obj, event)
|
return super().eventFilter(obj, event)
|
||||||
@ -254,9 +268,10 @@ class InputManager(QObject):
|
|||||||
def init_gamepad(self) -> None:
|
def init_gamepad(self) -> None:
|
||||||
self.check_gamepad()
|
self.check_gamepad()
|
||||||
threading.Thread(target=self.run_udev_monitor, daemon=True).start()
|
threading.Thread(target=self.run_udev_monitor, daemon=True).start()
|
||||||
logger.info("Input support initialized with hotplug (evdev + pyudev)")
|
logger.info("Gamepad support initialized with hotplug (evdev + pyudev)")
|
||||||
|
|
||||||
def run_udev_monitor(self) -> None:
|
def run_udev_monitor(self) -> None:
|
||||||
|
try:
|
||||||
context = pyudev.Context()
|
context = pyudev.Context()
|
||||||
monitor = pyudev.Monitor.from_netlink(context)
|
monitor = pyudev.Monitor.from_netlink(context)
|
||||||
monitor.filter_by(subsystem='input')
|
monitor.filter_by(subsystem='input')
|
||||||
@ -264,8 +279,11 @@ class InputManager(QObject):
|
|||||||
observer.start()
|
observer.start()
|
||||||
while self.running:
|
while self.running:
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in udev monitor: {e}", exc_info=True)
|
||||||
|
|
||||||
def handle_udev_event(self, action: str, device: pyudev.Device) -> None:
|
def handle_udev_event(self, action: str, device: pyudev.Device) -> None:
|
||||||
|
try:
|
||||||
if action == 'add':
|
if action == 'add':
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
self.check_gamepad()
|
self.check_gamepad()
|
||||||
@ -275,8 +293,13 @@ class InputManager(QObject):
|
|||||||
self.gamepad = None
|
self.gamepad = None
|
||||||
if self.gamepad_thread:
|
if self.gamepad_thread:
|
||||||
self.gamepad_thread.join()
|
self.gamepad_thread.join()
|
||||||
|
# Signal to exit fullscreen mode
|
||||||
|
self.toggle_fullscreen.emit(False)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error handling udev event: {e}", exc_info=True)
|
||||||
|
|
||||||
def check_gamepad(self) -> None:
|
def check_gamepad(self) -> None:
|
||||||
|
try:
|
||||||
new_gamepad = self.find_gamepad()
|
new_gamepad = self.find_gamepad()
|
||||||
if new_gamepad and new_gamepad != self.gamepad:
|
if new_gamepad and new_gamepad != self.gamepad:
|
||||||
logger.info(f"Gamepad connected: {new_gamepad.name}")
|
logger.info(f"Gamepad connected: {new_gamepad.name}")
|
||||||
@ -285,14 +308,22 @@ class InputManager(QObject):
|
|||||||
self.gamepad_thread.join()
|
self.gamepad_thread.join()
|
||||||
self.gamepad_thread = threading.Thread(target=self.monitor_gamepad, daemon=True)
|
self.gamepad_thread = threading.Thread(target=self.monitor_gamepad, daemon=True)
|
||||||
self.gamepad_thread.start()
|
self.gamepad_thread.start()
|
||||||
|
# Signal to enter fullscreen mode
|
||||||
|
self.toggle_fullscreen.emit(True)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error checking gamepad: {e}", exc_info=True)
|
||||||
|
|
||||||
def find_gamepad(self) -> InputDevice | None:
|
def find_gamepad(self) -> InputDevice | None:
|
||||||
|
try:
|
||||||
devices = [InputDevice(path) for path in list_devices()]
|
devices = [InputDevice(path) for path in list_devices()]
|
||||||
for device in devices:
|
for device in devices:
|
||||||
caps = device.capabilities()
|
caps = device.capabilities()
|
||||||
if ecodes.EV_KEY in caps or ecodes.EV_ABS in caps:
|
if ecodes.EV_KEY in caps or ecodes.EV_ABS in caps:
|
||||||
return device
|
return device
|
||||||
return None
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error finding gamepad: {e}", exc_info=True)
|
||||||
|
return None
|
||||||
|
|
||||||
def monitor_gamepad(self) -> None:
|
def monitor_gamepad(self) -> None:
|
||||||
try:
|
try:
|
||||||
@ -305,16 +336,29 @@ class InputManager(QObject):
|
|||||||
continue
|
continue
|
||||||
now = time.time()
|
now = time.time()
|
||||||
if event.type == ecodes.EV_KEY and event.value == 1:
|
if event.type == ecodes.EV_KEY and event.value == 1:
|
||||||
self.handle_button(event.code)
|
self.button_pressed.emit(event.code)
|
||||||
elif event.type == ecodes.EV_ABS:
|
elif event.type == ecodes.EV_ABS:
|
||||||
self.handle_dpad(event.code, event.value, now)
|
self.dpad_moved.emit(event.code, event.value, now)
|
||||||
|
except OSError as e:
|
||||||
|
if e.errno == 19: # ENODEV: No such device
|
||||||
|
logger.info("Gamepad disconnected during event loop")
|
||||||
|
else:
|
||||||
|
logger.error(f"OSError in gamepad monitoring: {e}", exc_info=True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error accessing gamepad: {e}")
|
logger.error(f"Error in gamepad monitoring: {e}", exc_info=True)
|
||||||
|
finally:
|
||||||
|
if self.gamepad:
|
||||||
|
try:
|
||||||
|
self.gamepad.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self.gamepad = None
|
||||||
|
|
||||||
def handle_button(self, button_code: int) -> None:
|
@Slot(int)
|
||||||
|
def handle_button_slot(self, button_code: int) -> None:
|
||||||
|
try:
|
||||||
app = QApplication.instance()
|
app = QApplication.instance()
|
||||||
if app is None:
|
if not app:
|
||||||
logger.error("QApplication instance is None")
|
|
||||||
return
|
return
|
||||||
active = QApplication.activeWindow()
|
active = QApplication.activeWindow()
|
||||||
focused = QApplication.focusWidget()
|
focused = QApplication.focusWidget()
|
||||||
@ -357,11 +401,14 @@ class InputManager(QObject):
|
|||||||
idx = (self._parent.stackedWidget.currentIndex() + 1) % len(self._parent.tabButtons)
|
idx = (self._parent.stackedWidget.currentIndex() + 1) % len(self._parent.tabButtons)
|
||||||
self._parent.switchTab(idx)
|
self._parent.switchTab(idx)
|
||||||
self._parent.tabButtons[idx].setFocus(Qt.FocusReason.OtherFocusReason)
|
self._parent.tabButtons[idx].setFocus(Qt.FocusReason.OtherFocusReason)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in handle_button_slot: {e}", exc_info=True)
|
||||||
|
|
||||||
def handle_dpad(self, code: int, value: int, current_time: float) -> None:
|
@Slot(int, int, float)
|
||||||
|
def handle_dpad_slot(self, code: int, value: int, current_time: float) -> None:
|
||||||
|
try:
|
||||||
app = QApplication.instance()
|
app = QApplication.instance()
|
||||||
if app is None:
|
if not app:
|
||||||
logger.error("QApplication instance is None")
|
|
||||||
return
|
return
|
||||||
active = QApplication.activeWindow()
|
active = QApplication.activeWindow()
|
||||||
|
|
||||||
@ -375,13 +422,11 @@ class InputManager(QObject):
|
|||||||
|
|
||||||
# Vertical navigation (DPAD up/down)
|
# Vertical navigation (DPAD up/down)
|
||||||
if code == ecodes.ABS_HAT0Y:
|
if code == ecodes.ABS_HAT0Y:
|
||||||
# ignore release
|
|
||||||
if value == 0:
|
if value == 0:
|
||||||
return
|
return
|
||||||
focused = QApplication.focusWidget()
|
focused = QApplication.focusWidget()
|
||||||
page = self._parent.stackedWidget.currentWidget()
|
page = self._parent.stackedWidget.currentWidget()
|
||||||
if value > 0:
|
if value > 0:
|
||||||
# down
|
|
||||||
if isinstance(focused, NavLabel):
|
if isinstance(focused, NavLabel):
|
||||||
focusables = page.findChildren(QWidget, options=Qt.FindChildOption.FindChildrenRecursively)
|
focusables = page.findChildren(QWidget, options=Qt.FindChildOption.FindChildrenRecursively)
|
||||||
focusables = [w for w in focusables if w.focusPolicy() & Qt.FocusPolicy.StrongFocus]
|
focusables = [w for w in focusables if w.focusPolicy() & Qt.FocusPolicy.StrongFocus]
|
||||||
@ -392,7 +437,6 @@ class InputManager(QObject):
|
|||||||
focused.focusNextChild()
|
focused.focusNextChild()
|
||||||
return
|
return
|
||||||
elif value < 0 and focused:
|
elif value < 0 and focused:
|
||||||
# up
|
|
||||||
focused.focusPreviousChild()
|
focused.focusPreviousChild()
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -411,8 +455,11 @@ class InputManager(QObject):
|
|||||||
self.trigger_dpad_movement(code, value)
|
self.trigger_dpad_movement(code, value)
|
||||||
self.last_move_time = current_time
|
self.last_move_time = current_time
|
||||||
self.current_axis_delay = self.repeat_axis_move_delay
|
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:
|
def trigger_dpad_movement(self, code: int, value: int) -> None:
|
||||||
|
try:
|
||||||
if code != ecodes.ABS_HAT0X:
|
if code != ecodes.ABS_HAT0X:
|
||||||
return
|
return
|
||||||
idx = self._parent.stackedWidget.currentIndex()
|
idx = self._parent.stackedWidget.currentIndex()
|
||||||
@ -422,9 +469,15 @@ class InputManager(QObject):
|
|||||||
new = (idx + 1) % len(self._parent.tabButtons)
|
new = (idx + 1) % len(self._parent.tabButtons)
|
||||||
self._parent.switchTab(new)
|
self._parent.switchTab(new)
|
||||||
self._parent.tabButtons[new].setFocus(Qt.FocusReason.OtherFocusReason)
|
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:
|
||||||
self.running = False
|
self.running = False
|
||||||
|
if self.gamepad_thread:
|
||||||
|
self.gamepad_thread.join()
|
||||||
if self.gamepad:
|
if self.gamepad:
|
||||||
self.gamepad.close()
|
self.gamepad.close()
|
||||||
logger.info("Input support cleaned up")
|
except Exception as e:
|
||||||
|
logger.error(f"Error during cleanup: {e}", exc_info=True)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user