2 Commits

Author SHA1 Message Date
cde92885d4 feat(virtual_keybord): added gamepad hint
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-17 00:04:47 +05:00
120c7b319c fix: improve gamepad detection using udev ID_INPUT_JOYSTICK property 2025-10-16 23:20:48 +05:00
2 changed files with 92 additions and 11 deletions

View File

@@ -4,7 +4,7 @@ import os
from typing import Protocol, cast
from evdev import InputDevice, InputEvent, ecodes, list_devices, ff
from enum import Enum
from pyudev import Context, Monitor, MonitorObserver, Device
from pyudev import Context, Monitor, MonitorObserver, Device, Devices
from PySide6.QtWidgets import QWidget, QStackedWidget, QApplication, QScrollArea, QLineEdit, QDialog, QMenu, QComboBox, QListView, QMessageBox, QListWidget, QTableWidget, QAbstractItemView, QTableWidgetItem
from PySide6.QtCore import Qt, QObject, QEvent, QPoint, Signal, Slot, QTimer
from PySide6.QtGui import QKeyEvent, QMouseEvent
@@ -1436,19 +1436,31 @@ class InputManager(QObject):
return super().eventFilter(obj, event)
def init_gamepad(self) -> None:
self.monitor_observer = None # Добавляем атрибут для хранения observer
self.monitor_observer = None
self.udev_context = Context() # Создаём context один раз
self.Devices = Devices # Сохраняем класс для использования в других методах
self.check_gamepad()
threading.Thread(target=self.run_udev_monitor, daemon=True).start()
logger.info("Gamepad support initialized with hotplug (evdev + pyudev)")
def run_udev_monitor(self) -> None:
try:
context = Context()
monitor = Monitor.from_netlink(context)
logger.info("Starting udev monitor...")
monitor = Monitor.from_netlink(self.udev_context)
monitor.filter_by(subsystem='input')
logger.info("Monitor created and filtered")
observer = MonitorObserver(monitor, self.handle_udev_event)
self.monitor_observer = observer # Сохраняем ссылку для остановки
observer.start() # Это блокирует поток до вызова send_stop()
self.monitor_observer = observer
logger.info("MonitorObserver created")
observer.start()
logger.info("MonitorObserver started")
# Держим поток живым, пока не получим сигнал остановки
while self.running:
time.sleep(1)
logger.info("MonitorObserver stopped gracefully")
except Exception as e:
logger.error(f"Error in udev monitor: {e}", exc_info=True)
@@ -1492,14 +1504,33 @@ class InputManager(QObject):
def find_gamepad(self) -> InputDevice | None:
try:
devices = [InputDevice(path) for path in list_devices()]
logger.info(f"Checking {len(devices)} devices for gamepad...")
for device in devices:
logger.debug(f"Checking device: {device.name} at {device.path}")
# Skip ASRock LED controller (vendor ID: 26ce, product ID: 01a2)
if device.info.vendor == 0x26ce and device.info.product == 0x01a2:
logger.debug(f"Skipping ASRock LED controller: {device.name}")
continue
caps = device.capabilities()
if ecodes.EV_KEY in caps or ecodes.EV_ABS in caps:
return device
# Получаем udev-устройство для проверки ID_INPUT_JOYSTICK
try:
udev_device = self.Devices.from_device_file(self.udev_context, device.path)
is_joystick = udev_device.get('ID_INPUT_JOYSTICK')
logger.debug(f"Device {device.name}: ID_INPUT_JOYSTICK = {is_joystick}")
if is_joystick == '1':
logger.info(f"Found gamepad: {device.name}")
return device
else:
logger.debug(f"Skipping non-joystick device: {device.name}")
except Exception as e:
logger.warning(f"Could not check udev properties for {device.path}: {e}")
continue
logger.warning("No gamepad found")
return None
except Exception as e:
logger.error(f"Error finding gamepad: {e}", exc_info=True)

View File

@@ -1,7 +1,8 @@
from typing import cast
from typing import cast, Any
from PySide6.QtWidgets import (QFrame, QVBoxLayout, QPushButton, QGridLayout,
QSizePolicy, QWidget, QLineEdit)
from PySide6.QtCore import Qt, Signal, QProcess
from PySide6.QtCore import Qt, Signal, QProcess, QSize
from PySide6.QtGui import QPixmap, QIcon
from portprotonqt.keyboard_layouts import keyboard_layouts
from portprotonqt.theme_manager import ThemeManager
from portprotonqt.config_utils import read_theme_from_config
@@ -43,6 +44,18 @@ class VirtualKeyboard(QFrame):
self.margins = 10
self.num_cols = 14
# Find input_manager and main_window
self.input_manager: Any = None
self.main_window: Any = None
parent_widget: QWidget | None = self._parent
while parent_widget:
if hasattr(parent_widget, 'input_manager'):
self.input_manager = cast(Any, parent_widget).input_manager
self.main_window = cast(Any, parent_widget)
parent_widget = cast(QWidget | None, parent_widget.parent())
self.current_theme_name = read_theme_from_config()
self.initUI()
self.hide()
@@ -119,6 +132,28 @@ class VirtualKeyboard(QFrame):
self.buttons: dict[str, QPushButton] = {}
self.update_keyboard()
def set_gamepad_icon(self, button, icon_type, gtype='default'):
"""Set gamepad icon on button based on type. Now works even without gamepad by using 'default' gtype."""
if icon_type in ['back', 'add_game']:
icon_name = self.main_window.get_button_icon(icon_type, gtype) if self.main_window else f"{icon_type}_default.png"
else: # nav left/right
if icon_type in ['left', 'right']:
direction = icon_type
icon_name = self.main_window.get_nav_icon(direction, gtype) if self.main_window else f"{direction}_default.png"
else:
direction = 'left' if icon_type == 'left' else 'right'
icon_name = self.main_window.get_nav_icon(direction, gtype) if self.main_window else f"{direction}_default.png"
icon_path = theme_manager.get_theme_image(icon_name, self.current_theme_name)
if icon_path:
pixmap = QPixmap(str(icon_path))
if not pixmap.isNull():
button.setIcon(QIcon(pixmap))
button.setIconSize(QSize(20, 20))
return
# Fallback: if no icon found, try standard Qt icon or leave empty
print(f"Warning: Icon {icon_name} not found for button {icon_type}")
def update_keyboard(self):
coords = self._save_focused_coords()
@@ -151,6 +186,9 @@ class VirtualKeyboard(QFrame):
button.setCheckable(True)
button.setChecked(self.shift_pressed)
button.clicked.connect(lambda checked: self.on_shift_click(checked))
# Add gamepad icon for Shift (RB/R)
gtype = self.input_manager.gamepad_type if self.input_manager and self.input_manager.gamepad else 'default'
self.set_gamepad_icon(button, 'right', gtype)
else:
button.clicked.connect(lambda checked=False, k=key: self.on_button_click(k))
@@ -163,6 +201,9 @@ class VirtualKeyboard(QFrame):
shift.setCheckable(True)
shift.setChecked(self.shift_pressed)
shift.clicked.connect(lambda checked: self.on_shift_click(checked))
# Add gamepad icon for Shift (RB/R)
gtype = self.input_manager.gamepad_type if self.input_manager and self.input_manager.gamepad else 'default'
self.set_gamepad_icon(shift, 'right', gtype)
self.keyboard_layout.addWidget(shift, 3, 11, 1, 3)
button = QPushButton('CAPS')
@@ -179,6 +220,9 @@ class VirtualKeyboard(QFrame):
backspace.setFixedSize(fixed_w, fixed_h)
backspace.pressed.connect(self.on_backspace_pressed)
backspace.released.connect(self.stop_backspace_repeat)
# Add gamepad icon for Backspace (X/Triangle)
gtype = self.input_manager.gamepad_type if self.input_manager and self.input_manager.gamepad else 'default'
self.set_gamepad_icon(backspace, 'add_game', gtype)
self.keyboard_layout.addWidget(backspace, 0, 13, 1, 1)
enter = QPushButton('Enter')
@@ -189,6 +233,9 @@ class VirtualKeyboard(QFrame):
lang = QPushButton('🌐')
lang.setFixedSize(fixed_w, fixed_h)
lang.clicked.connect(self.on_lang_click)
# Add gamepad icon for Lang (LB/L)
gtype = self.input_manager.gamepad_type if self.input_manager and self.input_manager.gamepad else 'default'
self.set_gamepad_icon(lang, 'left', gtype)
self.keyboard_layout.addWidget(lang, 4, 0, 1, 1)
clear = QPushButton('Clear')
@@ -219,6 +266,9 @@ class VirtualKeyboard(QFrame):
hide_button = QPushButton('Hide')
hide_button.setFixedSize(fixed_w * 2 + self.spacing, fixed_h)
hide_button.clicked.connect(self.hide)
# Add gamepad icon for Hide (B/Circle)
gtype = self.input_manager.gamepad_type if self.input_manager and self.input_manager.gamepad else 'default'
self.set_gamepad_icon(hide_button, 'back', gtype)
self.keyboard_layout.addWidget(hide_button, 4, 12, 1, 2)
if coords: