diff --git a/portprotonqt/dialogs.py b/portprotonqt/dialogs.py index 167fc84..f008b85 100644 --- a/portprotonqt/dialogs.py +++ b/portprotonqt/dialogs.py @@ -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() diff --git a/portprotonqt/input_manager.py b/portprotonqt/input_manager.py index ddf8119..d5d6b21 100644 --- a/portprotonqt/input_manager.py +++ b/portprotonqt/input_manager.py @@ -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()