From 1cf93a60c896103e236120a3ebadbc92e41fc798 Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Sun, 24 Aug 2025 12:15:06 +0500 Subject: [PATCH] feat: added favorites to file explorer Signed-off-by: Boris Yumankulov --- portprotonqt/config_utils.py | 38 ++++++++ portprotonqt/context_menu_manager.py | 126 +++++++++++++++++++++------ portprotonqt/dialogs.py | 104 ++++++++++++++++------ portprotonqt/input_manager.py | 55 ++++++++++-- 4 files changed, 264 insertions(+), 59 deletions(-) diff --git a/portprotonqt/config_utils.py b/portprotonqt/config_utils.py index 2066d2c..6e6cab6 100644 --- a/portprotonqt/config_utils.py +++ b/portprotonqt/config_utils.py @@ -549,3 +549,41 @@ def save_auto_fullscreen_gamepad(auto_fullscreen): cp["Display"]["auto_fullscreen_gamepad"] = str(auto_fullscreen) with open(CONFIG_FILE, "w", encoding="utf-8") as configfile: cp.write(configfile) + +def read_favorite_folders(): + """ + Читает список избранных папок из секции [FavoritesFolders] конфигурационного файла. + Список хранится как строка, заключённая в кавычки, с путями, разделёнными запятыми. + Если секция или параметр отсутствуют, возвращает пустой список. + """ + cp = configparser.ConfigParser() + if os.path.exists(CONFIG_FILE): + try: + cp.read(CONFIG_FILE, encoding="utf-8") + except Exception as e: + logger.error("Ошибка чтения конфига: %s", e) + return [] + if cp.has_section("FavoritesFolders") and cp.has_option("FavoritesFolders", "folders"): + favs = cp.get("FavoritesFolders", "folders", fallback="").strip() + if favs.startswith('"') and favs.endswith('"'): + favs = favs[1:-1] + return [os.path.normpath(s.strip()) for s in favs.split(",") if s.strip() and os.path.isdir(os.path.normpath(s.strip()))] + return [] + +def save_favorite_folders(folders): + """ + Сохраняет список избранных папок в секцию [FavoritesFolders] конфигурационного файла. + Список сохраняется как строка, заключённая в двойные кавычки, где пути разделены запятыми. + """ + cp = configparser.ConfigParser() + if os.path.exists(CONFIG_FILE): + try: + cp.read(CONFIG_FILE, encoding="utf-8") + except Exception as e: + logger.error("Ошибка чтения конфига: %s", e) + if "FavoritesFolders" not in cp: + cp["FavoritesFolders"] = {} + fav_str = ", ".join([os.path.normpath(folder) for folder in folders]) + cp["FavoritesFolders"]["folders"] = f'"{fav_str}"' + with open(CONFIG_FILE, "w", encoding="utf-8") as configfile: + cp.write(configfile) diff --git a/portprotonqt/context_menu_manager.py b/portprotonqt/context_menu_manager.py index 4e3e4dd..89cf4a1 100644 --- a/portprotonqt/context_menu_manager.py +++ b/portprotonqt/context_menu_manager.py @@ -12,7 +12,7 @@ from PySide6.QtWidgets import QMessageBox, QDialog, QMenu, QLineEdit, QApplicati from PySide6.QtCore import QUrl, QPoint, QObject, Signal, Qt from PySide6.QtGui import QDesktopServices, QIcon, QKeySequence from portprotonqt.localization import _ -from portprotonqt.config_utils import parse_desktop_entry, read_favorites, save_favorites +from portprotonqt.config_utils import parse_desktop_entry, read_favorites, save_favorites, read_favorite_folders, save_favorite_folders from portprotonqt.steam_api import is_game_in_steam, add_to_steam, remove_from_steam from portprotonqt.egs_api import add_egs_to_steam, get_egs_executable, remove_egs_from_steam from portprotonqt.dialogs import AddGameDialog, FileExplorer, generate_thumbnail @@ -150,6 +150,84 @@ class ContextMenuManager: return hasattr(self.parent, 'target_exe') and self.parent.target_exe == current_exe + def show_folder_context_menu(self, file_explorer, pos): + """Shows the context menu for a folder in FileExplorer.""" + try: + item = file_explorer.file_list.itemAt(pos) + if not item: + logger.debug("No item selected at position %s", pos) + return + selected = item.text() + if not selected.endswith("/"): + logger.debug("Selected item is not a folder: %s", selected) + return # Only for folders + full_path = os.path.normpath(os.path.join(file_explorer.current_path, selected.rstrip("/"))) + if not os.path.isdir(full_path): + logger.debug("Path is not a directory: %s", full_path) + return + + menu = QMenu(file_explorer) + menu.setStyleSheet(self.theme.CONTEXT_MENU_STYLE) + menu.setParent(file_explorer, Qt.WindowType.Popup) # Set transientParent for Wayland + + favorite_folders = read_favorite_folders() + is_favorite = full_path in favorite_folders + action_text = _("Remove from Favorites") if is_favorite else _("Add to Favorites") + favorite_action = menu.addAction(self._get_safe_icon("star" if is_favorite else "star_full"), action_text) + favorite_action.triggered.connect(lambda: self.toggle_favorite_folder(file_explorer, full_path, not is_favorite)) + + # Disconnect file_list signals to prevent navigation during menu interaction + try: + file_explorer.file_list.itemClicked.disconnect(file_explorer.handle_item_click) + file_explorer.file_list.itemDoubleClicked.disconnect(file_explorer.handle_item_double_click) + except TypeError: + pass # Signals may not be connected + + # Reconnect signals after menu closes + def reconnect_signals(): + try: + file_explorer.file_list.itemClicked.connect(file_explorer.handle_item_click) + file_explorer.file_list.itemDoubleClicked.connect(file_explorer.handle_item_double_click) + except Exception as e: + logger.error("Error reconnecting file list signals: %s", e) + + menu.aboutToHide.connect(reconnect_signals) + + # Set focus to the first menu item + actions = menu.actions() + if actions: + menu.setActiveAction(actions[0]) + + # Map local position to global for menu display + global_pos = file_explorer.file_list.mapToGlobal(pos) + menu.exec(global_pos) + except Exception as e: + logger.error("Error showing folder context menu: %s", e) + + def toggle_favorite_folder(self, file_explorer, folder_path, add): + """Adds or removes a folder from favorites.""" + favorite_folders = read_favorite_folders() + if add: + if folder_path not in favorite_folders: + favorite_folders.append(folder_path) + save_favorite_folders(favorite_folders) + logger.info(f"Folder added to favorites: {folder_path}") + else: + if folder_path in favorite_folders: + favorite_folders.remove(folder_path) + save_favorite_folders(favorite_folders) + logger.info(f"Folder removed from favorites: {folder_path}") + file_explorer.update_drives_list() + + def _get_safe_icon(self, icon_name: str) -> QIcon: + """Returns a QIcon, ensuring it is valid.""" + icon = self.theme_manager.get_icon(icon_name) + if isinstance(icon, QIcon): + return icon + elif isinstance(icon, str) and os.path.exists(icon): + return QIcon(icon) + return QIcon() + def show_context_menu(self, game_card, pos: QPoint): """ Show the context menu for a game card at the specified position. @@ -158,14 +236,6 @@ class ContextMenuManager: game_card: The GameCard instance requesting the context menu. pos: The position (in widget coordinates) where the menu should appear. """ - def get_safe_icon(icon_name: str) -> QIcon: - icon = self.theme_manager.get_icon(icon_name) - if isinstance(icon, QIcon): - return icon - elif isinstance(icon, str) and os.path.exists(icon): - return QIcon(icon) - return QIcon() - menu = QMenu(self.parent) menu.setStyleSheet(self.theme.CONTEXT_MENU_STYLE) @@ -175,7 +245,7 @@ class ContextMenuManager: exe_path = self._parse_exe_path(exec_line, game_card.name) if exec_line else None if not exe_path: # Show only "Delete from PortProton" if no valid exe - delete_action = menu.addAction(get_safe_icon("delete"), _("Delete from PortProton")) + delete_action = menu.addAction(self._get_safe_icon("delete"), _("Delete from PortProton")) delete_action.triggered.connect(lambda: self.delete_game(game_card.name, game_card.exec_line)) menu.exec(game_card.mapToGlobal(pos)) return @@ -184,7 +254,7 @@ class ContextMenuManager: is_running = self._is_game_running(game_card) action_text = _("Stop Game") if is_running else _("Launch Game") action_icon = "stop" if is_running else "play" - launch_action = menu.addAction(get_safe_icon(action_icon), action_text) + launch_action = menu.addAction(self._get_safe_icon(action_icon), action_text) launch_action.triggered.connect( lambda: self._launch_game(game_card) ) @@ -193,11 +263,11 @@ class ContextMenuManager: is_favorite = game_card.name in favorites icon_name = "star_full" if is_favorite else "star" text = _("Remove from Favorites") if is_favorite else _("Add to Favorites") - favorite_action = menu.addAction(get_safe_icon(icon_name), text) + favorite_action = menu.addAction(self._get_safe_icon(icon_name), text) favorite_action.triggered.connect(lambda: self.toggle_favorite(game_card, not is_favorite)) if game_card.game_source == "epic": - import_action = menu.addAction(get_safe_icon("epic_games"), _("Import to Legendary")) + import_action = menu.addAction(self._get_safe_icon("epic_games"), _("Import to Legendary")) import_action.triggered.connect( lambda: self.import_to_legendary(game_card.name, game_card.appid) ) @@ -205,13 +275,13 @@ class ContextMenuManager: is_in_steam = is_game_in_steam(game_card.name) icon_name = "delete" if is_in_steam else "steam" text = _("Remove from Steam") if is_in_steam else _("Add to Steam") - steam_action = menu.addAction(get_safe_icon(icon_name), text) + steam_action = menu.addAction(self._get_safe_icon(icon_name), text) steam_action.triggered.connect( lambda: self.remove_from_steam(game_card.name, game_card.exec_line, game_card.game_source) if is_in_steam else self.add_egs_to_steam(game_card.name, game_card.appid) ) - open_folder_action = menu.addAction(get_safe_icon("search"), _("Open Game Folder")) + open_folder_action = menu.addAction(self._get_safe_icon("search"), _("Open Game Folder")) open_folder_action.triggered.connect( lambda: self.open_egs_game_folder(game_card.appid) ) @@ -219,7 +289,7 @@ class ContextMenuManager: desktop_path = os.path.join(desktop_dir, f"{game_card.name}.desktop") icon_name = "delete" if os.path.exists(desktop_path) else "desktop" text = _("Remove from Desktop") if os.path.exists(desktop_path) else _("Add to Desktop") - desktop_action = menu.addAction(get_safe_icon(icon_name), text) + desktop_action = menu.addAction(self._get_safe_icon(icon_name), text) desktop_action.triggered.connect( lambda: self.remove_egs_from_desktop(game_card.name) if os.path.exists(desktop_path) @@ -228,7 +298,7 @@ class ContextMenuManager: applications_dir = os.path.join(os.path.expanduser("~"), ".local", "share", "applications") menu_path = os.path.join(applications_dir, f"{game_card.name}.desktop") menu_action = menu.addAction( - get_safe_icon("delete" if os.path.exists(menu_path) else "menu"), + self._get_safe_icon("delete" if os.path.exists(menu_path) else "menu"), _("Remove from Menu") if os.path.exists(menu_path) else _("Add to Menu") ) menu_action.triggered.connect( @@ -242,19 +312,19 @@ class ContextMenuManager: desktop_path = os.path.join(desktop_dir, f"{game_card.name}.desktop") icon_name = "delete" if os.path.exists(desktop_path) else "desktop" text = _("Remove from Desktop") if os.path.exists(desktop_path) else _("Add to Desktop") - desktop_action = menu.addAction(get_safe_icon(icon_name), text) + desktop_action = menu.addAction(self._get_safe_icon(icon_name), text) desktop_action.triggered.connect( lambda: self.remove_from_desktop(game_card.name) if os.path.exists(desktop_path) else self.add_to_desktop(game_card.name, game_card.exec_line) ) - edit_action = menu.addAction(get_safe_icon("edit"), _("Edit Shortcut")) + edit_action = menu.addAction(self._get_safe_icon("edit"), _("Edit Shortcut")) edit_action.triggered.connect( lambda: self.edit_game_shortcut(game_card.name, game_card.exec_line, game_card.cover_path) ) - delete_action = menu.addAction(get_safe_icon("delete"), _("Delete from PortProton")) + delete_action = menu.addAction(self._get_safe_icon("delete"), _("Delete from PortProton")) delete_action.triggered.connect(lambda: self.delete_game(game_card.name, game_card.exec_line)) - open_folder_action = menu.addAction(get_safe_icon("search"), _("Open Game Folder")) + open_folder_action = menu.addAction(self._get_safe_icon("search"), _("Open Game Folder")) open_folder_action.triggered.connect( lambda: self.open_game_folder(game_card.name, game_card.exec_line) ) @@ -262,7 +332,7 @@ class ContextMenuManager: menu_path = os.path.join(applications_dir, f"{game_card.name}.desktop") icon_name = "delete" if os.path.exists(menu_path) else "menu" text = _("Remove from Menu") if os.path.exists(menu_path) else _("Add to Menu") - menu_action = menu.addAction(get_safe_icon(icon_name), text) + menu_action = menu.addAction(self._get_safe_icon(icon_name), text) menu_action.triggered.connect( lambda: self.remove_from_menu(game_card.name) if os.path.exists(menu_path) @@ -271,7 +341,7 @@ class ContextMenuManager: is_in_steam = is_game_in_steam(game_card.name) icon_name = "delete" if is_in_steam else "steam" text = _("Remove from Steam") if is_in_steam else _("Add to Steam") - steam_action = menu.addAction(get_safe_icon(icon_name), text) + steam_action = menu.addAction(self._get_safe_icon(icon_name), text) steam_action.triggered.connect( lambda: ( self.remove_from_steam(game_card.name, game_card.exec_line, game_card.game_source) @@ -280,7 +350,7 @@ class ContextMenuManager: ) ) - # Устанавливаем фокус на первый элемент меню + # Set focus to the first menu item actions = menu.actions() if actions: menu.setActiveAction(actions[0]) @@ -422,7 +492,7 @@ class ContextMenuManager: ) return - # Используем FileExplorer с directory_only=True + # Use FileExplorer with directory_only=True file_explorer = FileExplorer( parent=self.parent, theme=self.theme, @@ -452,10 +522,10 @@ class ContextMenuManager: self._show_status_message(_("Importing '{game_name}' to Legendary...").format(game_name=game_name)) threading.Thread(target=run_import, daemon=True).start() - # Подключаем сигнал выбора файла/папки + # Connect the file selection signal file_explorer.file_signal.file_selected.connect(on_folder_selected) - # Центрируем FileExplorer относительно родительского виджета + # Center FileExplorer relative to the parent widget parent_widget = self.parent if parent_widget: parent_geometry = parent_widget.geometry() @@ -789,7 +859,7 @@ Icon={icon_path} _("Failed to delete custom data: {error}").format(error=str(e)) ) - # Перезагрузка списка игр и обновление сетки + # Reload games list and update grid self.load_games() self.update_game_grid() diff --git a/portprotonqt/dialogs.py b/portprotonqt/dialogs.py index 0d12418..e4e18e4 100644 --- a/portprotonqt/dialogs.py +++ b/portprotonqt/dialogs.py @@ -9,7 +9,7 @@ from PySide6.QtWidgets import ( from PySide6.QtCore import Qt, QObject, Signal, QMimeDatabase, QTimer from icoextract import IconExtractor, IconExtractorError from PIL import Image -from portprotonqt.config_utils import get_portproton_location +from portprotonqt.config_utils import get_portproton_location, read_favorite_folders from portprotonqt.localization import _ from portprotonqt.logger import get_logger import portprotonqt.themes.standart.styles as default_styles @@ -106,13 +106,15 @@ class FileExplorer(QDialog): self.setWindowModality(Qt.WindowModality.ApplicationModal) self.setWindowFlags(self.windowFlags() & ~Qt.WindowType.WindowContextHelpButtonHint) - # Find InputManager from parent + # Find InputManager and ContextMenuManager from parent self.input_manager = None + self.context_menu_manager = None parent = self.parent() while parent: if hasattr(parent, 'input_manager'): self.input_manager = cast("MainWindow", parent).input_manager - break + if hasattr(parent, 'context_menu_manager'): + self.context_menu_manager = cast("MainWindow", parent).context_menu_manager parent = parent.parent() if self.input_manager: @@ -159,7 +161,7 @@ class FileExplorer(QDialog): self.main_layout.setSpacing(10) self.setLayout(self.main_layout) - # Панель для смонтированных дисков + # Панель для смонтированных дисков и избранных папок self.drives_layout = QHBoxLayout() self.drives_scroll = QScrollArea() self.drives_scroll.setWidgetResizable(True) @@ -170,7 +172,7 @@ class FileExplorer(QDialog): self.drives_scroll.setFixedHeight(70) self.main_layout.addWidget(self.drives_scroll) self.drives_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) - self.drives_scroll.setFocusPolicy(Qt.FocusPolicy.StrongFocus) # Allow focus on scroll area + self.drives_scroll.setFocusPolicy(Qt.FocusPolicy.StrongFocus) # Путь self.path_label = QLabel() @@ -182,6 +184,8 @@ class FileExplorer(QDialog): self.file_list.setStyleSheet(self.theme.FILE_EXPLORER_STYLE) self.file_list.itemClicked.connect(self.handle_item_click) self.file_list.itemDoubleClicked.connect(self.handle_item_double_click) + self.file_list.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) + self.file_list.customContextMenuRequested.connect(self.show_folder_context_menu) self.main_layout.addWidget(self.file_list) # Кнопки @@ -198,6 +202,13 @@ class FileExplorer(QDialog): self.select_button.clicked.connect(self.select_item) self.cancel_button.clicked.connect(self.reject) + def show_folder_context_menu(self, pos): + """Shows the context menu for a folder using ContextMenuManager.""" + if self.context_menu_manager: + self.context_menu_manager.show_folder_context_menu(self, pos) + else: + logger.warning("ContextMenuManager not found in parent") + def move_selection(self, direction): """Перемещение выбора по списку""" current_row = self.file_list.currentRow() @@ -288,43 +299,84 @@ class FileExplorer(QDialog): logger.error(f"Error navigating to parent directory: {e}") def update_drives_list(self): - """Обновление списка смонтированных дисков""" + """Обновление списка смонтированных дисков и избранных папок.""" for i in reversed(range(self.drives_layout.count())): - widget = self.drives_layout.itemAt(i).widget() - if widget: + item = self.drives_layout.itemAt(i) + if item and item.widget(): + widget = item.widget() + self.drives_layout.removeWidget(widget) widget.deleteLater() + self.drive_buttons = [] drives = self.get_mounted_drives() - self.drive_buttons = [] # Store buttons for navigation + favorite_folders = read_favorite_folders() + + # Добавляем смонтированные диски for drive in drives: 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.setStyleSheet(self.theme.ACTION_BUTTON_STYLE) - button.setFocusPolicy(Qt.FocusPolicy.StrongFocus) # Make button focusable + button.setFocusPolicy(Qt.FocusPolicy.StrongFocus) button.clicked.connect(lambda checked, path=drive: self.change_drive(path)) self.drives_layout.addWidget(button) self.drive_buttons.append(button) - self.drives_layout.addStretch() - # Set focus to first drive button if available - if self.drive_buttons: - self.drive_buttons[0].setFocus() + # Добавляем избранные папки + for folder in favorite_folders: + folder_name = os.path.basename(folder) or folder.split('/')[-1] or folder + button = AutoSizeButton(folder_name, icon=self.theme_manager.get_icon("folder")) + button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE) + button.setFocusPolicy(Qt.FocusPolicy.StrongFocus) + button.clicked.connect(lambda checked, path=folder: self.change_drive(path)) + self.drives_layout.addWidget(button) + self.drive_buttons.append(button) + + # Добавляем растяжку, чтобы выровнять элементы + spacer = QWidget() + spacer.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred) + self.drives_layout.addWidget(spacer) 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}") + drive_name = focused_widget.text().strip() # Удаляем пробелы + logger.debug(f"Выбрано имя: {drive_name}") + + # Специальная обработка корневого каталога + if drive_name == "/": + if os.path.isdir("/") and os.access("/", os.R_OK): + self.current_path = "/" + self.update_file_list() + logger.info("Выбран корневой каталог: /") + return + else: + logger.warning("Корневой каталог недоступен: недостаточно прав или ошибка пути") + return + + # Проверяем избранные папки + favorite_folders = read_favorite_folders() + logger.debug(f"Избранные папки: {favorite_folders}") + for folder in favorite_folders: + folder_name = os.path.basename(os.path.normpath(folder)) or folder # Для корневых путей + if folder_name == drive_name and os.path.isdir(folder) and os.access(folder, os.R_OK): + self.current_path = os.path.normpath(folder) + self.update_file_list() + logger.info(f"Выбрана избранная папка: {self.current_path}") + return + + # Проверяем смонтированные диски + mounted_drives = self.get_mounted_drives() + logger.debug(f"Смонтированные диски: {mounted_drives}") + for drive in mounted_drives: + drive_basename = os.path.basename(os.path.normpath(drive)) or drive # Для корневых путей + if drive_basename == drive_name and os.path.isdir(drive) and os.access(drive, os.R_OK): + self.current_path = os.path.normpath(drive) + self.update_file_list() + logger.info(f"Выбран смонтированный диск: {self.current_path}") + return + + logger.warning(f"Путь недоступен: {drive_name}.") def change_drive(self, drive_path): """Переход к выбранному диску""" diff --git a/portprotonqt/input_manager.py b/portprotonqt/input_manager.py index 6dcbf38..0a45f78 100644 --- a/portprotonqt/input_manager.py +++ b/portprotonqt/input_manager.py @@ -161,7 +161,20 @@ class InputManager(QObject): def handle_file_explorer_button(self, button_code): try: + popup = QApplication.activePopupWidget() + if isinstance(popup, QMenu): + if button_code in BUTTONS['confirm']: # A button (BTN_SOUTH) + if popup.activeAction(): + popup.activeAction().trigger() + popup.close() + return + elif button_code in BUTTONS['back']: # B button + popup.close() + return + return # Skip other handling if menu is open + if not self.file_explorer or not hasattr(self.file_explorer, 'file_list'): + logger.debug("No file explorer or file_list available") return focused_widget = QApplication.focusWidget() @@ -169,27 +182,37 @@ class InputManager(QObject): 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: + logger.debug("File list is empty") return else: 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.current_path = os.path.normpath(full_path) self.file_explorer.update_file_list() elif not self.file_explorer.directory_only: - # Выбираем файл, если directory_only=False 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, cannot select: %s", full_path) + elif button_code in BUTTONS['context_menu']: # Start button (BTN_START) + if self.file_explorer.file_list.count() == 0: + logger.debug("File list is empty, cannot show context menu") + return + current_item = self.file_explorer.file_list.currentItem() + if current_item: + item_rect = self.file_explorer.file_list.visualItemRect(current_item) + pos = item_rect.center() # Use local coordinates for itemAt check + self.file_explorer.show_folder_context_menu(pos) + else: + logger.debug("No item selected for context menu") elif button_code in BUTTONS['add_game']: # X button if self.file_explorer.file_list.count() == 0: + logger.debug("File list is empty") 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: @@ -202,12 +225,29 @@ class InputManager(QObject): if self.original_button_handler: self.original_button_handler(button_code) except Exception as e: - logger.error(f"Error in FileExplorer button handler: {e}") + logger.error("Error in FileExplorer button handler: %s", e) def handle_file_explorer_dpad(self, code, value, current_time): """Обработка движения D-pad и левого стика для FileExplorer""" try: + popup = QApplication.activePopupWidget() + if isinstance(popup, QMenu): + if code == ecodes.ABS_HAT0Y and value != 0: + actions = popup.actions() + if not actions: + return + current_action = popup.activeAction() + current_idx = actions.index(current_action) if current_action in actions else -1 + if value > 0: # Down + next_idx = (current_idx + 1) % len(actions) if current_idx != -1 else 0 + popup.setActiveAction(actions[next_idx]) + elif value < 0: # Up + next_idx = (current_idx - 1) % len(actions) if current_idx != -1 else len(actions) - 1 + popup.setActiveAction(actions[next_idx]) + return # Skip other handling if menu is open + if not self.file_explorer or not hasattr(self.file_explorer, 'file_list') or not self.file_explorer.file_list: + logger.debug("No file explorer or file_list available") return focused_widget = QApplication.focusWidget() @@ -264,7 +304,7 @@ class InputManager(QObject): 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}") + logger.error("Error in FileExplorer dpad handler: %s", e) def handle_navigation_repeat(self): """Плавное повторение движения с переменной скоростью для FileExplorer""" @@ -742,6 +782,11 @@ class InputManager(QObject): if not app: return super().eventFilter(obj, event) + # Ensure obj is a QObject + if not isinstance(obj, QObject): + logger.debug(f"Skipping event filter for non-QObject: {type(obj).__name__}") + return False + # Handle key press and release events if not isinstance(event, QKeyEvent): return super().eventFilter(obj, event)