chore: cleanup MainWindow class

Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
This commit is contained in:
2025-09-30 15:43:11 +05:00
parent 69360f7e7e
commit b5efee29ea
2 changed files with 344 additions and 334 deletions

View File

@@ -0,0 +1,302 @@
from typing import Protocol
from portprotonqt.game_card import GameCard
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QScrollArea, QSlider, QScroller
from PySide6.QtCore import Qt, QTimer
from portprotonqt.custom_widgets import FlowLayout
from portprotonqt.config_utils import read_favorites, read_sort_method, read_card_size, save_card_size
from portprotonqt.image_utils import load_pixmap_async
from portprotonqt.context_menu_manager import ContextMenuManager, CustomLineEdit
class MainWindowProtocol(Protocol):
"""Protocol defining the interface that MainWindow must implement for GameLibraryManager."""
def openGameDetailPage(
self,
name: str,
description: str,
cover_path: str | None = None,
appid: str = "",
exec_line: str = "",
controller_support: str = "",
last_launch: str = "",
formatted_playtime: str = "",
protondb_tier: str = "",
game_source: str = "",
anticheat_status: str = "",
) -> None: ...
def createSearchWidget(self) -> tuple[QWidget, CustomLineEdit]: ...
def on_slider_released(self) -> None: ...
# Required attributes
searchEdit: CustomLineEdit
_last_card_width: int
current_hovered_card: GameCard | None
current_focused_card: GameCard | None
class GameLibraryManager:
def __init__(self, main_window: MainWindowProtocol, theme, context_menu_manager: ContextMenuManager | None):
self.main_window = main_window
self.theme = theme
self.context_menu_manager: ContextMenuManager | None = context_menu_manager
self.games: list[tuple] = []
self.filtered_games: list[tuple] = []
self.game_card_cache = {}
self.pending_images = {}
self.card_width = read_card_size()
self.gamesListWidget: QWidget | None = None
self.gamesListLayout: FlowLayout | None = None
self.sizeSlider: QSlider | None = None
def create_games_library_widget(self):
"""Creates the games library widget with search, grid, and slider."""
self.gamesLibraryWidget = QWidget()
self.gamesLibraryWidget.setStyleSheet(self.theme.LIBRARY_WIDGET_STYLE)
layout = QVBoxLayout(self.gamesLibraryWidget)
layout.setSpacing(15)
# Search widget
searchWidget, self.searchEdit = self.main_window.createSearchWidget()
layout.addWidget(searchWidget)
# Scroll area for game grid
scrollArea = QScrollArea()
scrollArea.setWidgetResizable(True)
scrollArea.setStyleSheet(self.theme.SCROLL_AREA_STYLE)
QScroller.grabGesture(scrollArea.viewport(), QScroller.ScrollerGestureType.LeftMouseButtonGesture)
self.gamesListWidget = QWidget()
self.gamesListWidget.setStyleSheet(self.theme.LIST_WIDGET_STYLE)
self.gamesListLayout = FlowLayout(self.gamesListWidget)
self.gamesListWidget.setLayout(self.gamesListLayout)
scrollArea.setWidget(self.gamesListWidget)
layout.addWidget(scrollArea)
# Slider for card size
sliderLayout = QHBoxLayout()
sliderLayout.addStretch()
self.sizeSlider = QSlider(Qt.Orientation.Horizontal)
self.sizeSlider.setMinimum(200)
self.sizeSlider.setMaximum(250)
self.sizeSlider.setValue(self.card_width)
self.sizeSlider.setTickInterval(10)
self.sizeSlider.setFixedWidth(150)
self.sizeSlider.setToolTip(f"{self.card_width} px")
self.sizeSlider.setStyleSheet(self.theme.SLIDER_SIZE_STYLE)
self.sizeSlider.sliderReleased.connect(self.main_window.on_slider_released)
sliderLayout.addWidget(self.sizeSlider)
layout.addLayout(sliderLayout)
# Calculate initial card width
def calculate_card_width():
if self.gamesListLayout is None:
return
available_width = scrollArea.width() - 20
spacing = self.gamesListLayout._spacing
target_cards_per_row = 8
calculated_width = (available_width - spacing * (target_cards_per_row - 1)) // target_cards_per_row
calculated_width = max(200, min(calculated_width, 250))
QTimer.singleShot(0, calculate_card_width)
# Connect scroll event for lazy loading
scrollArea.verticalScrollBar().valueChanged.connect(self.load_visible_images)
return self.gamesLibraryWidget
def on_slider_released(self):
"""Handles slider release to update card size."""
if self.sizeSlider is None:
return
self.card_width = self.sizeSlider.value()
self.sizeSlider.setToolTip(f"{self.card_width} px")
save_card_size(self.card_width)
for card in self.game_card_cache.values():
card.update_card_size(self.card_width)
self.update_game_grid()
def load_visible_images(self):
"""Loads images for visible game cards."""
if self.gamesListWidget is None:
return
visible_region = self.gamesListWidget.visibleRegion()
max_concurrent_loads = 5
loaded_count = 0
for card_key, card in self.game_card_cache.items():
if card_key in self.pending_images and visible_region.contains(card.pos()) and loaded_count < max_concurrent_loads:
cover_path, width, height, callback = self.pending_images.pop(card_key)
load_pixmap_async(cover_path, width, height, callback)
loaded_count += 1
def _on_card_focused(self, game_name: str, is_focused: bool):
"""Handles card focus events."""
card_key = None
for key, card in self.game_card_cache.items():
if card.name == game_name:
card_key = key
break
if not card_key:
return
card = self.game_card_cache[card_key]
if is_focused:
if self.main_window.current_hovered_card and self.main_window.current_hovered_card != card:
self.main_window.current_hovered_card._hovered = False
self.main_window.current_hovered_card.leaveEvent(None)
self.main_window.current_hovered_card = None
if self.main_window.current_focused_card and self.main_window.current_focused_card != card:
self.main_window.current_focused_card._focused = False
self.main_window.current_focused_card.clearFocus()
self.main_window.current_focused_card = card
else:
if self.main_window.current_focused_card == card:
self.main_window.current_focused_card = None
def _on_card_hovered(self, game_name: str, is_hovered: bool):
"""Handles card hover events."""
card_key = None
for key, card in self.game_card_cache.items():
if card.name == game_name:
card_key = key
break
if not card_key:
return
card = self.game_card_cache[card_key]
if is_hovered:
if self.main_window.current_focused_card and self.main_window.current_focused_card != card:
self.main_window.current_focused_card._focused = False
self.main_window.current_focused_card.clearFocus()
if self.main_window.current_hovered_card and self.main_window.current_hovered_card != card:
self.main_window.current_hovered_card._hovered = False
self.main_window.current_hovered_card.leaveEvent(None)
self.main_window.current_hovered_card = card
else:
if self.main_window.current_hovered_card == card:
self.main_window.current_hovered_card = None
def update_game_grid(self, games_list: list[tuple] | None = None):
"""Updates the game grid with the provided or current game list."""
if self.gamesListLayout is None or self.gamesListWidget is None:
return
games_list = games_list if games_list is not None else self.games
search_text = self.main_window.searchEdit.text().strip().lower()
favorites = read_favorites()
sort_method = read_sort_method()
def sort_key(game):
name = game[0]
fav_order = 0 if name in favorites else 1
if sort_method == "playtime":
return (fav_order, -game[11], -game[10])
elif sort_method == "alphabetical":
return (fav_order, name.lower())
elif sort_method == "favorites":
return (fav_order,)
else:
return (fav_order, -game[10], -game[11])
sorted_games = sorted(games_list, key=sort_key)
new_card_order = []
for game_data in sorted_games:
game_name = game_data[0]
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 self.context_menu_manager is None:
continue # Skip card creation if context_menu_manager is None
card = GameCard(
*game_data,
select_callback=self.main_window.openGameDetailPage,
theme=self.theme,
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)
card.addToMenuRequested.connect(self.context_menu_manager.add_to_menu)
card.removeFromMenuRequested.connect(self.context_menu_manager.remove_from_menu)
card.addToDesktopRequested.connect(self.context_menu_manager.add_to_desktop)
card.removeFromDesktopRequested.connect(self.context_menu_manager.remove_from_desktop)
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
new_card_order.append((game_key, card))
card.setVisible(should_be_visible)
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.load_visible_images()
existing_keys = {game_key for game_key, card 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]
self.gamesListLayout.update()
self.gamesListWidget.updateGeometry()
self.gamesListWidget.update()
self.main_window._last_card_width = self.card_width
def clear_layout(self, layout):
"""Clears all widgets from the layout."""
if layout is None:
return
while layout.count():
child = layout.takeAt(0)
if child.widget():
widget = child.widget()
for key, card in list(self.game_card_cache.items()):
if card == widget:
del self.game_card_cache[key]
if key in self.pending_images:
del self.pending_images[key]
widget.deleteLater()
def set_games(self, games: list[tuple]):
"""Sets the games list and updates the filtered games."""
self.games = games
self.filtered_games = self.games
self.update_game_grid()
def filter_games_delayed(self):
"""Filters games based on search text and updates the grid."""
text = self.main_window.searchEdit.text().strip().lower()
if text == "":
self.filtered_games = self.games
else:
self.filtered_games = [game for game in self.games if text in game[0].lower()]
self.update_game_grid(self.filtered_games)

View File

@@ -10,7 +10,7 @@ from portprotonqt.logger import get_logger
from portprotonqt.dialogs import AddGameDialog, FileExplorer
from portprotonqt.game_card import GameCard
from portprotonqt.animations import DetailPageAnimations
from portprotonqt.custom_widgets import FlowLayout, ClickableLabel, AutoSizeButton, NavLabel
from portprotonqt.custom_widgets import ClickableLabel, AutoSizeButton, NavLabel
from portprotonqt.portproton_api import PortProtonAPI
from portprotonqt.input_manager import InputManager
from portprotonqt.context_menu_manager import ContextMenuManager, CustomLineEdit
@@ -34,9 +34,11 @@ from portprotonqt.localization import _, get_egs_language, read_metadata_transla
from portprotonqt.howlongtobeat_api import HowLongToBeat
from portprotonqt.downloader import Downloader
from portprotonqt.tray_manager import TrayManager
from portprotonqt.game_library_manager import GameLibraryManager
from PySide6.QtWidgets import (QLineEdit, QMainWindow, QStatusBar, QWidget, QVBoxLayout, QLabel, QHBoxLayout, QStackedWidget, QComboBox, QScrollArea, QSlider,
QDialog, QFormLayout, QFrame, QGraphicsDropShadowEffect, QMessageBox, QApplication, QPushButton, QProgressBar, QCheckBox, QSizePolicy, QScroller)
from PySide6.QtWidgets import (QLineEdit, QMainWindow, QStatusBar, QWidget, QVBoxLayout, QLabel, QHBoxLayout, QStackedWidget, QComboBox,
QDialog, QFormLayout, QFrame, QGraphicsDropShadowEffect, QMessageBox, QApplication, QPushButton, QProgressBar, QCheckBox, QSizePolicy)
from PySide6.QtCore import Qt, QAbstractAnimation, QUrl, Signal, QTimer, Slot
from PySide6.QtGui import QIcon, QPixmap, QColor, QDesktopServices
from typing import cast
@@ -47,14 +49,12 @@ from datetime import datetime
logger = get_logger(__name__)
class MainWindow(QMainWindow):
"""Main window of PortProtonQt."""
games_loaded = Signal(list)
update_progress = Signal(int) # Signal to update progress bar
update_status_message = Signal(str, int) # Signal to update status message
update_progress = Signal(int)
update_status_message = Signal(str, int)
def __init__(self, app_name: str):
super().__init__()
# Создаём менеджер тем и читаем, какая тема выбрана
self.theme_manager = ThemeManager()
self.is_exiting = False
selected_theme = read_theme_from_config()
@@ -62,50 +62,50 @@ class MainWindow(QMainWindow):
self.theme = self.theme_manager.apply_theme(selected_theme)
self.tray_manager = TrayManager(self, app_name, self.current_theme_name)
self.card_width = read_card_size()
self._last_card_width = self.card_width
self.setWindowTitle(app_name)
self.setMinimumSize(800, 600)
self.games = []
self.filtered_games = self.games
self.game_processes = []
self.target_exe = None
self.current_running_button = None
self.portproton_location = get_portproton_location()
self.game_library_manager = GameLibraryManager(self, self.theme, None)
self.context_menu_manager = ContextMenuManager(
self,
self.portproton_location,
self.theme,
self.loadGames,
self.updateGameGrid
self.game_library_manager.update_game_grid
)
self.game_library_manager.context_menu_manager = self.context_menu_manager
QApplication.setStyle("Fusion")
self.setStyleSheet(self.theme.MAIN_WINDOW_STYLE)
self.setAcceptDrops(True)
self.current_exec_line = None
self.currentDetailPage = None
self.current_play_button = None
self.current_focused_card = None
self.current_focused_card: GameCard | None = None
self.current_hovered_card: GameCard | None = None
self.pending_games = []
self.game_card_cache = {}
self.pending_images = {}
self.total_games = 0
self.games_load_timer = QTimer(self)
self.games_load_timer.setSingleShot(True)
self.games_load_timer.timeout.connect(self.finalize_game_loading)
self.games_loaded.connect(self.on_games_loaded)
self.current_add_game_dialog = None
self.current_hovered_card = None
# Добавляем таймер для дебаунсинга сохранения настроек
self.settingsDebounceTimer = QTimer(self)
self.settingsDebounceTimer.setSingleShot(True)
self.settingsDebounceTimer.setInterval(300) # 300 мс задержка
self.settingsDebounceTimer.setInterval(300)
self.settingsDebounceTimer.timeout.connect(self.applySettingsDelayed)
read_time_config()
# Set LEGENDARY_CONFIG_PATH to ~/.cache/PortProtonQt/legendary_cache
self.legendary_config_path = os.path.join(
os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache")),
"PortProtonQt", "legendary_cache"
@@ -144,7 +144,7 @@ class MainWindow(QMainWindow):
headerLayout.setContentsMargins(0, 0, 0, 0)
headerLayout.addStretch()
self.input_manager = InputManager(self)
self.input_manager = InputManager(self) # type: ignore
self.input_manager.button_pressed.connect(self.updateControlHints)
self.input_manager.dpad_moved.connect(self.updateControlHints)
@@ -196,15 +196,13 @@ class MainWindow(QMainWindow):
self.stackedWidget = QStackedWidget()
mainLayout.addWidget(self.stackedWidget)
# Создаём все вкладки
self.createInstalledTab() # вкладка 0
self.createAutoInstallTab() # вкладка 1
self.createEmulatorsTab() # вкладка 2
self.createWineTab() # вкладка 3
self.createPortProtonTab() # вкладка 4
self.createThemeTab() # вкладка 5
self.createInstalledTab()
self.createAutoInstallTab()
self.createEmulatorsTab()
self.createWineTab()
self.createPortProtonTab()
self.createThemeTab()
# Подсказки управления
self.controlHintsWidget = self.createControlHintsWidget()
mainLayout.addWidget(self.controlHintsWidget)
@@ -222,6 +220,11 @@ class MainWindow(QMainWindow):
else:
self.showNormal()
def on_slider_released(self) -> None:
"""Delegate to game library manager."""
if hasattr(self, 'game_library_manager'):
self.game_library_manager.on_slider_released()
def get_button_icon(self, action: str, gtype: GamepadType) -> str:
"""Get the icon name for a specific action and gamepad type."""
mappings = {
@@ -429,31 +432,7 @@ class MainWindow(QMainWindow):
@Slot(list)
def on_games_loaded(self, games: list[tuple]):
self.games = games
favorites = read_favorites()
sort_method = read_sort_method()
# Sort by: favorites first, then descending playtime, then descending last launch
if sort_method == "playtime":
self.games.sort(key=lambda g: (0 if g[0] in favorites else 1, -g[11], -g[10]))
# Sort by: favorites first, then alphabetically by game name
elif sort_method == "alphabetical":
self.games.sort(key=lambda g: (0 if g[0] in favorites else 1, g[0].lower()))
# Sort by: favorites first, then leave the rest in their original order
elif sort_method == "favorites":
self.games.sort(key=lambda g: (0 if g[0] in favorites else 1))
# Sort by: favorites first, then descending last launch, then descending playtime
elif sort_method == "last_launch":
self.games.sort(key=lambda g: (0 if g[0] in favorites else 1, -g[10], -g[11]))
# Fallback: same as last_launch
else:
self.games.sort(key=lambda g: (0 if g[0] in favorites else 1, -g[10], -g[11]))
self.updateGameGrid()
self.game_library_manager.set_games(games)
self.progress_bar.setVisible(False)
def open_portproton_forum_topic(self, topic_name: str):
@@ -466,65 +445,6 @@ class MainWindow(QMainWindow):
url = QUrl(f"{base_url}t/{result}")
QDesktopServices.openUrl(url)
def _on_card_focused(self, game_name: str, is_focused: bool):
"""Обработчик сигнала focusChanged от GameCard."""
card_key = None
for key, card in self.game_card_cache.items():
if card.name == game_name:
card_key = key
break
if not card_key:
return
card = self.game_card_cache[card_key]
if is_focused:
# Если карточка получила фокус
if self.current_hovered_card and self.current_hovered_card != card:
# Сбрасываем текущую hovered карточку
self.current_hovered_card._hovered = False
self.current_hovered_card.leaveEvent(None)
self.current_hovered_card = None
if self.current_focused_card and self.current_focused_card != card:
# Сбрасываем текущую focused карточку
self.current_focused_card._focused = False
self.current_focused_card.clearFocus()
self.current_focused_card = card
else:
# Если карточка потеряла фокус
if self.current_focused_card == card:
self.current_focused_card = None
def _on_card_hovered(self, game_name: str, is_hovered: bool):
"""Обработчик сигнала hoverChanged от GameCard."""
card_key = None
for key, card in self.game_card_cache.items():
if card.name == game_name:
card_key = key
break
if not card_key:
return
card = self.game_card_cache[card_key]
if is_hovered:
# Если мышь наведена на карточку
if self.current_focused_card and self.current_focused_card != card:
# Сбрасываем текущую focused карточку
self.current_focused_card._focused = False
self.current_focused_card.clearFocus()
if self.current_hovered_card and self.current_hovered_card != card:
# Сбрасываем предыдущую hovered карточку
self.current_hovered_card._hovered = False
self.current_hovered_card.leaveEvent(None)
self.current_hovered_card = card
else:
# Если мышь покинула карточку
if self.current_hovered_card == card:
self.current_hovered_card = None
def loadGames(self):
display_filter = read_display_filter()
favorites = read_favorites()
@@ -797,7 +717,7 @@ class MainWindow(QMainWindow):
overlay = SystemOverlay(self, self.theme)
overlay.exec()
def createSearchWidget(self) -> tuple[QWidget, QLineEdit]:
def createSearchWidget(self) -> tuple[QWidget, CustomLineEdit]:
self.container = QWidget()
self.container.setStyleSheet(self.theme.CONTAINER_STYLE)
layout = QHBoxLayout(self.container)
@@ -823,12 +743,11 @@ class MainWindow(QMainWindow):
self.searchEdit.setClearButtonEnabled(True)
self.searchEdit.setStyleSheet(self.theme.SEARCH_EDIT_STYLE)
# Добавляем дебансирование для поиска
self.searchEdit.textChanged.connect(self.startSearchDebounce)
self.searchDebounceTimer = QTimer(self)
self.searchDebounceTimer.setSingleShot(True)
self.searchDebounceTimer.setInterval(300)
self.searchDebounceTimer.timeout.connect(self.filterGamesDelayed)
self.searchDebounceTimer.timeout.connect(self.game_library_manager.filter_games_delayed)
layout.addWidget(self.searchEdit)
return self.container, self.searchEdit
@@ -836,76 +755,10 @@ class MainWindow(QMainWindow):
def startSearchDebounce(self, text):
self.searchDebounceTimer.start()
def on_slider_released(self):
self.card_width = self.sizeSlider.value()
self.sizeSlider.setToolTip(f"{self.card_width} px")
save_card_size(self.card_width)
for card in self.game_card_cache.values():
card.update_card_size(self.card_width)
self.updateGameGrid()
def filterGamesDelayed(self):
"""Filters games based on search text and updates the grid."""
text = self.searchEdit.text().strip().lower()
if text == "":
self.filtered_games = self.games
else:
self.filtered_games = [game for game in self.games if text in game[0].lower()]
self.updateGameGrid(self.filtered_games)
def createInstalledTab(self):
self.gamesLibraryWidget = QWidget()
self.gamesLibraryWidget.setStyleSheet(self.theme.LIBRARY_WIDGET_STYLE)
layout = QVBoxLayout(self.gamesLibraryWidget)
layout.setSpacing(15)
searchWidget, self.searchEdit = self.createSearchWidget()
layout.addWidget(searchWidget)
scrollArea = QScrollArea()
scrollArea.setWidgetResizable(True)
scrollArea.setStyleSheet(self.theme.SCROLL_AREA_STYLE)
QScroller.grabGesture(scrollArea.viewport(), QScroller.ScrollerGestureType.LeftMouseButtonGesture)
self.gamesListWidget = QWidget()
self.gamesListWidget.setStyleSheet(self.theme.LIST_WIDGET_STYLE)
self.gamesListLayout = FlowLayout(self.gamesListWidget)
self.gamesListWidget.setLayout(self.gamesListLayout)
scrollArea.setWidget(self.gamesListWidget)
layout.addWidget(scrollArea)
sliderLayout = QHBoxLayout()
sliderLayout.addStretch()
# Слайдер
self.sizeSlider = QSlider(Qt.Orientation.Horizontal)
self.sizeSlider.setMinimum(200)
self.sizeSlider.setMaximum(250)
self.sizeSlider.setValue(self.card_width)
self.sizeSlider.setTickInterval(10)
self.sizeSlider.setFixedWidth(150)
self.sizeSlider.setToolTip(f"{self.card_width} px")
self.sizeSlider.setStyleSheet(self.theme.SLIDER_SIZE_STYLE)
self.sizeSlider.sliderReleased.connect(self.on_slider_released)
sliderLayout.addWidget(self.sizeSlider)
layout.addLayout(sliderLayout)
def calculate_card_width():
available_width = scrollArea.width() - 20
spacing = self.gamesListLayout._spacing
target_cards_per_row = 8
calculated_width = (available_width - spacing * (target_cards_per_row - 1)) // target_cards_per_row
calculated_width = max(200, min(calculated_width, 250))
QTimer.singleShot(0, calculate_card_width)
# Добавляем обработчик прокрутки для ленивой загрузки
scrollArea.verticalScrollBar().valueChanged.connect(self.loadVisibleImages)
self.gamesLibraryWidget = self.game_library_manager.create_games_library_widget()
self.stackedWidget.addWidget(self.gamesLibraryWidget)
self.updateGameGrid()
self.game_library_manager.update_game_grid()
def resizeEvent(self, event):
super().resizeEvent(event)
@@ -923,135 +776,6 @@ class MainWindow(QMainWindow):
if abs(self.width() - self._last_width) > 10:
self._last_width = self.width()
def loadVisibleImages(self):
visible_region = self.gamesListWidget.visibleRegion()
max_concurrent_loads = 5
loaded_count = 0
for card_key, card in self.game_card_cache.items():
if card_key in self.pending_images and visible_region.contains(card.pos()) and loaded_count < max_concurrent_loads:
cover_path, width, height, callback = self.pending_images.pop(card_key)
load_pixmap_async(cover_path, width, height, callback)
loaded_count += 1
def updateGameGrid(self, games_list=None):
"""Обновляет сетку игровых карточек с сохранением порядка сортировки"""
# Подготовка данных
games_list = games_list if games_list is not None else self.games
search_text = self.searchEdit.text().strip().lower()
favorites = read_favorites()
sort_method = read_sort_method()
# Сортируем игры согласно текущим настройкам
def sort_key(game):
name = game[0]
# Избранные всегда первые
if name in favorites:
fav_order = 0
else:
fav_order = 1
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
sorted_games = sorted(games_list, key=sort_key)
# Создаем временный список для новых карточек
new_card_order = []
# Обрабатываем каждую игру в отсортированном порядке
for game_data in sorted_games:
game_name = game_data[0]
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
# Создаем новую карточку
card = GameCard(
*game_data,
select_callback=self.openGameDetailPage,
theme=self.theme,
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)
card.addToMenuRequested.connect(self.context_menu_manager.add_to_menu)
card.removeFromMenuRequested.connect(self.context_menu_manager.remove_from_menu)
card.addToDesktopRequested.connect(self.context_menu_manager.add_to_desktop)
card.removeFromDesktopRequested.connect(self.context_menu_manager.remove_from_desktop)
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
new_card_order.append((game_key, card))
card.setVisible(should_be_visible)
# Полностью перестраиваем макет в правильном порядке, чистим 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]
# Принудительно обновляем макет
self.gamesListLayout.update()
self.gamesListWidget.updateGeometry()
self.gamesListWidget.update()
# Сохраняем текущий размер карточек
self._last_card_width = self.card_width
def clearLayout(self, layout):
"""Удаляет все виджеты из layout."""
while layout.count():
child = layout.takeAt(0)
if child.widget():
widget = child.widget()
# Remove from game_card_cache if it's a GameCard
for key, card in list(self.game_card_cache.items()):
if card == widget:
del self.game_card_cache[key]
# Also remove from pending_images if present
if key in self.pending_images:
del self.pending_images[key]
widget.deleteLater()
def dragEnterEvent(self, event):
if event.mimeData().hasUrls():
@@ -1069,26 +793,22 @@ class MainWindow(QMainWindow):
break
def openAddGameDialog(self, exe_path=None):
"""Открывает диалоговое окно 'Add Game' с текущей темой."""
# Проверяем, открыт ли уже диалог
if self.current_add_game_dialog is not None and self.current_add_game_dialog.isVisible():
self.current_add_game_dialog.activateWindow() # Активируем существующий диалог
self.current_add_game_dialog.raise_() # Поднимаем окно
self.current_add_game_dialog.activateWindow()
self.current_add_game_dialog.raise_()
return
dialog = AddGameDialog(self, self.theme)
dialog.setFocus(Qt.FocusReason.OtherFocusReason)
self.current_add_game_dialog = dialog # Сохраняем ссылку на диалог
self.current_add_game_dialog = dialog
# Предзаполняем путь к .exe при drag-and-drop
if exe_path:
dialog.exeEdit.setText(exe_path)
dialog.nameEdit.setText(os.path.splitext(os.path.basename(exe_path))[0])
dialog.updatePreview()
# Обработчик закрытия диалога
def on_dialog_finished():
self.current_add_game_dialog = None # Сбрасываем ссылку при закрытии
self.current_add_game_dialog = None
dialog.finished.connect(on_dialog_finished)
@@ -1100,14 +820,12 @@ class MainWindow(QMainWindow):
if not name or not exe_path:
return
# Сохраняем .desktop файл
desktop_entry, desktop_path = dialog.getDesktopEntryData()
if desktop_entry and desktop_path:
with open(desktop_path, "w", encoding="utf-8") as f:
f.write(desktop_entry)
os.chmod(desktop_path, 0o755)
# Проверяем путь обложки, если он отличается от стандартной
if os.path.isfile(user_cover):
exe_name = os.path.splitext(os.path.basename(exe_path))[0]
xdg_data_home = os.getenv("XDG_DATA_HOME",
@@ -1119,14 +837,12 @@ class MainWindow(QMainWindow):
exe_name
)
os.makedirs(custom_folder, exist_ok=True)
# Сохраняем пользовательскую обложку как cover.*
ext = os.path.splitext(user_cover)[1].lower()
if ext in [".png", ".jpg", ".jpeg", ".bmp"]:
shutil.copyfile(user_cover, os.path.join(custom_folder, f"cover{ext}"))
self.games = self.loadGames()
self.updateGameGrid()
self.game_library_manager.update_game_grid()
def createAutoInstallTab(self):
"""Вкладка 'Auto Install'."""
@@ -1488,18 +1204,14 @@ class MainWindow(QMainWindow):
self.statusBar().showMessage(_("Cache cleared"), 3000)
def applySettingsDelayed(self):
"""Applies settings with the new filter and updates the game list."""
read_time_config()
self.games = []
self.loadGames()
display_filter = read_display_filter()
for card in self.game_card_cache.values():
for card in self.game_library_manager.game_card_cache.values():
card.update_badge_visibility(display_filter)
def savePortProtonSettings(self):
"""
Сохраняет параметры конфигурации в конфигурационный файл.
"""
time_idx = self.timeDetailCombo.currentIndex()
time_key = self.time_keys[time_idx]
save_time_config(time_key)
@@ -1512,7 +1224,6 @@ class MainWindow(QMainWindow):
filter_key = self.filter_keys[filter_idx]
save_display_filter(filter_key)
# Сохранение proxy настроек
proxy_url = self.proxyUrlEdit.text().strip()
proxy_user = self.proxyUserEdit.text().strip()
proxy_password = self.proxyPasswordEdit.text().strip()
@@ -1524,11 +1235,10 @@ class MainWindow(QMainWindow):
auto_fullscreen_gamepad = self.autoFullscreenGamepadCheckBox.isChecked()
save_auto_fullscreen_gamepad(auto_fullscreen_gamepad)
# Сохранение настройки виброотдачи геймпада
rumble_enabled = self.gamepadRumbleCheckBox.isChecked()
save_rumble_config(rumble_enabled)
for card in self.game_card_cache.values():
for card in self.game_library_manager.game_card_cache.values():
card.update_badge_visibility(filter_key)
if self.currentDetailPage and self.current_exec_line:
@@ -1541,14 +1251,12 @@ class MainWindow(QMainWindow):
self.settingsDebounceTimer.start()
# Управление полноэкранным режимом
gamepad_connected = self.input_manager.find_gamepad() is not None
if fullscreen or (auto_fullscreen_gamepad and gamepad_connected):
self.showFullScreen()
else:
# Если обе галочки сняты и геймпад не подключен, возвращаем нормальное состояние
self.showNormal()
self.resize(*read_window_geometry()) # Восстанавливаем сохраненные размеры окна
self.resize(*read_window_geometry())
self.statusBar().showMessage(_("Settings saved"), 3000)
@@ -2130,7 +1838,7 @@ class MainWindow(QMainWindow):
favorites.append(game_name)
label.setText("")
save_favorites(favorites)
self.updateGameGrid()
self.game_library_manager.update_game_grid()
def activateFocusedWidget(self):
"""Activate the currently focused widget."""