Compare commits
12 Commits
uv-manual
...
renovate/p
Author | SHA1 | Date | |
---|---|---|---|
|
cb07904c1b | ||
05e0d9d846
|
|||
81433d3c56
|
|||
0ff66e282b
|
|||
831b7739ba
|
|||
50e1dfda57
|
|||
fcf04e521d
|
|||
74d0700d7c
|
|||
0435c77630
|
|||
1cf93a60c8
|
|||
31247d21c3
|
|||
c6017a7dce
|
@@ -20,7 +20,7 @@ jobs:
|
||||
- name: Install uv manually
|
||||
run: |
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
source $HOME/.local/bin/env
|
||||
. $HOME/.local/bin/env
|
||||
uv --version
|
||||
|
||||
- name: Download external renovate config
|
||||
|
@@ -9,6 +9,10 @@
|
||||
- Больше типов анимаций при открытии карточки игры (подробности см. в документации).
|
||||
- Анимация при закрытии карточки игры (подробности см. в документации).
|
||||
- Добавлен обработчик нажатий стрелок на клавиатуре в поле ввода (позволяет перемещаться между символами с помощью стрелок).
|
||||
- Система быстрого доступа (избранного) в диалоге выбора файлов.
|
||||
- Автоматическая прокрутка для панели дисков в диалоге выбора файлов.
|
||||
- Возможность выбора папок и / или дисков в диалоге выбора файлов через клавиатуру.
|
||||
- Поднятия в родительскую директорию в диалоге выбора файлов на BackSpace.
|
||||
|
||||
### Changed
|
||||
- Уменьшена длительность анимации открытия карточки с 800 до 350 мс.
|
||||
@@ -28,6 +32,8 @@
|
||||
- Вкладки больше не переключаются стрелками, если фокус в поле ввода.
|
||||
- Исправлено переключение слайдера: RT (Xbox) / R2 (PS), LT (Xbox) / L2 (PS).
|
||||
- Переведен заголовок окна диалога выбора файлов.
|
||||
- Отображение устройств смонтированных в /run/media в диалоге выбора файлов.
|
||||
- Закрытие диалога добавления / редактирования игры и диалога выбора файлов через escape.
|
||||
|
||||
### Contributors
|
||||
- @Alex Smith
|
||||
|
@@ -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)
|
||||
|
@@ -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()
|
||||
|
||||
|
@@ -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:
|
||||
@@ -137,8 +139,9 @@ class FileExplorer(QDialog):
|
||||
if len(parts) < 2:
|
||||
continue
|
||||
mount_point = parts[1]
|
||||
# Исключаем системные и временные пути
|
||||
if mount_point.startswith(('/run', '/dev', '/sys', '/proc', '/tmp', '/snap', '/var/lib')):
|
||||
# Исключаем системные и временные пути, но сохраняем /run/media
|
||||
if (mount_point.startswith(('/dev', '/sys', '/proc', '/tmp', '/snap', '/var/lib')) or
|
||||
(mount_point.startswith('/run') and not mount_point.startswith('/run/media'))):
|
||||
continue
|
||||
# Проверяем, является ли точка монтирования директорией и доступна ли она
|
||||
if os.path.isdir(mount_point) and os.access(mount_point, os.R_OK):
|
||||
@@ -158,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)
|
||||
@@ -169,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()
|
||||
@@ -181,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)
|
||||
|
||||
# Кнопки
|
||||
@@ -197,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()
|
||||
@@ -286,44 +298,96 @@ class FileExplorer(QDialog):
|
||||
except Exception as e:
|
||||
logger.error(f"Error navigating to parent directory: {e}")
|
||||
|
||||
def ensure_button_visible(self, button):
|
||||
"""Ensure the specified button is visible in the drives_scroll area."""
|
||||
try:
|
||||
if not button or not self.drives_scroll:
|
||||
return
|
||||
# Ensure the button is visible in the scroll area
|
||||
self.drives_scroll.ensureWidgetVisible(button, 50, 50)
|
||||
logger.debug(f"Ensured button {button.text()} is visible in drives_scroll")
|
||||
except Exception as e:
|
||||
logger.error(f"Error ensuring button visible: {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)
|
||||
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(f"Путь диска недоступен: {drive_path}")
|
||||
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):
|
||||
"""Переход к выбранному диску"""
|
||||
|
@@ -4,7 +4,7 @@ import os
|
||||
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
|
||||
@@ -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()
|
||||
@@ -216,14 +256,17 @@ class InputManager(QObject):
|
||||
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()
|
||||
self.file_explorer.ensure_button_visible(self.file_explorer.drive_buttons[0])
|
||||
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()
|
||||
self.file_explorer.ensure_button_visible(self.file_explorer.drive_buttons[next_idx])
|
||||
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()
|
||||
self.file_explorer.ensure_button_visible(self.file_explorer.drive_buttons[next_idx])
|
||||
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
|
||||
@@ -264,7 +307,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 +785,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)
|
||||
@@ -754,6 +802,54 @@ class InputManager(QObject):
|
||||
|
||||
# Handle key press events
|
||||
if event.type() == QEvent.Type.KeyPress:
|
||||
# Handle FileExplorer specific logic
|
||||
if self.file_explorer:
|
||||
# Handle drive buttons in FileExplorer
|
||||
if key in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
|
||||
if isinstance(focused, AutoSizeButton) and hasattr(self.file_explorer, 'drive_buttons') and focused in self.file_explorer.drive_buttons:
|
||||
self.file_explorer.select_drive()
|
||||
return True
|
||||
elif isinstance(focused, QListWidget) and focused == self.file_explorer.file_list:
|
||||
current_item = focused.currentItem()
|
||||
if current_item:
|
||||
selected = current_item.text()
|
||||
full_path = os.path.join(self.file_explorer.current_path, selected)
|
||||
if os.path.isdir(full_path):
|
||||
if selected == "../":
|
||||
self.file_explorer.previous_dir()
|
||||
else:
|
||||
self.file_explorer.current_path = os.path.normpath(full_path)
|
||||
self.file_explorer.update_file_list()
|
||||
elif not self.file_explorer.directory_only:
|
||||
self.file_explorer.file_signal.file_selected.emit(os.path.normpath(full_path))
|
||||
self.file_explorer.accept()
|
||||
return True
|
||||
else:
|
||||
self._parent.activateFocusedWidget()
|
||||
return True
|
||||
|
||||
# Handle FileExplorer navigation with right arrow key
|
||||
if key == Qt.Key.Key_Right:
|
||||
try:
|
||||
if hasattr(self.file_explorer, 'drive_buttons') and self.file_explorer.drive_buttons:
|
||||
if not isinstance(focused, AutoSizeButton) or focused not in self.file_explorer.drive_buttons:
|
||||
self.file_explorer.drive_buttons[0].setFocus()
|
||||
self.file_explorer.ensure_button_visible(self.file_explorer.drive_buttons[0])
|
||||
else:
|
||||
current_idx = self.file_explorer.drive_buttons.index(focused)
|
||||
next_idx = min(current_idx + 1, len(self.file_explorer.drive_buttons) - 1)
|
||||
self.file_explorer.drive_buttons[next_idx].setFocus()
|
||||
self.file_explorer.ensure_button_visible(self.file_explorer.drive_buttons[next_idx])
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling right arrow in FileExplorer: {e}")
|
||||
return True
|
||||
|
||||
# Handle Backspace for FileExplorer navigation
|
||||
if key == Qt.Key.Key_Backspace:
|
||||
self.file_explorer.previous_dir()
|
||||
return True
|
||||
|
||||
# Handle QLineEdit cursor movement with Left/Right arrows
|
||||
if isinstance(focused, QLineEdit) and key in (Qt.Key.Key_Left, Qt.Key.Key_Right):
|
||||
if key == Qt.Key.Key_Left:
|
||||
@@ -778,9 +874,12 @@ class InputManager(QObject):
|
||||
self.file_explorer.previous_dir()
|
||||
return True
|
||||
|
||||
# Close AddGameDialog with Escape
|
||||
if key == Qt.Key.Key_Escape and isinstance(popup, QDialog):
|
||||
popup.reject()
|
||||
# Close Dialogs with Escape
|
||||
if key == Qt.Key.Key_Escape:
|
||||
if isinstance(focused, QLineEdit):
|
||||
return False
|
||||
if isinstance(active_win, QDialog):
|
||||
active_win.reject()
|
||||
return True
|
||||
|
||||
# FullscreenDialog navigation
|
||||
@@ -797,7 +896,7 @@ class InputManager(QObject):
|
||||
return True # Consume event to prevent tab switching
|
||||
|
||||
# Handle tab switching with Left/Right arrow keys when not in GameCard focus or QLineEdit
|
||||
if key in (Qt.Key.Key_Left, Qt.Key.Key_Right) and (not isinstance(focused, GameCard | QLineEdit) or focused is None):
|
||||
if key in (Qt.Key.Key_Left, Qt.Key.Key_Right) and not isinstance(focused, GameCard | QLineEdit) and not self.file_explorer:
|
||||
idx = self._parent.stackedWidget.currentIndex()
|
||||
total = len(self._parent.tabButtons)
|
||||
if key == Qt.Key.Key_Left:
|
||||
|
@@ -27,7 +27,7 @@ classifiers = [
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"babel>=2.17.0",
|
||||
"beautifulsoup4>=4.13.4",
|
||||
"beautifulsoup4>=4.13.5",
|
||||
"evdev>=1.9.2",
|
||||
"icoextract>=0.2.0",
|
||||
"numpy>=2.2.4",
|
||||
@@ -36,7 +36,7 @@ dependencies = [
|
||||
"psutil>=7.0.0",
|
||||
"pyside6>=6.9.1",
|
||||
"pyudev>=0.24.3",
|
||||
"requests>=2.32.4",
|
||||
"requests>=2.32.5",
|
||||
"tqdm>=4.67.1",
|
||||
"vdf>=3.4",
|
||||
"websocket-client>=1.8.0",
|
||||
@@ -105,5 +105,5 @@ ignore = [
|
||||
dev = [
|
||||
"pre-commit>=4.3.0",
|
||||
"pyaspeller>=2.0.2",
|
||||
"pyright>=1.1.403",
|
||||
"pyright>=1.1.404",
|
||||
]
|
||||
|
@@ -19,7 +19,7 @@
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"matchManagers": ["github-actions", "pre-commit"],
|
||||
"matchManagers": ["github-actions", "pre-commit", "poetry"],
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
|
Reference in New Issue
Block a user