Compare commits
13 Commits
Author | SHA1 | Date | |
---|---|---|---|
31a7ef3e7e
|
|||
|
cb07904c1b | ||
05e0d9d846
|
|||
81433d3c56
|
|||
0ff66e282b
|
|||
831b7739ba
|
|||
50e1dfda57
|
|||
fcf04e521d
|
|||
74d0700d7c
|
|||
0435c77630
|
|||
1cf93a60c8
|
|||
31247d21c3
|
|||
c6017a7dce
|
@@ -20,7 +20,7 @@ jobs:
|
|||||||
- name: Install uv manually
|
- name: Install uv manually
|
||||||
run: |
|
run: |
|
||||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||||
source $HOME/.local/bin/env
|
. $HOME/.local/bin/env
|
||||||
uv --version
|
uv --version
|
||||||
|
|
||||||
- name: Download external renovate config
|
- name: Download external renovate config
|
||||||
|
@@ -9,6 +9,10 @@
|
|||||||
- Больше типов анимаций при открытии карточки игры (подробности см. в документации).
|
- Больше типов анимаций при открытии карточки игры (подробности см. в документации).
|
||||||
- Анимация при закрытии карточки игры (подробности см. в документации).
|
- Анимация при закрытии карточки игры (подробности см. в документации).
|
||||||
- Добавлен обработчик нажатий стрелок на клавиатуре в поле ввода (позволяет перемещаться между символами с помощью стрелок).
|
- Добавлен обработчик нажатий стрелок на клавиатуре в поле ввода (позволяет перемещаться между символами с помощью стрелок).
|
||||||
|
- Система быстрого доступа (избранного) в диалоге выбора файлов.
|
||||||
|
- Автоматическая прокрутка для панели дисков в диалоге выбора файлов.
|
||||||
|
- Возможность выбора папок и / или дисков в диалоге выбора файлов через клавиатуру.
|
||||||
|
- Поднятия в родительскую директорию в диалоге выбора файлов на BackSpace.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Уменьшена длительность анимации открытия карточки с 800 до 350 мс.
|
- Уменьшена длительность анимации открытия карточки с 800 до 350 мс.
|
||||||
@@ -28,6 +32,8 @@
|
|||||||
- Вкладки больше не переключаются стрелками, если фокус в поле ввода.
|
- Вкладки больше не переключаются стрелками, если фокус в поле ввода.
|
||||||
- Исправлено переключение слайдера: RT (Xbox) / R2 (PS), LT (Xbox) / L2 (PS).
|
- Исправлено переключение слайдера: RT (Xbox) / R2 (PS), LT (Xbox) / L2 (PS).
|
||||||
- Переведен заголовок окна диалога выбора файлов.
|
- Переведен заголовок окна диалога выбора файлов.
|
||||||
|
- Отображение устройств смонтированных в /run/media в диалоге выбора файлов.
|
||||||
|
- Закрытие диалога добавления / редактирования игры и диалога выбора файлов через escape.
|
||||||
|
|
||||||
### Contributors
|
### Contributors
|
||||||
- @Alex Smith
|
- @Alex Smith
|
||||||
|
@@ -549,3 +549,41 @@ def save_auto_fullscreen_gamepad(auto_fullscreen):
|
|||||||
cp["Display"]["auto_fullscreen_gamepad"] = str(auto_fullscreen)
|
cp["Display"]["auto_fullscreen_gamepad"] = str(auto_fullscreen)
|
||||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||||
cp.write(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.QtCore import QUrl, QPoint, QObject, Signal, Qt
|
||||||
from PySide6.QtGui import QDesktopServices, QIcon, QKeySequence
|
from PySide6.QtGui import QDesktopServices, QIcon, QKeySequence
|
||||||
from portprotonqt.localization import _
|
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.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.egs_api import add_egs_to_steam, get_egs_executable, remove_egs_from_steam
|
||||||
from portprotonqt.dialogs import AddGameDialog, FileExplorer, generate_thumbnail
|
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
|
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):
|
def show_context_menu(self, game_card, pos: QPoint):
|
||||||
"""
|
"""
|
||||||
Show the context menu for a game card at the specified position.
|
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.
|
game_card: The GameCard instance requesting the context menu.
|
||||||
pos: The position (in widget coordinates) where the menu should appear.
|
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 = QMenu(self.parent)
|
||||||
menu.setStyleSheet(self.theme.CONTEXT_MENU_STYLE)
|
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
|
exe_path = self._parse_exe_path(exec_line, game_card.name) if exec_line else None
|
||||||
if not exe_path:
|
if not exe_path:
|
||||||
# Show only "Delete from PortProton" if no valid exe
|
# 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))
|
delete_action.triggered.connect(lambda: self.delete_game(game_card.name, game_card.exec_line))
|
||||||
menu.exec(game_card.mapToGlobal(pos))
|
menu.exec(game_card.mapToGlobal(pos))
|
||||||
return
|
return
|
||||||
@@ -184,7 +254,7 @@ class ContextMenuManager:
|
|||||||
is_running = self._is_game_running(game_card)
|
is_running = self._is_game_running(game_card)
|
||||||
action_text = _("Stop Game") if is_running else _("Launch Game")
|
action_text = _("Stop Game") if is_running else _("Launch Game")
|
||||||
action_icon = "stop" if is_running else "play"
|
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(
|
launch_action.triggered.connect(
|
||||||
lambda: self._launch_game(game_card)
|
lambda: self._launch_game(game_card)
|
||||||
)
|
)
|
||||||
@@ -193,11 +263,11 @@ class ContextMenuManager:
|
|||||||
is_favorite = game_card.name in favorites
|
is_favorite = game_card.name in favorites
|
||||||
icon_name = "star_full" if is_favorite else "star"
|
icon_name = "star_full" if is_favorite else "star"
|
||||||
text = _("Remove from Favorites") if is_favorite else _("Add to Favorites")
|
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))
|
favorite_action.triggered.connect(lambda: self.toggle_favorite(game_card, not is_favorite))
|
||||||
|
|
||||||
if game_card.game_source == "epic":
|
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(
|
import_action.triggered.connect(
|
||||||
lambda: self.import_to_legendary(game_card.name, game_card.appid)
|
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)
|
is_in_steam = is_game_in_steam(game_card.name)
|
||||||
icon_name = "delete" if is_in_steam else "steam"
|
icon_name = "delete" if is_in_steam else "steam"
|
||||||
text = _("Remove from Steam") if is_in_steam else _("Add to 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(
|
steam_action.triggered.connect(
|
||||||
lambda: self.remove_from_steam(game_card.name, game_card.exec_line, game_card.game_source)
|
lambda: self.remove_from_steam(game_card.name, game_card.exec_line, game_card.game_source)
|
||||||
if is_in_steam
|
if is_in_steam
|
||||||
else self.add_egs_to_steam(game_card.name, game_card.appid)
|
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(
|
open_folder_action.triggered.connect(
|
||||||
lambda: self.open_egs_game_folder(game_card.appid)
|
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")
|
desktop_path = os.path.join(desktop_dir, f"{game_card.name}.desktop")
|
||||||
icon_name = "delete" if os.path.exists(desktop_path) else "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")
|
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(
|
desktop_action.triggered.connect(
|
||||||
lambda: self.remove_egs_from_desktop(game_card.name)
|
lambda: self.remove_egs_from_desktop(game_card.name)
|
||||||
if os.path.exists(desktop_path)
|
if os.path.exists(desktop_path)
|
||||||
@@ -228,7 +298,7 @@ class ContextMenuManager:
|
|||||||
applications_dir = os.path.join(os.path.expanduser("~"), ".local", "share", "applications")
|
applications_dir = os.path.join(os.path.expanduser("~"), ".local", "share", "applications")
|
||||||
menu_path = os.path.join(applications_dir, f"{game_card.name}.desktop")
|
menu_path = os.path.join(applications_dir, f"{game_card.name}.desktop")
|
||||||
menu_action = menu.addAction(
|
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")
|
_("Remove from Menu") if os.path.exists(menu_path) else _("Add to Menu")
|
||||||
)
|
)
|
||||||
menu_action.triggered.connect(
|
menu_action.triggered.connect(
|
||||||
@@ -242,19 +312,19 @@ class ContextMenuManager:
|
|||||||
desktop_path = os.path.join(desktop_dir, f"{game_card.name}.desktop")
|
desktop_path = os.path.join(desktop_dir, f"{game_card.name}.desktop")
|
||||||
icon_name = "delete" if os.path.exists(desktop_path) else "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")
|
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(
|
desktop_action.triggered.connect(
|
||||||
lambda: self.remove_from_desktop(game_card.name)
|
lambda: self.remove_from_desktop(game_card.name)
|
||||||
if os.path.exists(desktop_path)
|
if os.path.exists(desktop_path)
|
||||||
else self.add_to_desktop(game_card.name, game_card.exec_line)
|
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(
|
edit_action.triggered.connect(
|
||||||
lambda: self.edit_game_shortcut(game_card.name, game_card.exec_line, game_card.cover_path)
|
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))
|
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(
|
open_folder_action.triggered.connect(
|
||||||
lambda: self.open_game_folder(game_card.name, game_card.exec_line)
|
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")
|
menu_path = os.path.join(applications_dir, f"{game_card.name}.desktop")
|
||||||
icon_name = "delete" if os.path.exists(menu_path) else "menu"
|
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")
|
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(
|
menu_action.triggered.connect(
|
||||||
lambda: self.remove_from_menu(game_card.name)
|
lambda: self.remove_from_menu(game_card.name)
|
||||||
if os.path.exists(menu_path)
|
if os.path.exists(menu_path)
|
||||||
@@ -271,7 +341,7 @@ class ContextMenuManager:
|
|||||||
is_in_steam = is_game_in_steam(game_card.name)
|
is_in_steam = is_game_in_steam(game_card.name)
|
||||||
icon_name = "delete" if is_in_steam else "steam"
|
icon_name = "delete" if is_in_steam else "steam"
|
||||||
text = _("Remove from Steam") if is_in_steam else _("Add to 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(
|
steam_action.triggered.connect(
|
||||||
lambda: (
|
lambda: (
|
||||||
self.remove_from_steam(game_card.name, game_card.exec_line, game_card.game_source)
|
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()
|
actions = menu.actions()
|
||||||
if actions:
|
if actions:
|
||||||
menu.setActiveAction(actions[0])
|
menu.setActiveAction(actions[0])
|
||||||
@@ -422,7 +492,7 @@ class ContextMenuManager:
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Используем FileExplorer с directory_only=True
|
# Use FileExplorer with directory_only=True
|
||||||
file_explorer = FileExplorer(
|
file_explorer = FileExplorer(
|
||||||
parent=self.parent,
|
parent=self.parent,
|
||||||
theme=self.theme,
|
theme=self.theme,
|
||||||
@@ -452,10 +522,10 @@ class ContextMenuManager:
|
|||||||
self._show_status_message(_("Importing '{game_name}' to Legendary...").format(game_name=game_name))
|
self._show_status_message(_("Importing '{game_name}' to Legendary...").format(game_name=game_name))
|
||||||
threading.Thread(target=run_import, daemon=True).start()
|
threading.Thread(target=run_import, daemon=True).start()
|
||||||
|
|
||||||
# Подключаем сигнал выбора файла/папки
|
# Connect the file selection signal
|
||||||
file_explorer.file_signal.file_selected.connect(on_folder_selected)
|
file_explorer.file_signal.file_selected.connect(on_folder_selected)
|
||||||
|
|
||||||
# Центрируем FileExplorer относительно родительского виджета
|
# Center FileExplorer relative to the parent widget
|
||||||
parent_widget = self.parent
|
parent_widget = self.parent
|
||||||
if parent_widget:
|
if parent_widget:
|
||||||
parent_geometry = parent_widget.geometry()
|
parent_geometry = parent_widget.geometry()
|
||||||
@@ -789,7 +859,7 @@ Icon={icon_path}
|
|||||||
_("Failed to delete custom data: {error}").format(error=str(e))
|
_("Failed to delete custom data: {error}").format(error=str(e))
|
||||||
)
|
)
|
||||||
|
|
||||||
# Перезагрузка списка игр и обновление сетки
|
# Reload games list and update grid
|
||||||
self.load_games()
|
self.load_games()
|
||||||
self.update_game_grid()
|
self.update_game_grid()
|
||||||
|
|
||||||
|
@@ -9,7 +9,7 @@ from PySide6.QtWidgets import (
|
|||||||
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
|
||||||
from PIL import Image
|
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.localization import _
|
||||||
from portprotonqt.logger import get_logger
|
from portprotonqt.logger import get_logger
|
||||||
import portprotonqt.themes.standart.styles as default_styles
|
import portprotonqt.themes.standart.styles as default_styles
|
||||||
@@ -106,13 +106,15 @@ class FileExplorer(QDialog):
|
|||||||
self.setWindowModality(Qt.WindowModality.ApplicationModal)
|
self.setWindowModality(Qt.WindowModality.ApplicationModal)
|
||||||
self.setWindowFlags(self.windowFlags() & ~Qt.WindowType.WindowContextHelpButtonHint)
|
self.setWindowFlags(self.windowFlags() & ~Qt.WindowType.WindowContextHelpButtonHint)
|
||||||
|
|
||||||
# Find InputManager from parent
|
# Find InputManager and ContextMenuManager from parent
|
||||||
self.input_manager = None
|
self.input_manager = None
|
||||||
|
self.context_menu_manager = None
|
||||||
parent = self.parent()
|
parent = self.parent()
|
||||||
while parent:
|
while parent:
|
||||||
if hasattr(parent, 'input_manager'):
|
if hasattr(parent, 'input_manager'):
|
||||||
self.input_manager = cast("MainWindow", 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()
|
parent = parent.parent()
|
||||||
|
|
||||||
if self.input_manager:
|
if self.input_manager:
|
||||||
@@ -137,8 +139,9 @@ class FileExplorer(QDialog):
|
|||||||
if len(parts) < 2:
|
if len(parts) < 2:
|
||||||
continue
|
continue
|
||||||
mount_point = parts[1]
|
mount_point = parts[1]
|
||||||
# Исключаем системные и временные пути
|
# Исключаем системные и временные пути, но сохраняем /run/media
|
||||||
if mount_point.startswith(('/run', '/dev', '/sys', '/proc', '/tmp', '/snap', '/var/lib')):
|
if (mount_point.startswith(('/dev', '/sys', '/proc', '/tmp', '/snap', '/var/lib')) or
|
||||||
|
(mount_point.startswith('/run') and not mount_point.startswith('/run/media'))):
|
||||||
continue
|
continue
|
||||||
# Проверяем, является ли точка монтирования директорией и доступна ли она
|
# Проверяем, является ли точка монтирования директорией и доступна ли она
|
||||||
if os.path.isdir(mount_point) and os.access(mount_point, os.R_OK):
|
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.main_layout.setSpacing(10)
|
||||||
self.setLayout(self.main_layout)
|
self.setLayout(self.main_layout)
|
||||||
|
|
||||||
# Панель для смонтированных дисков
|
# Панель для смонтированных дисков и избранных папок
|
||||||
self.drives_layout = QHBoxLayout()
|
self.drives_layout = QHBoxLayout()
|
||||||
self.drives_scroll = QScrollArea()
|
self.drives_scroll = QScrollArea()
|
||||||
self.drives_scroll.setWidgetResizable(True)
|
self.drives_scroll.setWidgetResizable(True)
|
||||||
@@ -169,7 +172,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.drives_scroll.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||||
|
|
||||||
# Путь
|
# Путь
|
||||||
self.path_label = QLabel()
|
self.path_label = QLabel()
|
||||||
@@ -181,6 +184,8 @@ class FileExplorer(QDialog):
|
|||||||
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.file_list.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
||||||
|
self.file_list.customContextMenuRequested.connect(self.show_folder_context_menu)
|
||||||
self.main_layout.addWidget(self.file_list)
|
self.main_layout.addWidget(self.file_list)
|
||||||
|
|
||||||
# Кнопки
|
# Кнопки
|
||||||
@@ -197,6 +202,13 @@ class FileExplorer(QDialog):
|
|||||||
self.select_button.clicked.connect(self.select_item)
|
self.select_button.clicked.connect(self.select_item)
|
||||||
self.cancel_button.clicked.connect(self.reject)
|
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):
|
def move_selection(self, direction):
|
||||||
"""Перемещение выбора по списку"""
|
"""Перемещение выбора по списку"""
|
||||||
current_row = self.file_list.currentRow()
|
current_row = self.file_list.currentRow()
|
||||||
@@ -286,44 +298,96 @@ class FileExplorer(QDialog):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error navigating to parent directory: {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):
|
def update_drives_list(self):
|
||||||
"""Обновление списка смонтированных дисков"""
|
"""Обновление списка смонтированных дисков и избранных папок."""
|
||||||
for i in reversed(range(self.drives_layout.count())):
|
for i in reversed(range(self.drives_layout.count())):
|
||||||
widget = self.drives_layout.itemAt(i).widget()
|
item = self.drives_layout.itemAt(i)
|
||||||
if widget:
|
if item and item.widget():
|
||||||
|
widget = item.widget()
|
||||||
|
self.drives_layout.removeWidget(widget)
|
||||||
widget.deleteLater()
|
widget.deleteLater()
|
||||||
|
|
||||||
|
self.drive_buttons = []
|
||||||
drives = self.get_mounted_drives()
|
drives = self.get_mounted_drives()
|
||||||
self.drive_buttons = [] # Store buttons for navigation
|
favorite_folders = read_favorite_folders()
|
||||||
|
|
||||||
|
# Добавляем смонтированные диски
|
||||||
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.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||||
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.drive_buttons.append(button)
|
||||||
self.drives_layout.addStretch()
|
|
||||||
|
|
||||||
# Set focus to first drive button if available
|
# Добавляем избранные папки
|
||||||
if self.drive_buttons:
|
for folder in favorite_folders:
|
||||||
self.drive_buttons[0].setFocus()
|
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):
|
def select_drive(self):
|
||||||
"""Handle drive selection via gamepad"""
|
"""Обрабатывает выбор диска или избранной папки через геймпад."""
|
||||||
focused_widget = QApplication.focusWidget()
|
focused_widget = QApplication.focusWidget()
|
||||||
if isinstance(focused_widget, AutoSizeButton) and focused_widget in self.drive_buttons:
|
if isinstance(focused_widget, AutoSizeButton) and focused_widget in self.drive_buttons:
|
||||||
drive_path = None
|
drive_name = focused_widget.text().strip() # Удаляем пробелы
|
||||||
for drive in self.get_mounted_drives():
|
logger.debug(f"Выбрано имя: {drive_name}")
|
||||||
drive_name = os.path.basename(drive) or drive.split('/')[-1] or drive
|
|
||||||
if drive_name == focused_widget.text():
|
# Специальная обработка корневого каталога
|
||||||
drive_path = drive
|
if drive_name == "/":
|
||||||
break
|
if os.path.isdir("/") and os.access("/", os.R_OK):
|
||||||
if drive_path and os.path.isdir(drive_path) and os.access(drive_path, os.R_OK):
|
self.current_path = "/"
|
||||||
self.current_path = os.path.normpath(drive_path)
|
self.update_file_list()
|
||||||
self.update_file_list()
|
logger.info("Выбран корневой каталог: /")
|
||||||
else:
|
return
|
||||||
logger.warning(f"Путь диска недоступен: {drive_path}")
|
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):
|
def change_drive(self, drive_path):
|
||||||
"""Переход к выбранному диску"""
|
"""Переход к выбранному диску"""
|
||||||
|
@@ -4,7 +4,7 @@ import os
|
|||||||
from typing import Protocol, cast
|
from typing import Protocol, cast
|
||||||
from evdev import InputDevice, InputEvent, ecodes, list_devices, ff
|
from evdev import InputDevice, InputEvent, ecodes, list_devices, ff
|
||||||
from pyudev import Context, Monitor, MonitorObserver, Device
|
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.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
|
||||||
@@ -161,7 +161,20 @@ class InputManager(QObject):
|
|||||||
|
|
||||||
def handle_file_explorer_button(self, button_code):
|
def handle_file_explorer_button(self, button_code):
|
||||||
try:
|
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'):
|
if not self.file_explorer or not hasattr(self.file_explorer, 'file_list'):
|
||||||
|
logger.debug("No file explorer or file_list available")
|
||||||
return
|
return
|
||||||
|
|
||||||
focused_widget = QApplication.focusWidget()
|
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:
|
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
|
self.file_explorer.select_drive() # Select the focused drive
|
||||||
elif self.file_explorer.file_list.count() == 0:
|
elif self.file_explorer.file_list.count() == 0:
|
||||||
|
logger.debug("File list is empty")
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
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):
|
||||||
# Открываем директорию
|
|
||||||
self.file_explorer.current_path = os.path.normpath(full_path)
|
self.file_explorer.current_path = os.path.normpath(full_path)
|
||||||
self.file_explorer.update_file_list()
|
self.file_explorer.update_file_list()
|
||||||
elif not self.file_explorer.directory_only:
|
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.file_signal.file_selected.emit(os.path.normpath(full_path))
|
||||||
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['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
|
elif button_code in BUTTONS['add_game']: # X button
|
||||||
if self.file_explorer.file_list.count() == 0:
|
if self.file_explorer.file_list.count() == 0:
|
||||||
|
logger.debug("File list is empty")
|
||||||
return
|
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):
|
||||||
# Подтверждаем выбор директории
|
|
||||||
self.file_explorer.file_signal.file_selected.emit(os.path.normpath(full_path))
|
self.file_explorer.file_signal.file_selected.emit(os.path.normpath(full_path))
|
||||||
self.file_explorer.accept()
|
self.file_explorer.accept()
|
||||||
else:
|
else:
|
||||||
@@ -202,12 +225,29 @@ class InputManager(QObject):
|
|||||||
if self.original_button_handler:
|
if self.original_button_handler:
|
||||||
self.original_button_handler(button_code)
|
self.original_button_handler(button_code)
|
||||||
except Exception as e:
|
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):
|
def handle_file_explorer_dpad(self, code, value, current_time):
|
||||||
"""Обработка движения D-pad и левого стика для FileExplorer"""
|
"""Обработка движения D-pad и левого стика для FileExplorer"""
|
||||||
try:
|
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:
|
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
|
return
|
||||||
|
|
||||||
focused_widget = QApplication.focusWidget()
|
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 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
|
# If not focused on a drive button, focus the first one
|
||||||
self.file_explorer.drive_buttons[0].setFocus()
|
self.file_explorer.drive_buttons[0].setFocus()
|
||||||
|
self.file_explorer.ensure_button_visible(self.file_explorer.drive_buttons[0])
|
||||||
return
|
return
|
||||||
current_idx = self.file_explorer.drive_buttons.index(focused_widget)
|
current_idx = self.file_explorer.drive_buttons.index(focused_widget)
|
||||||
if value < 0: # Left
|
if value < 0: # Left
|
||||||
next_idx = max(current_idx - 1, 0)
|
next_idx = max(current_idx - 1, 0)
|
||||||
self.file_explorer.drive_buttons[next_idx].setFocus()
|
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
|
elif value > 0: # Right
|
||||||
next_idx = min(current_idx + 1, len(self.file_explorer.drive_buttons) - 1)
|
next_idx = min(current_idx + 1, len(self.file_explorer.drive_buttons) - 1)
|
||||||
self.file_explorer.drive_buttons[next_idx].setFocus()
|
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):
|
elif code in (ecodes.ABS_HAT0Y, ecodes.ABS_Y):
|
||||||
if isinstance(focused_widget, AutoSizeButton) and focused_widget in self.file_explorer.drive_buttons:
|
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
|
# Move focus to file list if navigating down from drive buttons
|
||||||
@@ -264,7 +307,7 @@ class InputManager(QObject):
|
|||||||
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:
|
||||||
logger.error(f"Error in FileExplorer dpad handler: {e}")
|
logger.error("Error in FileExplorer dpad handler: %s", e)
|
||||||
|
|
||||||
def handle_navigation_repeat(self):
|
def handle_navigation_repeat(self):
|
||||||
"""Плавное повторение движения с переменной скоростью для FileExplorer"""
|
"""Плавное повторение движения с переменной скоростью для FileExplorer"""
|
||||||
@@ -742,6 +785,11 @@ class InputManager(QObject):
|
|||||||
if not app:
|
if not app:
|
||||||
return super().eventFilter(obj, event)
|
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
|
# Handle key press and release events
|
||||||
if not isinstance(event, QKeyEvent):
|
if not isinstance(event, QKeyEvent):
|
||||||
return super().eventFilter(obj, event)
|
return super().eventFilter(obj, event)
|
||||||
@@ -754,6 +802,54 @@ class InputManager(QObject):
|
|||||||
|
|
||||||
# Handle key press events
|
# Handle key press events
|
||||||
if event.type() == QEvent.Type.KeyPress:
|
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
|
# Handle QLineEdit cursor movement with Left/Right arrows
|
||||||
if isinstance(focused, QLineEdit) and key in (Qt.Key.Key_Left, Qt.Key.Key_Right):
|
if isinstance(focused, QLineEdit) and key in (Qt.Key.Key_Left, Qt.Key.Key_Right):
|
||||||
if key == Qt.Key.Key_Left:
|
if key == Qt.Key.Key_Left:
|
||||||
@@ -778,10 +874,13 @@ class InputManager(QObject):
|
|||||||
self.file_explorer.previous_dir()
|
self.file_explorer.previous_dir()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Close AddGameDialog with Escape
|
# Close Dialogs with Escape
|
||||||
if key == Qt.Key.Key_Escape and isinstance(popup, QDialog):
|
if key == Qt.Key.Key_Escape:
|
||||||
popup.reject()
|
if isinstance(focused, QLineEdit):
|
||||||
return True
|
return False
|
||||||
|
if isinstance(active_win, QDialog):
|
||||||
|
active_win.reject()
|
||||||
|
return True
|
||||||
|
|
||||||
# FullscreenDialog navigation
|
# FullscreenDialog navigation
|
||||||
if isinstance(active_win, FullscreenDialog):
|
if isinstance(active_win, FullscreenDialog):
|
||||||
@@ -797,7 +896,7 @@ class InputManager(QObject):
|
|||||||
return True # Consume event to prevent tab switching
|
return True # Consume event to prevent tab switching
|
||||||
|
|
||||||
# Handle tab switching with Left/Right arrow keys when not in GameCard focus or QLineEdit
|
# 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()
|
idx = self._parent.stackedWidget.currentIndex()
|
||||||
total = len(self._parent.tabButtons)
|
total = len(self._parent.tabButtons)
|
||||||
if key == Qt.Key.Key_Left:
|
if key == Qt.Key.Key_Left:
|
||||||
|
@@ -27,7 +27,7 @@ classifiers = [
|
|||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"babel>=2.17.0",
|
"babel>=2.17.0",
|
||||||
"beautifulsoup4>=4.13.4",
|
"beautifulsoup4>=4.13.5",
|
||||||
"evdev>=1.9.2",
|
"evdev>=1.9.2",
|
||||||
"icoextract>=0.2.0",
|
"icoextract>=0.2.0",
|
||||||
"numpy>=2.2.4",
|
"numpy>=2.2.4",
|
||||||
@@ -36,7 +36,7 @@ dependencies = [
|
|||||||
"psutil>=7.0.0",
|
"psutil>=7.0.0",
|
||||||
"pyside6>=6.9.1",
|
"pyside6>=6.9.1",
|
||||||
"pyudev>=0.24.3",
|
"pyudev>=0.24.3",
|
||||||
"requests>=2.32.4",
|
"requests>=2.32.5",
|
||||||
"tqdm>=4.67.1",
|
"tqdm>=4.67.1",
|
||||||
"vdf>=3.4",
|
"vdf>=3.4",
|
||||||
"websocket-client>=1.8.0",
|
"websocket-client>=1.8.0",
|
||||||
@@ -105,5 +105,5 @@ ignore = [
|
|||||||
dev = [
|
dev = [
|
||||||
"pre-commit>=4.3.0",
|
"pre-commit>=4.3.0",
|
||||||
"pyaspeller>=2.0.2",
|
"pyaspeller>=2.0.2",
|
||||||
"pyright>=1.1.403",
|
"pyright>=1.1.404",
|
||||||
]
|
]
|
||||||
|
@@ -19,7 +19,7 @@
|
|||||||
"enabled": false
|
"enabled": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"matchManagers": ["github-actions", "pre-commit"],
|
"matchManagers": ["github-actions", "pre-commit", "poetry"],
|
||||||
"enabled": false
|
"enabled": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
24
uv.lock
generated
24
uv.lock
generated
@@ -17,15 +17,15 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "beautifulsoup4"
|
name = "beautifulsoup4"
|
||||||
version = "4.13.4"
|
version = "4.13.5"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "soupsieve" },
|
{ name = "soupsieve" },
|
||||||
{ name = "typing-extensions" },
|
{ name = "typing-extensions" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067 }
|
sdist = { url = "https://files.pythonhosted.org/packages/85/2e/3e5079847e653b1f6dc647aa24549d68c6addb4c595cc0d902d1b19308ad/beautifulsoup4-4.13.5.tar.gz", hash = "sha256:5e70131382930e7c3de33450a2f54a63d5e4b19386eab43a5b34d594268f3695", size = 622954 }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285 },
|
{ url = "https://files.pythonhosted.org/packages/04/eb/f4151e0c7377a6e08a38108609ba5cede57986802757848688aeedd1b9e8/beautifulsoup4-4.13.5-py3-none-any.whl", hash = "sha256:642085eaa22233aceadff9c69651bc51e8bf3f874fb6d7104ece2beb24b47c4a", size = 105113 },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -531,7 +531,7 @@ dev = [
|
|||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "babel", specifier = ">=2.17.0" },
|
{ name = "babel", specifier = ">=2.17.0" },
|
||||||
{ name = "beautifulsoup4", specifier = ">=4.13.4" },
|
{ name = "beautifulsoup4", specifier = ">=4.13.5" },
|
||||||
{ name = "evdev", specifier = ">=1.9.2" },
|
{ name = "evdev", specifier = ">=1.9.2" },
|
||||||
{ name = "icoextract", specifier = ">=0.2.0" },
|
{ name = "icoextract", specifier = ">=0.2.0" },
|
||||||
{ name = "numpy", specifier = ">=2.2.4" },
|
{ name = "numpy", specifier = ">=2.2.4" },
|
||||||
@@ -540,7 +540,7 @@ requires-dist = [
|
|||||||
{ name = "psutil", specifier = ">=7.0.0" },
|
{ name = "psutil", specifier = ">=7.0.0" },
|
||||||
{ name = "pyside6", specifier = ">=6.9.1" },
|
{ name = "pyside6", specifier = ">=6.9.1" },
|
||||||
{ name = "pyudev", specifier = ">=0.24.3" },
|
{ name = "pyudev", specifier = ">=0.24.3" },
|
||||||
{ name = "requests", specifier = ">=2.32.4" },
|
{ name = "requests", specifier = ">=2.32.5" },
|
||||||
{ name = "tqdm", specifier = ">=4.67.1" },
|
{ name = "tqdm", specifier = ">=4.67.1" },
|
||||||
{ name = "vdf", specifier = ">=3.4" },
|
{ name = "vdf", specifier = ">=3.4" },
|
||||||
{ name = "websocket-client", specifier = ">=1.8.0" },
|
{ name = "websocket-client", specifier = ">=1.8.0" },
|
||||||
@@ -550,7 +550,7 @@ requires-dist = [
|
|||||||
dev = [
|
dev = [
|
||||||
{ name = "pre-commit", specifier = ">=4.3.0" },
|
{ name = "pre-commit", specifier = ">=4.3.0" },
|
||||||
{ name = "pyaspeller", specifier = ">=2.0.2" },
|
{ name = "pyaspeller", specifier = ">=2.0.2" },
|
||||||
{ name = "pyright", specifier = ">=1.1.403" },
|
{ name = "pyright", specifier = ">=1.1.404" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -598,15 +598,15 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyright"
|
name = "pyright"
|
||||||
version = "1.1.403"
|
version = "1.1.404"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "nodeenv" },
|
{ name = "nodeenv" },
|
||||||
{ name = "typing-extensions" },
|
{ name = "typing-extensions" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/fe/f6/35f885264ff08c960b23d1542038d8da86971c5d8c955cfab195a4f672d7/pyright-1.1.403.tar.gz", hash = "sha256:3ab69b9f41c67fb5bbb4d7a36243256f0d549ed3608678d381d5f51863921104", size = 3913526 }
|
sdist = { url = "https://files.pythonhosted.org/packages/e2/6e/026be64c43af681d5632722acd100b06d3d39f383ec382ff50a71a6d5bce/pyright-1.1.404.tar.gz", hash = "sha256:455e881a558ca6be9ecca0b30ce08aa78343ecc031d37a198ffa9a7a1abeb63e", size = 4065679 }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/49/b6/b04e5c2f41a5ccad74a1a4759da41adb20b4bc9d59a5e08d29ba60084d07/pyright-1.1.403-py3-none-any.whl", hash = "sha256:c0eeca5aa76cbef3fcc271259bbd785753c7ad7bcac99a9162b4c4c7daed23b3", size = 5684504 },
|
{ url = "https://files.pythonhosted.org/packages/84/30/89aa7f7d7a875bbb9a577d4b1dc5a3e404e3d2ae2657354808e905e358e0/pyright-1.1.404-py3-none-any.whl", hash = "sha256:c7b7ff1fdb7219c643079e4c3e7d4125f0dafcc19d253b47e898d130ea426419", size = 5902951 },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -712,7 +712,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "requests"
|
name = "requests"
|
||||||
version = "2.32.4"
|
version = "2.32.5"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "certifi" },
|
{ name = "certifi" },
|
||||||
@@ -720,9 +720,9 @@ dependencies = [
|
|||||||
{ name = "idna" },
|
{ name = "idna" },
|
||||||
{ name = "urllib3" },
|
{ name = "urllib3" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258 }
|
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517 }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847 },
|
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738 },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
Reference in New Issue
Block a user