feat: added favorites to file explorer
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
This commit is contained in:
@@ -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:
|
||||||
@@ -159,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)
|
||||||
@@ -170,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()
|
||||||
@@ -182,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)
|
||||||
|
|
||||||
# Кнопки
|
# Кнопки
|
||||||
@@ -198,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()
|
||||||
@@ -288,43 +299,84 @@ class FileExplorer(QDialog):
|
|||||||
logger.error(f"Error navigating to parent directory: {e}")
|
logger.error(f"Error navigating to parent directory: {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):
|
||||||
"""Переход к выбранному диску"""
|
"""Переход к выбранному диску"""
|
||||||
|
@@ -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()
|
||||||
@@ -264,7 +304,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 +782,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)
|
||||||
|
Reference in New Issue
Block a user