feat(file-explorer): add gamepad support for selecting mounted drives
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
This commit is contained in:
@ -3,7 +3,7 @@ import tempfile
|
|||||||
from typing import cast, TYPE_CHECKING
|
from typing import cast, TYPE_CHECKING
|
||||||
from PySide6.QtGui import QPixmap, QIcon
|
from PySide6.QtGui import QPixmap, QIcon
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QDialog, QLineEdit, QFormLayout, QHBoxLayout, QLabel, QVBoxLayout, QListWidget, QScrollArea, QWidget, QListWidgetItem, QSizePolicy
|
QDialog, QLineEdit, QFormLayout, QHBoxLayout, QLabel, QVBoxLayout, QListWidget, QScrollArea, QWidget, QListWidgetItem, QSizePolicy, QApplication
|
||||||
)
|
)
|
||||||
from PySide6.QtCore import Qt, QObject, Signal, QMimeDatabase, QTimer
|
from PySide6.QtCore import Qt, QObject, Signal, QMimeDatabase, QTimer
|
||||||
from icoextract import IconExtractor, IconExtractorError
|
from icoextract import IconExtractor, IconExtractorError
|
||||||
@ -167,6 +167,7 @@ class FileExplorer(QDialog):
|
|||||||
self.drives_scroll.setFixedHeight(70)
|
self.drives_scroll.setFixedHeight(70)
|
||||||
self.main_layout.addWidget(self.drives_scroll)
|
self.main_layout.addWidget(self.drives_scroll)
|
||||||
self.drives_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
self.drives_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||||
|
self.drives_scroll.setFocusPolicy(Qt.FocusPolicy.StrongFocus) # Allow focus on scroll area
|
||||||
|
|
||||||
# Путь
|
# Путь
|
||||||
self.path_label = QLabel()
|
self.path_label = QLabel()
|
||||||
@ -177,7 +178,7 @@ class FileExplorer(QDialog):
|
|||||||
self.file_list = QListWidget()
|
self.file_list = QListWidget()
|
||||||
self.file_list.setStyleSheet(self.theme.FILE_EXPLORER_STYLE)
|
self.file_list.setStyleSheet(self.theme.FILE_EXPLORER_STYLE)
|
||||||
self.file_list.itemClicked.connect(self.handle_item_click)
|
self.file_list.itemClicked.connect(self.handle_item_click)
|
||||||
self.file_list.itemDoubleClicked.connect(self.handle_item_double_click) # Подключение двойного клика
|
self.file_list.itemDoubleClicked.connect(self.handle_item_double_click)
|
||||||
self.main_layout.addWidget(self.file_list)
|
self.main_layout.addWidget(self.file_list)
|
||||||
|
|
||||||
# Кнопки
|
# Кнопки
|
||||||
@ -291,14 +292,37 @@ class FileExplorer(QDialog):
|
|||||||
widget.deleteLater()
|
widget.deleteLater()
|
||||||
|
|
||||||
drives = self.get_mounted_drives()
|
drives = self.get_mounted_drives()
|
||||||
|
self.drive_buttons = [] # Store buttons for navigation
|
||||||
for drive in drives:
|
for drive in drives:
|
||||||
drive_name = os.path.basename(drive) or drive.split('/')[-1] or drive
|
drive_name = os.path.basename(drive) or drive.split('/')[-1] or drive
|
||||||
button = AutoSizeButton(drive_name, icon=self.theme_manager.get_icon("mount_point"))
|
button = AutoSizeButton(drive_name, icon=self.theme_manager.get_icon("mount_point"))
|
||||||
button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||||
|
button.setFocusPolicy(Qt.FocusPolicy.StrongFocus) # Make button focusable
|
||||||
button.clicked.connect(lambda checked, path=drive: self.change_drive(path))
|
button.clicked.connect(lambda checked, path=drive: self.change_drive(path))
|
||||||
self.drives_layout.addWidget(button)
|
self.drives_layout.addWidget(button)
|
||||||
|
self.drive_buttons.append(button)
|
||||||
self.drives_layout.addStretch()
|
self.drives_layout.addStretch()
|
||||||
|
|
||||||
|
# Set focus to first drive button if available
|
||||||
|
if self.drive_buttons:
|
||||||
|
self.drive_buttons[0].setFocus()
|
||||||
|
|
||||||
|
def select_drive(self):
|
||||||
|
"""Handle drive selection via gamepad"""
|
||||||
|
focused_widget = QApplication.focusWidget()
|
||||||
|
if isinstance(focused_widget, AutoSizeButton) and focused_widget in self.drive_buttons:
|
||||||
|
drive_path = None
|
||||||
|
for drive in self.get_mounted_drives():
|
||||||
|
drive_name = os.path.basename(drive) or drive.split('/')[-1] or drive
|
||||||
|
if drive_name == focused_widget.text():
|
||||||
|
drive_path = drive
|
||||||
|
break
|
||||||
|
if drive_path and os.path.isdir(drive_path) and os.access(drive_path, os.R_OK):
|
||||||
|
self.current_path = os.path.normpath(drive_path)
|
||||||
|
self.update_file_list()
|
||||||
|
else:
|
||||||
|
logger.warning(f"Путь диска недоступен: {drive_path}")
|
||||||
|
|
||||||
def change_drive(self, drive_path):
|
def change_drive(self, drive_path):
|
||||||
"""Переход к выбранному диску"""
|
"""Переход к выбранному диску"""
|
||||||
if os.path.isdir(drive_path) and os.access(drive_path, os.R_OK):
|
if os.path.isdir(drive_path) and os.access(drive_path, os.R_OK):
|
||||||
|
@ -9,7 +9,7 @@ from PySide6.QtCore import Qt, QObject, QEvent, QPoint, Signal, Slot, QTimer
|
|||||||
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, AutoSizeButton
|
||||||
from portprotonqt.game_card import GameCard
|
from portprotonqt.game_card import GameCard
|
||||||
from portprotonqt.config_utils import read_fullscreen_config, read_window_geometry, save_window_geometry, read_auto_fullscreen_gamepad, read_rumble_config
|
from portprotonqt.config_utils import read_fullscreen_config, read_window_geometry, save_window_geometry, read_auto_fullscreen_gamepad, read_rumble_config
|
||||||
from portprotonqt.dialogs import AddGameDialog
|
from portprotonqt.dialogs import AddGameDialog
|
||||||
@ -162,20 +162,13 @@ class InputManager(QObject):
|
|||||||
if not self.file_explorer or not hasattr(self.file_explorer, 'file_list'):
|
if not self.file_explorer or not hasattr(self.file_explorer, 'file_list'):
|
||||||
return
|
return
|
||||||
|
|
||||||
if button_code in BUTTONS['add_game']:
|
focused_widget = QApplication.focusWidget()
|
||||||
if self.file_explorer.file_list.count() == 0:
|
if button_code in BUTTONS['confirm']: # A button (BTN_SOUTH)
|
||||||
|
if isinstance(focused_widget, AutoSizeButton) and hasattr(self.file_explorer, 'drive_buttons') and focused_widget in self.file_explorer.drive_buttons:
|
||||||
|
self.file_explorer.select_drive() # Select the focused drive
|
||||||
|
elif self.file_explorer.file_list.count() == 0:
|
||||||
return
|
return
|
||||||
selected = self.file_explorer.file_list.currentItem().text()
|
|
||||||
full_path = os.path.join(self.file_explorer.current_path, selected)
|
|
||||||
if os.path.isdir(full_path):
|
|
||||||
# Подтверждаем выбор директории
|
|
||||||
self.file_explorer.file_signal.file_selected.emit(os.path.normpath(full_path))
|
|
||||||
self.file_explorer.accept()
|
|
||||||
else:
|
else:
|
||||||
logger.debug("Selected item is not a directory: %s", full_path)
|
|
||||||
elif button_code in BUTTONS['confirm']:
|
|
||||||
if self.file_explorer.file_list.count() == 0:
|
|
||||||
return
|
|
||||||
selected = self.file_explorer.file_list.currentItem().text()
|
selected = self.file_explorer.file_list.currentItem().text()
|
||||||
full_path = os.path.join(self.file_explorer.current_path, selected)
|
full_path = os.path.join(self.file_explorer.current_path, selected)
|
||||||
if os.path.isdir(full_path):
|
if os.path.isdir(full_path):
|
||||||
@ -188,9 +181,20 @@ class InputManager(QObject):
|
|||||||
self.file_explorer.accept()
|
self.file_explorer.accept()
|
||||||
else:
|
else:
|
||||||
logger.debug("Selected item is not a directory, cannot select: %s", full_path)
|
logger.debug("Selected item is not a directory, cannot select: %s", full_path)
|
||||||
elif button_code in BUTTONS['back']:
|
elif button_code in BUTTONS['add_game']: # X button
|
||||||
|
if self.file_explorer.file_list.count() == 0:
|
||||||
|
return
|
||||||
|
selected = self.file_explorer.file_list.currentItem().text()
|
||||||
|
full_path = os.path.join(self.file_explorer.current_path, selected)
|
||||||
|
if os.path.isdir(full_path):
|
||||||
|
# Подтверждаем выбор директории
|
||||||
|
self.file_explorer.file_signal.file_selected.emit(os.path.normpath(full_path))
|
||||||
|
self.file_explorer.accept()
|
||||||
|
else:
|
||||||
|
logger.debug("Selected item is not a directory: %s", full_path)
|
||||||
|
elif button_code in BUTTONS['back']: # B button
|
||||||
self.file_explorer.close()
|
self.file_explorer.close()
|
||||||
elif button_code in BUTTONS['prev_dir']:
|
elif button_code in BUTTONS['prev_dir']: # Y button
|
||||||
self.file_explorer.previous_dir()
|
self.file_explorer.previous_dir()
|
||||||
else:
|
else:
|
||||||
if self.original_button_handler:
|
if self.original_button_handler:
|
||||||
@ -204,15 +208,33 @@ class InputManager(QObject):
|
|||||||
if not self.file_explorer or not hasattr(self.file_explorer, 'file_list') or not self.file_explorer.file_list:
|
if not self.file_explorer or not hasattr(self.file_explorer, 'file_list') or not self.file_explorer.file_list:
|
||||||
return
|
return
|
||||||
|
|
||||||
if not self.file_explorer.file_list.count():
|
focused_widget = QApplication.focusWidget()
|
||||||
|
if code in (ecodes.ABS_HAT0X, ecodes.ABS_X) and hasattr(self.file_explorer, 'drive_buttons') and self.file_explorer.drive_buttons:
|
||||||
|
# Navigate drive buttons horizontally
|
||||||
|
if not isinstance(focused_widget, AutoSizeButton) or focused_widget not in self.file_explorer.drive_buttons:
|
||||||
|
# If not focused on a drive button, focus the first one
|
||||||
|
self.file_explorer.drive_buttons[0].setFocus()
|
||||||
|
return
|
||||||
|
current_idx = self.file_explorer.drive_buttons.index(focused_widget)
|
||||||
|
if value < 0: # Left
|
||||||
|
next_idx = max(current_idx - 1, 0)
|
||||||
|
self.file_explorer.drive_buttons[next_idx].setFocus()
|
||||||
|
elif value > 0: # Right
|
||||||
|
next_idx = min(current_idx + 1, len(self.file_explorer.drive_buttons) - 1)
|
||||||
|
self.file_explorer.drive_buttons[next_idx].setFocus()
|
||||||
|
elif code in (ecodes.ABS_HAT0Y, ecodes.ABS_Y):
|
||||||
|
if isinstance(focused_widget, AutoSizeButton) and focused_widget in self.file_explorer.drive_buttons:
|
||||||
|
# Move focus to file list if navigating down from drive buttons
|
||||||
|
if value > 0 and self.file_explorer.file_list.count() > 0:
|
||||||
|
self.file_explorer.file_list.setFocus()
|
||||||
|
self.file_explorer.file_list.setCurrentRow(0)
|
||||||
|
self.file_explorer.file_list.scrollToItem(self.file_explorer.file_list.currentItem())
|
||||||
return
|
return
|
||||||
|
|
||||||
if code in (ecodes.ABS_HAT0Y, ecodes.ABS_Y):
|
|
||||||
# Для D-pad - реакция с фиксированной скоростью
|
# Для D-pad - реакция с фиксированной скоростью
|
||||||
if code == ecodes.ABS_HAT0Y:
|
if code == ecodes.ABS_HAT0Y:
|
||||||
if value != 0:
|
if value != 0:
|
||||||
self.current_direction = value
|
self.current_direction = value
|
||||||
self.stick_value = 1.0 # Максимальная скорость для D-pad, чтобы скачков не было
|
self.stick_value = 1.0 # Максимальная скорость для D-pad
|
||||||
if not self.nav_timer.isActive():
|
if not self.nav_timer.isActive():
|
||||||
self.file_explorer.move_selection(self.current_direction)
|
self.file_explorer.move_selection(self.current_direction)
|
||||||
self.last_nav_time = current_time
|
self.last_nav_time = current_time
|
||||||
@ -220,7 +242,6 @@ class InputManager(QObject):
|
|||||||
else:
|
else:
|
||||||
self.current_direction = 0
|
self.current_direction = 0
|
||||||
self.nav_timer.stop()
|
self.nav_timer.stop()
|
||||||
|
|
||||||
# Для стика - плавное управление с учетом степени отклонения
|
# Для стика - плавное управление с учетом степени отклонения
|
||||||
elif code == ecodes.ABS_Y:
|
elif code == ecodes.ABS_Y:
|
||||||
if abs(value) < self.dead_zone:
|
if abs(value) < self.dead_zone:
|
||||||
@ -229,19 +250,15 @@ class InputManager(QObject):
|
|||||||
self.nav_timer.stop()
|
self.nav_timer.stop()
|
||||||
self.stick_activated = False
|
self.stick_activated = False
|
||||||
return
|
return
|
||||||
|
|
||||||
# Рассчитываем "силу" отклонения (0.3 - 1.0)
|
|
||||||
normalized_value = (abs(value) - self.dead_zone) / (32768 - self.dead_zone)
|
normalized_value = (abs(value) - self.dead_zone) / (32768 - self.dead_zone)
|
||||||
speed_factor = 0.3 + (normalized_value * 0.7) # От 30% до 100% скорости
|
speed_factor = 0.3 + (normalized_value * 0.7) # От 30% до 100% скорости
|
||||||
self.current_direction = -1 if value < 0 else 1
|
self.current_direction = -1 if value < 0 else 1
|
||||||
self.stick_value = speed_factor
|
self.stick_value = speed_factor
|
||||||
self.stick_activated = True
|
self.stick_activated = True
|
||||||
|
|
||||||
if not self.nav_timer.isActive():
|
if not self.nav_timer.isActive():
|
||||||
self.file_explorer.move_selection(self.current_direction)
|
self.file_explorer.move_selection(self.current_direction)
|
||||||
self.last_nav_time = current_time
|
self.last_nav_time = current_time
|
||||||
self.nav_timer.start(int(self.initial_nav_delay * 1000))
|
self.nav_timer.start(int(self.initial_nav_delay * 1000))
|
||||||
|
|
||||||
elif self.original_dpad_handler:
|
elif self.original_dpad_handler:
|
||||||
self.original_dpad_handler(code, value, current_time)
|
self.original_dpad_handler(code, value, current_time)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
Reference in New Issue
Block a user