fix(input-manager): centralize gamepad handling for FileExplorer
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
This commit is contained in:
@ -2,13 +2,12 @@ import os
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
|
||||
from PySide6.QtGui import QPixmap
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QLineEdit, QFormLayout, QPushButton,
|
||||
QHBoxLayout, QDialogButtonBox, QFileDialog, QLabel, QVBoxLayout, QListWidget
|
||||
)
|
||||
from PySide6.QtCore import Qt, QObject, Signal, QTimer
|
||||
from PySide6.QtCore import Qt, QObject, Signal
|
||||
from icoextract import IconExtractor, IconExtractorError
|
||||
from PIL import Image
|
||||
|
||||
@ -18,8 +17,6 @@ from portprotonqt.logger import get_logger
|
||||
import portprotonqt.themes.standart.styles as default_styles
|
||||
from portprotonqt.themes.standart.styles import FileExplorerStyles
|
||||
|
||||
from evdev import ecodes
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
def generate_thumbnail(inputfile, outfile, size=128, force_resize=True):
|
||||
@ -102,21 +99,17 @@ class FileExplorer(QDialog):
|
||||
self.setWindowFlags(self.windowFlags() & ~Qt.WindowType.WindowContextHelpButtonHint)
|
||||
self.setStyleSheet(FileExplorerStyles.WINDOW_STYLE)
|
||||
|
||||
# Для управления геймпадом
|
||||
# Find InputManager from parent
|
||||
self.input_manager = None
|
||||
self.gamepad_connected = False
|
||||
self.setup_gamepad_handling()
|
||||
parent = self.parent()
|
||||
while parent:
|
||||
if hasattr(parent, 'input_manager'):
|
||||
self.input_manager = parent.input_manager
|
||||
break
|
||||
parent = parent.parent()
|
||||
|
||||
# Для плавного перемещения
|
||||
self.nav_timer = QTimer(self)
|
||||
self.nav_timer.timeout.connect(self.handle_navigation_repeat)
|
||||
self.current_direction = 0
|
||||
self.last_nav_time = 0
|
||||
self.initial_nav_delay = 0.1 # Начальная задержка перед первым повторением (сек)
|
||||
self.repeat_nav_delay = 0.05 # Интервал между повторениями (сек)
|
||||
self.stick_activated = False
|
||||
self.stick_value = 0 # Текущее значение стика (для плавности)
|
||||
self.dead_zone = 8000 # Мертвая зона стика
|
||||
if self.input_manager:
|
||||
self.input_manager.enable_file_explorer_mode(self)
|
||||
|
||||
def setup_ui(self):
|
||||
"""Настройка интерфейса"""
|
||||
@ -138,7 +131,7 @@ class FileExplorer(QDialog):
|
||||
self.file_list.itemClicked.connect(self.handle_item_click)
|
||||
self.layout.addWidget(self.file_list)
|
||||
|
||||
# Кнопки всякие
|
||||
# Кнопки
|
||||
self.button_layout = QHBoxLayout()
|
||||
self.button_layout.setSpacing(10)
|
||||
self.select_button = QPushButton("Select (A)")
|
||||
@ -152,138 +145,10 @@ class FileExplorer(QDialog):
|
||||
self.select_button.clicked.connect(self.select_item)
|
||||
self.cancel_button.clicked.connect(self.reject)
|
||||
|
||||
# Чтобы хомяк открывался
|
||||
# Начальная папка
|
||||
self.current_path = os.path.expanduser("~")
|
||||
self.update_file_list()
|
||||
|
||||
def setup_gamepad_handling(self):
|
||||
"""Настройка обработки геймпада"""
|
||||
parent = self.parent()
|
||||
while parent:
|
||||
if hasattr(parent, 'input_manager'):
|
||||
self.input_manager = parent.input_manager
|
||||
break
|
||||
parent = parent.parent()
|
||||
|
||||
if self.input_manager:
|
||||
try:
|
||||
# Сохраняем оригинальные обработчики геймпада (нужно для возврата управления)
|
||||
self.original_button_handler = self.input_manager.handle_button_slot
|
||||
self.original_dpad_handler = self.input_manager.handle_dpad_slot
|
||||
self.original_gamepad_state = self.input_manager._gamepad_handling_enabled
|
||||
|
||||
# Устанавливаем свои обработчики геймпада для файлового менеджера
|
||||
self.input_manager.handle_button_slot = self.handle_gamepad_button
|
||||
self.input_manager.handle_dpad_slot = self.handle_dpad_movement
|
||||
self.input_manager._gamepad_handling_enabled = True
|
||||
|
||||
self.gamepad_connected = True
|
||||
logger.debug("Gamepad handling successfully connected")
|
||||
except Exception as e:
|
||||
logger.error(f"Error connecting gamepad handlers: {e}")
|
||||
self.gamepad_connected = False
|
||||
|
||||
def handle_gamepad_button(self, button_code):
|
||||
"""Обработка кнопок геймпада"""
|
||||
try:
|
||||
if not self or not hasattr(self, 'file_list'):
|
||||
return
|
||||
|
||||
if button_code in {ecodes.BTN_SOUTH}: # Кнопка A
|
||||
self.select_item()
|
||||
elif button_code in {ecodes.BTN_EAST}: # Кнопка B
|
||||
self.close()
|
||||
else:
|
||||
if hasattr(self, 'original_button_handler') and self.original_button_handler:
|
||||
self.original_button_handler(button_code)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in gamepad button handler: {e}")
|
||||
|
||||
def handle_dpad_movement(self, code, value, current_time):
|
||||
"""Обработка движения D-pad и левого стика"""
|
||||
try:
|
||||
if not self or not hasattr(self, 'file_list') or not self.file_list:
|
||||
return
|
||||
|
||||
if not self.file_list.count():
|
||||
return
|
||||
|
||||
if code in (ecodes.ABS_HAT0Y, ecodes.ABS_Y):
|
||||
# Для D-pad - реакция с фиксированной скоростью
|
||||
if code == ecodes.ABS_HAT0Y:
|
||||
if value != 0:
|
||||
self.current_direction = value
|
||||
self.stick_value = 1.0 # Максимальная скорость для D-pad, чтобы скачков не было
|
||||
if not self.nav_timer.isActive():
|
||||
self.move_selection(self.current_direction)
|
||||
self.last_nav_time = current_time
|
||||
self.nav_timer.start(int(self.initial_nav_delay * 1000))
|
||||
else:
|
||||
self.current_direction = 0
|
||||
self.nav_timer.stop()
|
||||
|
||||
# Для стика - плавное управление с учетом степени отклонения
|
||||
elif code == ecodes.ABS_Y:
|
||||
if abs(value) < self.dead_zone:
|
||||
if self.stick_activated:
|
||||
self.current_direction = 0
|
||||
self.nav_timer.stop()
|
||||
self.stick_activated = False
|
||||
return
|
||||
|
||||
# Рассчитываем "силу" отклонения (0.3 - 1.0)
|
||||
normalized_value = (abs(value) - self.dead_zone) / (32768 - self.dead_zone)
|
||||
speed_factor = 0.3 + (normalized_value * 0.7) # От 30% до 100% скорости
|
||||
|
||||
self.current_direction = -1 if value < 0 else 1
|
||||
self.stick_value = speed_factor
|
||||
self.stick_activated = True
|
||||
|
||||
if not self.nav_timer.isActive():
|
||||
self.move_selection(self.current_direction)
|
||||
self.last_nav_time = current_time
|
||||
self.nav_timer.start(int(self.initial_nav_delay * 1000))
|
||||
|
||||
else:
|
||||
if hasattr(self, 'original_dpad_handler') and self.original_dpad_handler:
|
||||
self.original_dpad_handler(code, value, current_time)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in dpad handler: {e}")
|
||||
|
||||
def handle_navigation_repeat(self):
|
||||
"""Плавное повторение движения с переменной скоростью"""
|
||||
try:
|
||||
if not self or not hasattr(self, 'file_list') or not self.file_list:
|
||||
return
|
||||
|
||||
if self.current_direction != 0:
|
||||
now = time.time()
|
||||
# Динамический интервал в зависимости от stick_value
|
||||
dynamic_delay = self.repeat_nav_delay / self.stick_value
|
||||
if now - self.last_nav_time >= dynamic_delay:
|
||||
self.move_selection(self.current_direction)
|
||||
self.last_nav_time = now
|
||||
except Exception as e:
|
||||
logger.error(f"Error in navigation repeat: {e}")
|
||||
|
||||
def restore_gamepad_handling(self):
|
||||
"""Восстановление оригинальных обработчиков главного окна программы (дефолт возвращаем)"""
|
||||
try:
|
||||
if self.input_manager and self.gamepad_connected:
|
||||
if hasattr(self, 'original_button_handler') and self.original_button_handler:
|
||||
self.input_manager.handle_button_slot = self.original_button_handler
|
||||
|
||||
if hasattr(self, 'original_dpad_handler') and self.original_dpad_handler:
|
||||
self.input_manager.handle_dpad_slot = self.original_dpad_handler
|
||||
|
||||
if hasattr(self, 'original_gamepad_state'):
|
||||
self.input_manager._gamepad_handling_enabled = self.original_gamepad_state
|
||||
|
||||
self.gamepad_connected = False
|
||||
logger.debug("Gamepad handling successfully restored")
|
||||
except Exception as e:
|
||||
logger.error(f"Error restoring gamepad handlers: {e}")
|
||||
|
||||
def move_selection(self, direction):
|
||||
"""Перемещение выбора по списку"""
|
||||
current_row = self.file_list.currentRow()
|
||||
@ -294,7 +159,7 @@ class FileExplorer(QDialog):
|
||||
self.file_list.scrollToItem(self.file_list.currentItem())
|
||||
|
||||
def handle_item_click(self, item):
|
||||
"""Обработка левого клика мышкой"""
|
||||
"""Обработка клика мышью"""
|
||||
self.file_list.setCurrentItem(item)
|
||||
self.select_item()
|
||||
|
||||
@ -337,11 +202,10 @@ class FileExplorer(QDialog):
|
||||
self.path_label.setText(f"Access denied: {self.current_path}")
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""Закрываем окно"""
|
||||
"""Закрытие окна"""
|
||||
try:
|
||||
self.restore_gamepad_handling()
|
||||
self.nav_timer.stop()
|
||||
|
||||
if self.input_manager:
|
||||
self.input_manager.disable_file_explorer_mode()
|
||||
if self.parent():
|
||||
self.parent().activateWindow()
|
||||
self.parent().setFocus()
|
||||
@ -351,13 +215,15 @@ class FileExplorer(QDialog):
|
||||
super().closeEvent(event)
|
||||
|
||||
def reject(self):
|
||||
"""Закрываем окно диалога (два)"""
|
||||
self.restore_gamepad_handling()
|
||||
"""Закрытие диалога"""
|
||||
if self.input_manager:
|
||||
self.input_manager.disable_file_explorer_mode()
|
||||
super().reject()
|
||||
|
||||
def accept(self):
|
||||
"""Принятие (применение) диалога"""
|
||||
self.restore_gamepad_handling()
|
||||
"""Принятие диалога"""
|
||||
if self.input_manager:
|
||||
self.input_manager.disable_file_explorer_mode()
|
||||
super().accept()
|
||||
|
||||
|
||||
|
@ -3,7 +3,7 @@ import threading
|
||||
from typing import Protocol, cast
|
||||
from evdev import InputDevice, InputEvent, ecodes, list_devices, ff
|
||||
from pyudev import Context, Monitor, MonitorObserver, Device
|
||||
from PySide6.QtWidgets import QWidget, QStackedWidget, QApplication, QScrollArea, QLineEdit, QDialog, QMenu, QComboBox, QListView, QMessageBox
|
||||
from PySide6.QtWidgets import QWidget, QStackedWidget, QApplication, QScrollArea, QLineEdit, QDialog, QMenu, QComboBox, QListView, QMessageBox, QListWidget
|
||||
from PySide6.QtCore import Qt, QObject, QEvent, QPoint, Signal, Slot, QTimer
|
||||
from PySide6.QtGui import QKeyEvent
|
||||
from portprotonqt.logger import get_logger
|
||||
@ -93,6 +93,21 @@ class InputManager(QObject):
|
||||
self.last_trigger_time = 0.0
|
||||
self.trigger_cooldown = 0.2
|
||||
|
||||
# FileExplorer specific attributes
|
||||
self.file_explorer = None
|
||||
self.original_button_handler = None
|
||||
self.original_dpad_handler = None
|
||||
self.original_gamepad_state = None
|
||||
self.nav_timer = QTimer(self)
|
||||
self.nav_timer.timeout.connect(self.handle_navigation_repeat)
|
||||
self.current_direction = 0
|
||||
self.last_nav_time = 0
|
||||
self.initial_nav_delay = 0.1 # Начальная задержка перед первым повторением (сек)
|
||||
self.repeat_nav_delay = 0.05 # Интервал между повторениями (сек)
|
||||
self.stick_activated = False
|
||||
self.stick_value = 0 # Текущее значение стика (для плавности)
|
||||
self.dead_zone = 8000 # Мертвая зона стика
|
||||
|
||||
# Add variables for continuous D-pad movement
|
||||
self.dpad_timer = QTimer(self)
|
||||
self.dpad_timer.timeout.connect(self.handle_dpad_repeat)
|
||||
@ -112,6 +127,114 @@ class InputManager(QObject):
|
||||
# Initialize evdev + hotplug
|
||||
self.init_gamepad()
|
||||
|
||||
def enable_file_explorer_mode(self, file_explorer):
|
||||
"""Настройка обработки геймпада для FileExplorer"""
|
||||
try:
|
||||
self.file_explorer = file_explorer
|
||||
self.original_button_handler = self.handle_button_slot
|
||||
self.original_dpad_handler = self.handle_dpad_slot
|
||||
self.original_gamepad_state = self._gamepad_handling_enabled
|
||||
self.handle_button_slot = self.handle_file_explorer_button
|
||||
self.handle_dpad_slot = self.handle_file_explorer_dpad
|
||||
self._gamepad_handling_enabled = True
|
||||
logger.debug("Gamepad handling successfully connected for FileExplorer")
|
||||
except Exception as e:
|
||||
logger.error(f"Error connecting gamepad handlers for FileExplorer: {e}")
|
||||
|
||||
def disable_file_explorer_mode(self):
|
||||
"""Восстановление оригинальных обработчиков главного окна программы (дефолт возвращаем)"""
|
||||
try:
|
||||
if self.file_explorer:
|
||||
self.handle_button_slot = self.original_button_handler
|
||||
self.handle_dpad_slot = self.original_dpad_handler
|
||||
self._gamepad_handling_enabled = self.original_gamepad_state
|
||||
self.file_explorer = None
|
||||
self.nav_timer.stop()
|
||||
logger.debug("Gamepad handling successfully restored")
|
||||
except Exception as e:
|
||||
logger.error(f"Error restoring gamepad handlers: {e}")
|
||||
|
||||
def handle_file_explorer_button(self, button_code):
|
||||
"""Обработка кнопок геймпада для FileExplorer"""
|
||||
try:
|
||||
if not self.file_explorer or not hasattr(self.file_explorer, 'file_list'):
|
||||
return
|
||||
|
||||
if button_code in BUTTONS['confirm']: # Кнопка A
|
||||
self.file_explorer.select_item()
|
||||
elif button_code in BUTTONS['back']: # Кнопка B
|
||||
self.file_explorer.close()
|
||||
else:
|
||||
if self.original_button_handler:
|
||||
self.original_button_handler(button_code)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in FileExplorer button handler: {e}")
|
||||
|
||||
def handle_file_explorer_dpad(self, code, value, current_time):
|
||||
"""Обработка движения D-pad и левого стика для FileExplorer"""
|
||||
try:
|
||||
if not self.file_explorer or not hasattr(self.file_explorer, 'file_list') or not self.file_explorer.file_list:
|
||||
return
|
||||
|
||||
if not self.file_explorer.file_list.count():
|
||||
return
|
||||
|
||||
if code in (ecodes.ABS_HAT0Y, ecodes.ABS_Y):
|
||||
# Для D-pad - реакция с фиксированной скоростью
|
||||
if code == ecodes.ABS_HAT0Y:
|
||||
if value != 0:
|
||||
self.current_direction = value
|
||||
self.stick_value = 1.0 # Максимальная скорость для D-pad, чтобы скачков не было
|
||||
if not self.nav_timer.isActive():
|
||||
self.file_explorer.move_selection(self.current_direction)
|
||||
self.last_nav_time = current_time
|
||||
self.nav_timer.start(int(self.initial_nav_delay * 1000))
|
||||
else:
|
||||
self.current_direction = 0
|
||||
self.nav_timer.stop()
|
||||
|
||||
# Для стика - плавное управление с учетом степени отклонения
|
||||
elif code == ecodes.ABS_Y:
|
||||
if abs(value) < self.dead_zone:
|
||||
if self.stick_activated:
|
||||
self.current_direction = 0
|
||||
self.nav_timer.stop()
|
||||
self.stick_activated = False
|
||||
return
|
||||
|
||||
# Рассчитываем "силу" отклонения (0.3 - 1.0)
|
||||
normalized_value = (abs(value) - self.dead_zone) / (32768 - self.dead_zone)
|
||||
speed_factor = 0.3 + (normalized_value * 0.7) # От 30% до 100% скорости
|
||||
self.current_direction = -1 if value < 0 else 1
|
||||
self.stick_value = speed_factor
|
||||
self.stick_activated = True
|
||||
|
||||
if nöt self.nav_timer.isActive():
|
||||
self.file_explorer.move_selection(self.current_direction)
|
||||
self.last_nav_time = current_time
|
||||
self.nav_timer.start(int(self.initial_nav_delay * 1000))
|
||||
|
||||
elif self.original_dpad_handler:
|
||||
self.original_dpad_handler(code, value, current_time)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in FileExplorer dpad handler: {e}")
|
||||
|
||||
def handle_navigation_repeat(self):
|
||||
"""Плавное повторение движения с переменной скоростью для FileExplorer"""
|
||||
try:
|
||||
if not self.file_explorer or not hasattr(self.file_explorer, 'file_list') or not self.file_explorer.file_list:
|
||||
return
|
||||
|
||||
if self.current_direction != 0:
|
||||
now = time.time()
|
||||
# Динамический интервал в зависимости от stick_value
|
||||
dynamic_delay = self.repeat_nav_delay / self.stick_value
|
||||
if now - self.last_nav_time >= dynamic_delay:
|
||||
self.file_explorer.move_selection(self.current_direction)
|
||||
self.last_nav_time = now
|
||||
except Exception as e:
|
||||
logger.error(f"Error in navigation repeat: {e}")
|
||||
|
||||
@Slot(bool)
|
||||
def handle_fullscreen_slot(self, enable: bool) -> None:
|
||||
try:
|
||||
@ -138,6 +261,7 @@ class InputManager(QObject):
|
||||
self._gamepad_handling_enabled = False
|
||||
self.stop_rumble()
|
||||
self.dpad_timer.stop()
|
||||
self.nav_timer.stop()
|
||||
|
||||
def enable_gamepad_handling(self) -> None:
|
||||
"""Включает обработку событий геймпада."""
|
||||
@ -821,6 +945,7 @@ class InputManager(QObject):
|
||||
try:
|
||||
self.running = False
|
||||
self.dpad_timer.stop()
|
||||
self.nav_timer.stop()
|
||||
self.stop_rumble()
|
||||
if self.gamepad_thread:
|
||||
self.gamepad_thread.join()
|
||||
|
Reference in New Issue
Block a user