3 Commits

Author SHA1 Message Date
48048a3f50 feat: replace QFileDialog with custom FileExplorer for Legendary import
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-30 00:25:28 +05:00
7c617eef78 chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-29 23:25:27 +05:00
08ba801f74 fix: prevent empty area when updating game grid thank to @Vector_null
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-29 23:23:22 +05:00
5 changed files with 201 additions and 136 deletions

View File

@@ -26,6 +26,7 @@
- Ошибки темы в нативном пакете
- Ошибки темы в Gamescope
- Размер иконок для desktop файлов теперь 128x128
- Пустая область при обновлении сетки игр
### Contributors
- @Dervart

View File

@@ -6,14 +6,14 @@ import subprocess
import threading
import logging
import orjson
from PySide6.QtWidgets import QMessageBox, QDialog, QMenu, QFileDialog
from PySide6.QtWidgets import QMessageBox, QDialog, QMenu
from PySide6.QtCore import QUrl, QPoint, QObject, Signal, Qt
from PySide6.QtGui import QDesktopServices
from portprotonqt.localization import _
from portprotonqt.config_utils import parse_desktop_entry, read_favorites, save_favorites
from portprotonqt.steam_api import is_game_in_steam, add_to_steam, remove_from_steam
from portprotonqt.egs_api import add_egs_to_steam, get_egs_executable, remove_egs_from_steam
from portprotonqt.dialogs import AddGameDialog, generate_thumbnail
from portprotonqt.dialogs import AddGameDialog, FileExplorer, generate_thumbnail
logger = logging.getLogger(__name__)
@@ -282,18 +282,25 @@ class ContextMenuManager:
"""
if not self._check_portproton():
return
folder_path = QFileDialog.getExistingDirectory(
self.parent, _("Select Game Installation Folder"), os.path.expanduser("~")
)
if not folder_path:
self._show_status_message(_("No folder selected"))
return
if not os.path.exists(self.legendary_path):
self.signals.show_warning_dialog.emit(
_("Error"),
_("Legendary executable not found at {path}").format(path=self.legendary_path)
)
return
# Используем FileExplorer с directory_only=True
file_explorer = FileExplorer(
parent=self.parent,
theme=self.theme,
initial_path=os.path.expanduser("~"),
directory_only=True
)
def on_folder_selected(folder_path):
if not folder_path:
self._show_status_message(_("No folder selected"))
return
def run_import():
cmd = [self.legendary_path, "import", app_name, folder_path]
try:
@@ -312,6 +319,20 @@ class ContextMenuManager:
self._show_status_message(_("Importing '{game_name}' to Legendary...").format(game_name=game_name))
threading.Thread(target=run_import, daemon=True).start()
# Подключаем сигнал выбора файла/папки
file_explorer.file_signal.file_selected.connect(on_folder_selected)
# Центрируем FileExplorer относительно родительского виджета
parent_widget = self.parent
if parent_widget:
parent_geometry = parent_widget.geometry()
center_point = parent_geometry.center()
file_explorer_geometry = file_explorer.geometry()
file_explorer_geometry.moveCenter(center_point)
file_explorer.setGeometry(file_explorer_geometry)
file_explorer.show()
def toggle_favorite(self, game_card, add: bool):
"""
Toggle the favorite status of a game and update its icon.

View File

@@ -88,12 +88,13 @@ class FileSelectedSignal(QObject):
file_selected = Signal(str) # Сигнал с путем к выбранному файлу
class FileExplorer(QDialog):
def __init__(self, parent=None, theme=None, file_filter=None, initial_path=None):
def __init__(self, parent=None, theme=None, file_filter=None, initial_path=None, directory_only=False):
super().__init__(parent)
self.theme = theme if theme else default_styles
self.theme_manager = ThemeManager()
self.file_signal = FileSelectedSignal()
self.file_filter = file_filter # Store the file filter
self.directory_only = directory_only # Store the directory_only flag
self.mime_db = QMimeDatabase() # Initialize QMimeDatabase for mimetype detection
self.path_history = {} # Dictionary to store last selected item per directory
self.initial_path = initial_path # Store initial path if provided
@@ -216,13 +217,21 @@ class FileExplorer(QDialog):
full_path = os.path.join(self.current_path, selected)
if os.path.isdir(full_path):
# Если выбрана директория, нормализуем путь
if self.directory_only:
# Подтверждаем выбор директории
self.file_signal.file_selected.emit(os.path.normpath(full_path))
self.accept()
else:
# Открываем директорию
self.current_path = os.path.normpath(full_path)
self.update_file_list()
else:
if not self.directory_only:
# Для файла отправляем нормализованный путь
self.file_signal.file_selected.emit(os.path.normpath(full_path))
self.accept()
else:
logger.debug("Selected item is not a directory, ignoring: %s", full_path)
def previous_dir(self):
"""Возврат к родительской директории"""
@@ -288,14 +297,7 @@ class FileExplorer(QDialog):
items = os.listdir(self.current_path)
dirs = [d for d in items if os.path.isdir(os.path.join(self.current_path, d))]
# Apply file filter if provided
files = [f for f in items if os.path.isfile(os.path.join(self.current_path, f))]
if self.file_filter:
if isinstance(self.file_filter, str):
files = [f for f in files if f.lower().endswith(self.file_filter)]
elif isinstance(self.file_filter, tuple):
files = [f for f in files if any(f.lower().endswith(ext) for ext in self.file_filter)]
# Добавляем директории
for d in sorted(dirs):
item = QListWidgetItem(f"{d}/")
folder_icon = self.theme_manager.get_icon("folder")
@@ -307,6 +309,15 @@ class FileExplorer(QDialog):
item.setIcon(folder_icon)
self.file_list.addItem(item)
# Добавляем файлы только если directory_only=False
if not self.directory_only:
files = [f for f in items if os.path.isfile(os.path.join(self.current_path, f))]
if self.file_filter:
if isinstance(self.file_filter, str):
files = [f for f in files if f.lower().endswith(self.file_filter)]
elif isinstance(self.file_filter, tuple):
files = [f for f in files if any(f.lower().endswith(ext) for ext in self.file_filter)]
for f in sorted(files):
item = QListWidgetItem(f)
file_path = os.path.join(self.current_path, f)

View File

@@ -1,5 +1,6 @@
import time
import threading
import os
from typing import Protocol, cast
from evdev import InputDevice, InputEvent, ecodes, list_devices, ff
from pyudev import Context, Monitor, MonitorObserver, Device
@@ -162,8 +163,28 @@ class InputManager(QObject):
if not self.file_explorer or not hasattr(self.file_explorer, 'file_list'):
return
if button_code in BUTTONS['confirm']:
self.file_explorer.select_item()
if button_code in BUTTONS['add_game']:
if self.file_explorer.file_list.count() == 0:
return
selected = self.file_explorer.file_list.currentItem().text()
full_path = os.path.join(self.file_explorer.current_path, selected)
if os.path.isdir(full_path):
# Подтверждаем выбор директории
self.file_explorer.file_signal.file_selected.emit(os.path.normpath(full_path))
self.file_explorer.accept()
else:
logger.debug("Selected item is not a directory: %s", full_path)
elif button_code in BUTTONS['confirm']:
if self.file_explorer.file_list.count() == 0:
return
selected = self.file_explorer.file_list.currentItem().text()
full_path = os.path.join(self.file_explorer.current_path, selected)
if os.path.isdir(full_path):
# Открываем директорию
self.file_explorer.current_path = os.path.normpath(full_path)
self.file_explorer.update_file_list()
else:
logger.debug("Selected item is not a directory, cannot open: %s", full_path)
elif button_code in BUTTONS['back']:
self.file_explorer.close()
elif button_code in BUTTONS['prev_dir']:

View File

@@ -696,59 +696,50 @@ class MainWindow(QMainWindow):
loaded_count += 1
def updateGameGrid(self, games_list=None):
"""Updates the game grid with the provided games list or self.games."""
if games_list is None:
games_list = self.games
if not games_list:
# Скрываем все карточки, если список пуст
for card in self.game_card_cache.values():
card.hide()
self.game_card_cache.clear()
self.pending_images.clear()
self.gamesListWidget.updateGeometry()
return
# Создаем словарь текущих игр с уникальным ключом (name + exec_line)
current_games = {(game_data[0], game_data[4]): game_data for game_data in games_list}
# Проверяем, изменился ли список игр или размер карточек
current_game_keys = set(current_games.keys())
cached_game_keys = set(self.game_card_cache.keys())
card_width_changed = self.card_width != getattr(self, '_last_card_width', None)
if current_game_keys == cached_game_keys and not card_width_changed:
# Список игр и размер карточек не изменились, обновляем только видимость
"""Обновляет сетку игровых карточек с сохранением порядка сортировки"""
# Подготовка данных
games_list = games_list if games_list is not None else self.games
search_text = self.searchEdit.text().strip().lower()
for game_key, card in self.game_card_cache.items():
game_name = game_key[0]
card.setVisible(search_text in game_name.lower() or not search_text)
self.loadVisibleImages()
return
favorites = read_favorites()
sort_method = read_sort_method()
# Обновляем размер карточек, если он изменился
if card_width_changed:
for card in self.game_card_cache.values():
card.setFixedWidth(self.card_width + 20) # Учитываем extra_margin в GameCard
# Сортируем игры согласно текущим настройкам
def sort_key(game):
name = game[0]
# Избранные всегда первые
if name in favorites:
fav_order = 0
else:
fav_order = 1
# Удаляем карточки, которых больше нет в списке
layout_changed = False
for card_key in list(self.game_card_cache.keys()):
if card_key not in current_games:
card = self.game_card_cache.pop(card_key)
self.gamesListLayout.removeWidget(card)
card.deleteLater()
if card_key in self.pending_images:
del self.pending_images[card_key]
layout_changed = True
if sort_method == "playtime":
return (fav_order, -game[11], -game[10]) # playtime_seconds, last_launch_ts
elif sort_method == "alphabetical":
return (fav_order, name.lower())
elif sort_method == "favorites":
return (fav_order,)
else: # "last_launch" или по умолчанию
return (fav_order, -game[10], -game[11]) # last_launch_ts, playtime_seconds
# Добавляем новые карточки и обновляем существующие
for game_data in games_list:
sorted_games = sorted(games_list, key=sort_key)
# Создаем временный список для новых карточек
new_card_order = []
# Обрабатываем каждую игру в отсортированном порядке
for game_data in sorted_games:
game_name = game_data[0]
game_key = (game_name, game_data[4])
search_text = self.searchEdit.text().strip().lower()
should_be_visible = search_text in game_name.lower() or not search_text
exec_line = game_data[4]
game_key = (game_name, exec_line)
should_be_visible = not search_text or search_text in game_name.lower()
# Если карточка уже существует - используем существующую
if game_key in self.game_card_cache:
card = self.game_card_cache[game_key]
card.setVisible(should_be_visible)
new_card_order.append((game_key, card))
continue
if game_key not in self.game_card_cache:
# Создаем новую карточку
card = GameCard(
*game_data,
@@ -757,8 +748,11 @@ class MainWindow(QMainWindow):
card_width=self.card_width,
context_menu_manager=self.context_menu_manager
)
# Подключаем сигналы
card.hoverChanged.connect(self._on_card_hovered)
card.focusChanged.connect(self._on_card_focused)
# Подключаем сигналы контекстного меню
card.editShortcutRequested.connect(self.context_menu_manager.edit_game_shortcut)
card.deleteGameRequested.connect(self.context_menu_manager.delete_game)
@@ -769,25 +763,42 @@ class MainWindow(QMainWindow):
card.addToSteamRequested.connect(self.context_menu_manager.add_to_steam)
card.removeFromSteamRequested.connect(self.context_menu_manager.remove_from_steam)
card.openGameFolderRequested.connect(self.context_menu_manager.open_game_folder)
# Добавляем в кэш и временный список
self.game_card_cache[game_key] = card
self.gamesListLayout.addWidget(card)
layout_changed = True
else:
# Обновляем видимость существующей карточки
card = self.game_card_cache[game_key]
new_card_order.append((game_key, card))
card.setVisible(should_be_visible)
# Сохраняем текущий card_width
self._last_card_width = self.card_width
# Полностью перестраиваем макет в правильном порядке, чистим FlowLayout
while self.gamesListLayout.count():
child = self.gamesListLayout.takeAt(0)
if child.widget():
child.widget().setParent(None)
# Добавляем карточки в макет в отсортированном порядке
for _game_key, card in new_card_order:
self.gamesListLayout.addWidget(card)
# Загружаем обложку, если карточка видима
if card.isVisible():
self.loadVisibleImages()
# Удаляем карточки для игр, которых больше нет в списке
existing_keys = {game_key for game_key, _ in new_card_order}
for card_key in list(self.game_card_cache.keys()):
if card_key not in existing_keys:
card = self.game_card_cache.pop(card_key)
card.deleteLater()
if card_key in self.pending_images:
del self.pending_images[card_key]
# Принудительно обновляем макет
if layout_changed:
self.gamesListLayout.update()
self.gamesListWidget.updateGeometry()
self.gamesListWidget.update()
# Загружаем изображения для видимых карточек
self.loadVisibleImages()
# Сохраняем текущий размер карточек
self._last_card_width = self.card_width
def clearLayout(self, layout):
"""Удаляет все виджеты из layout."""