diff --git a/rare/components/dialogs/launch_dialog.py b/rare/components/dialogs/launch_dialog.py index 812d5074..27798a73 100644 --- a/rare/components/dialogs/launch_dialog.py +++ b/rare/components/dialogs/launch_dialog.py @@ -46,11 +46,6 @@ class ImageWorker(LaunchWorker): na_dlc_list = [dlc[0] for dlc in na_dlcs.values()] game_list = games + dlc_list + na_games + na_dlc_list - fetched = [False] * len(game_list) - - def set_fetched(idx): - fetched[idx] = True - self.signals.progress.emit(sum(fetched)) for i, game in enumerate(game_list): if game.app_title == "Unreal Engine": @@ -58,13 +53,6 @@ class ImageWorker(LaunchWorker): self.core.lgd.set_game_meta(game.app_name, game) self.image_manager.download_image_blocking(game) self.signals.progress.emit(int(i / len(game_list) * 100)) - # self.image_manager.download_image( - # game, - # load_callback=lambda: set_fetched(i), - # priority=i - # ) - # while not all(fetched): - # continue self.signals.progress.emit(100) diff --git a/rare/components/tabs/downloads/__init__.py b/rare/components/tabs/downloads/__init__.py index bcd157df..17225e39 100644 --- a/rare/components/tabs/downloads/__init__.py +++ b/rare/components/tabs/downloads/__init__.py @@ -283,7 +283,7 @@ class UpdateWidget(QWidget): + self.game.version + " -> " + self.core.get_asset( - self.game.app_name, self.game.platform, True + self.game.app_name, self.game.platform, False ).build_version ) ) diff --git a/rare/components/tabs/games/__init__.py b/rare/components/tabs/games/__init__.py index e26914e3..e1f8f00e 100644 --- a/rare/components/tabs/games/__init__.py +++ b/rare/components/tabs/games/__init__.py @@ -1,8 +1,8 @@ from logging import getLogger from typing import Tuple, Dict, Union, List -from PyQt5.QtCore import QSettings, QObjectCleanupHandler -from PyQt5.QtWidgets import QStackedWidget, QVBoxLayout, QWidget +from PyQt5.QtCore import QSettings, Qt, pyqtSlot +from PyQt5.QtWidgets import QStackedWidget, QVBoxLayout, QWidget, QScrollArea, QFrame from legendary.models.game import InstalledGame, Game from rare.shared import ( @@ -14,6 +14,7 @@ from rare.shared import ( from rare.shared.image_manager import ImageManagerSingleton from rare.ui.components.tabs.games.games_tab import Ui_GamesTab from rare.widgets.library_layout import LibraryLayout +from rare.widgets.sliding_stack import SlidingStackedWidget from .cloud_save_utils import CloudSaveUtils from .game_info import GameInfoTabs from .game_info.uninstalled_info import UninstalledInfoTabs @@ -23,24 +24,26 @@ from .game_widgets.base_uninstalled_widget import BaseUninstalledWidget from .game_widgets.installed_icon_widget import InstalledIconWidget from .game_widgets.installed_list_widget import InstalledListWidget from .game_widgets.installing_game_widget import InstallingGameWidget -from .game_widgets.uninstalled_icon_widget import IconWidgetUninstalled -from .game_widgets.uninstalled_list_widget import ListWidgetUninstalled +from .game_widgets.uninstalled_icon_widget import UninstalledIconWidget +from .game_widgets.uninstalled_list_widget import UninstalledListWidget from .head_bar import GameListHeadBar from .import_sync import ImportSyncTabs logger = getLogger("GamesTab") -class GamesTab(QStackedWidget, Ui_GamesTab): +class GamesTab(QStackedWidget): widgets: Dict[str, Tuple[ - Union[InstalledIconWidget, IconWidgetUninstalled], Union[InstalledListWidget, ListWidgetUninstalled]]] = dict() + Union[InstalledIconWidget, UninstalledIconWidget], Union[InstalledListWidget, UninstalledListWidget]]] = dict() running_games = list() updates = set() active_filter = 0 def __init__(self, parent=None): super(GamesTab, self).__init__(parent=parent) - self.setupUi(self) + self.ui = Ui_GamesTab() + self.ui.setupUi(self) + self.core = LegendaryCoreSingleton() self.signals = GlobalSignalsSingleton() self.args = ArgumentsSingleton() @@ -59,7 +62,7 @@ class GamesTab(QStackedWidget, Ui_GamesTab): self.head_bar = GameListHeadBar(self) self.head_bar.import_clicked.connect(self.show_import) self.head_bar.egl_sync_clicked.connect(self.show_egl_sync) - self.games.layout().insertWidget(0, self.head_bar) + self.ui.games.layout().insertWidget(0, self.head_bar) self.game_info_tabs = GameInfoTabs(self.dlcs, self.game_utils, self) self.game_info_tabs.back_clicked.connect(lambda: self.setCurrentIndex(0)) @@ -101,13 +104,41 @@ class GamesTab(QStackedWidget, Ui_GamesTab): self.no_assets = [] self.installed = self.core.get_installed_list() - self.setup_game_list() + + self.view_stack = SlidingStackedWidget(self.ui.library_frame) + self.view_stack.setFrameStyle(QFrame.NoFrame) + self.icon_view_scroll = QScrollArea(self.view_stack) + self.icon_view_scroll.setWidgetResizable(True) + self.icon_view_scroll.setFrameShape(QFrame.NoFrame) + self.icon_view_scroll.horizontalScrollBar().setDisabled(True) + self.list_view_scroll = QScrollArea(self.view_stack) + self.list_view_scroll.setWidgetResizable(True) + self.list_view_scroll.setFrameShape(QFrame.NoFrame) + self.list_view_scroll.horizontalScrollBar().setDisabled(True) + self.icon_view = QWidget(self.icon_view_scroll) + self.icon_view.setLayout(LibraryLayout(self.icon_view)) + self.icon_view.layout().setContentsMargins(0, 0, 0, 0) + self.icon_view.layout().setAlignment(Qt.AlignTop) + self.list_view = QWidget(self.list_view_scroll) + self.list_view.setLayout(QVBoxLayout(self.list_view)) + self.list_view.layout().setContentsMargins(3, 3, 9, 3) + self.list_view.layout().setAlignment(Qt.AlignTop) + self.icon_view_scroll.setWidget(self.icon_view) + self.list_view_scroll.setWidget(self.list_view) + self.view_stack.addWidget(self.icon_view_scroll) + self.view_stack.addWidget(self.list_view_scroll) + self.ui.library_frame_layout.addWidget(self.view_stack) + + # add installing game widget for icon view: List view not supported + self.installing_widget = InstallingGameWidget() + self.icon_view.layout().addWidget(self.installing_widget) + self.installing_widget.setVisible(False) if not self.settings.value("icon_view", True, bool): - self.scroll_widget.layout().insertWidget(1, self.list_view) + self.view_stack.setCurrentWidget(self.list_view_scroll) self.head_bar.view.list() else: - self.scroll_widget.layout().insertWidget(1, self.icon_view) + self.view_stack.setCurrentWidget(self.icon_view_scroll) self.head_bar.search_bar.textChanged.connect(lambda x: self.filter_games("", x)) self.head_bar.filterChanged.connect(self.filter_games) @@ -118,7 +149,6 @@ class GamesTab(QStackedWidget, Ui_GamesTab): if f >= len(self.head_bar.available_filters): f = 0 self.active_filter = self.head_bar.available_filters[f] - self.filter_games(self.active_filter) # signals self.signals.dl_progress.connect(self.installing_widget.set_status) @@ -131,7 +161,7 @@ class GamesTab(QStackedWidget, Ui_GamesTab): self.game_utils.update_list.connect(self.update_list) - self.game_list_scroll_area.horizontalScrollBar().setDisabled(True) + self.setup_game_list() def installation_finished(self, app_name: str): self.installing_widget.setVisible(False) @@ -178,19 +208,17 @@ class GamesTab(QStackedWidget, Ui_GamesTab): self.uninstalled_info_tabs.update_game(game) self.setCurrentIndex(3) + @pyqtSlot() + def update_count_games_label(self): + self.ui.count_games_label.setText( + self.tr("Installed Games: {}\tAvailable Games: {}").format( + len(self.core.get_installed_list()), len(self.game_list) + ) + ) + def setup_game_list(self): - self.icon_view = QWidget() - self.icon_view.setLayout(LibraryLayout()) - self.list_view = QWidget() - self.list_view.setLayout(QVBoxLayout()) - self.update_count_games_label() - # add installing game widget for icon view: List view not supported - self.installing_widget = InstallingGameWidget() - self.icon_view.layout().addWidget(self.installing_widget) - self.installing_widget.setVisible(False) - # add installed games for igame in sorted(self.core.get_installed_list(), key=lambda x: x.title): icon_widget, list_widget = self.add_installed_widget(igame.app_name) @@ -212,13 +240,7 @@ class GamesTab(QStackedWidget, Ui_GamesTab): icon_widget, list_widget = self.add_uninstalled_widget(game) self.icon_view.layout().addWidget(icon_widget) self.list_view.layout().addWidget(list_widget) - - def update_count_games_label(self): - self.count_games_label.setText( - self.tr("Installed Games: {} Available Games: {}").format( - len(self.core.get_installed_list()), len(self.game_list) - ) - ) + self.filter_games(self.active_filter) def add_installed_widget(self, app_name): pixmap = self.image_manager.get_pixmap(app_name) @@ -257,8 +279,8 @@ class GamesTab(QStackedWidget, Ui_GamesTab): elif self.ue_name: pixmap = self.image_manager.get_pixmap(self.ue_name, color=False) - icon_widget = IconWidgetUninstalled(game, self.core, pixmap) - list_widget = ListWidgetUninstalled(self.core, game, pixmap) + icon_widget = UninstalledIconWidget(game, self.core, pixmap) + list_widget = UninstalledListWidget(self.core, game, pixmap) except Exception as e: logger.error(f"{game.app_name} is broken. Don't add it to game list: {e}") return None, None @@ -279,7 +301,7 @@ class GamesTab(QStackedWidget, Ui_GamesTab): if not filter_name and (t := self.active_filter): filter_name = t - def get_visibility(widget): + def get_visibility(widget) -> Tuple[bool, float]: app_name = widget.game.app_name if not isinstance(widget, @@ -310,19 +332,50 @@ class GamesTab(QStackedWidget, Ui_GamesTab): visible = True if ( - search_text.lower() not in app_name.lower() - and search_text.lower() not in widget.game.app_title.lower() + search_text not in widget.game.app_name.lower() + and search_text not in widget.game.app_title.lower() ): - visible = False - return visible + opacity = 0.25 + else: + opacity = 1.0 + return visible, opacity for t in self.widgets.values(): - visible = get_visibility(t[0]) + visible, opacity = get_visibility(t[0]) for w in t: w.setVisible(visible) + w.image.setOpacity(opacity) + + self.sort_list(search_text) if self.installing_widget.game: - self.installing_widget.setVisible(get_visibility(self.installing_widget)) + self.installing_widget.setVisible(get_visibility(self.installing_widget)[0]) + + @pyqtSlot() + def sort_list(self, sort_by: str = ""): + # lk: this is the existing sorting implemenation + # lk: it sorts by installed then by title + installing_widget = self.icon_view.layout().remove(type(self.installing_widget).__name__) + if sort_by: + self.icon_view.layout().sort(lambda x: (sort_by not in x.widget().game.app_title.lower(),)) + else: + self.icon_view.layout().sort( + lambda x: ( + not x.widget().is_installed, + x.widget().is_non_asset, + x.widget().app_title, + ) + ) + self.icon_view.layout().insert(0, installing_widget) + list_widgets = self.list_view.findChildren(InstalledListWidget) + self.list_view.findChildren(UninstalledListWidget) + if sort_by: + list_widgets.sort(key=lambda x: (sort_by not in x.game.app_title.lower(),)) + else: + list_widgets.sort( + key=lambda x: (not x.is_installed, x.is_non_asset, x.app_title) + ) + for idx, wl in enumerate(list_widgets): + self.list_view.layout().insertWidget(idx, wl) def update_list(self, app_names: list = None): logger.debug(f"Updating list for {app_names}") @@ -351,11 +404,15 @@ class GamesTab(QStackedWidget, Ui_GamesTab): widgets[0], BaseUninstalledWidget ): logger.debug(f"Update Gamelist: New installed {app_name}") + self.icon_view.layout().removeWidget(self.widgets[app_name][0]) + self.list_view.layout().removeWidget(self.widgets[app_name][1]) self.widgets[app_name][0].deleteLater() self.widgets[app_name][1].deleteLater() self.widgets.pop(app_name) - self.add_installed_widget(app_name) + icon_widget, list_widget = self.add_installed_widget(app_name) + self.icon_view.layout().addWidget(icon_widget) + self.list_view.layout().addWidget(list_widget) update_list = True # uninstalled @@ -363,6 +420,8 @@ class GamesTab(QStackedWidget, Ui_GamesTab): widgets[0].game.app_name ) and isinstance(widgets[0], BaseInstalledWidget): logger.debug(f"Update list: Uninstalled: {app_name}") + self.icon_view.layout().removeWidget(self.widgets[app_name][0]) + self.list_view.layout().removeWidget(self.widgets[app_name][1]) self.widgets[app_name][0].deleteLater() self.widgets[app_name][1].deleteLater() @@ -370,14 +429,16 @@ class GamesTab(QStackedWidget, Ui_GamesTab): game = self.core.get_game(app_name, False) try: - self.add_uninstalled_widget(game) + icon_widget, list_widget = self.add_uninstalled_widget(game) + self.icon_view.layout().addWidget(icon_widget) + self.list_view.layout().addWidget(list_widget) except Exception: pass update_list = True # do not update, if only update finished if update_list: - self._update_games() + self.sort_list() else: installed_names = [i.app_name for i in self.core.get_installed_list()] @@ -401,11 +462,15 @@ class GamesTab(QStackedWidget, Ui_GamesTab): if new_installed_games: for name in new_installed_games: + self.icon_view.layout().removeWidget(self.widgets[name][0]) + self.list_view.layout().removeWidget(self.widgets[name][1]) self.widgets[name][0].deleteLater() self.widgets[name][1].deleteLater() self.widgets.pop(name) - self.add_installed_widget(name) + icon_widget, list_widget = self.add_installed_widget(name) + self.icon_view.layout().addWidget(icon_widget) + self.list_view.layout().addWidget(list_widget) for name in new_uninstalled_games: self.icon_view.layout().removeWidget(self.widgets[name][0]) @@ -418,104 +483,19 @@ class GamesTab(QStackedWidget, Ui_GamesTab): game = self.core.get_game(name, False) try: - self.add_uninstalled_widget(game) + icon_widget, list_widget = self.add_uninstalled_widget(game) + self.icon_view.layout().addWidget(icon_widget) + self.list_view.layout().addWidget(list_widget) except Exception: pass - for igame in sorted( - self.core.get_installed_list(), key=lambda x: x.title - ): - i_widget, list_widget = self.widgets[igame.app_name] - - self.icon_view.layout().addWidget(i_widget) - self.list_view.layout().addWidget(list_widget) - self.installed = self.core.get_installed_list() - - for game in self.no_assets: - i_widget, list_widget = self.widgets[game.app_name] - self.icon_view.layout().addWidget(i_widget) - self.list_view.layout().addWidget(list_widget) - - # get Uninstalled games - self.uninstalled_games = [] - games, self.dlcs = self.core.get_game_and_dlc_list() - for game in sorted(games, key=lambda x: x.app_title): - if ( - not self.core.is_installed(game.app_name) - and game.app_name not in self.no_asset_names - ): - i_widget, list_widget = self.widgets[game.app_name] - self.icon_view.layout().addWidget(i_widget) - self.list_view.layout().addWidget(list_widget) - self.uninstalled_games.append(game) + self.sort_list() self.update_count_games_label() - def _update_games(self): - icon_layout = FlowLayout() - list_layout = QVBoxLayout() - - icon_layout.addWidget(self.installing_widget) - - for igame in sorted(self.core.get_installed_list(), key=lambda x: x.title) + self.no_assets: - i_widget, l_widget = self.widgets.get(igame.app_name, (None, None)) - if i_widget and l_widget: - icon_layout.addWidget(i_widget) - list_layout.addWidget(l_widget) - else: - logger.warning("Found installed game, without widget. Generating widget... ") - try: - i_widget, l_widget = self.add_installed_widget(igame.app_name) - icon_layout.addWidget(i_widget) - list_layout.addWidget(l_widget) - except Exception as e: - logger.error(str(e)) - continue - - # get Uninstalled games - self.game_list, self.dlcs = self.core.get_game_and_dlc_list(update_assets=False) - # add uninstalled games - for game in sorted(self.game_list, key=lambda x: x.app_title): - if self.core.is_installed(game.app_name) or game.app_name in self.no_asset_names: - continue - i_widget, list_widget = self.widgets.get(game.app_name, (None, None)) - if i_widget and list_widget: - icon_layout.addWidget(i_widget) - list_layout.addWidget(list_widget) - else: - logger.warning("Found installed game, without widget. Generating widget... ") - try: - i_widget, l_widget = self.add_uninstalled_widget(game) - if not i_widget or not l_widget: - logger.warning(f"Ignoring {game.app_name}") - continue - icon_layout.addWidget(i_widget) - list_layout.addWidget(l_widget) - except Exception as e: - logger.error(str(e)) - continue - - QObjectCleanupHandler().add(self.icon_view.layout()) - QObjectCleanupHandler().add(self.list_view.layout()) - - self.icon_view.setLayout(icon_layout) - self.list_view.setLayout(list_layout) - - self.icon_view.setParent(None) - self.list_view.setParent(None) - - # insert widget in layout - self.scroll_widget.layout().insertWidget( - 1, self.icon_view if self.head_bar.view.isChecked() else self.list_view - ) - def toggle_view(self): self.settings.setValue("icon_view", not self.head_bar.view.isChecked()) if not self.head_bar.view.isChecked(): - self.scroll_widget.layout().replaceWidget(self.list_view, self.icon_view) - self.list_view.setParent(None) + self.view_stack.slideInWidget(self.icon_view_scroll) else: - self.scroll_widget.layout().replaceWidget(self.icon_view, self.list_view) - self.icon_view.setParent(None) - - self.game_list_scroll_area.verticalScrollBar().setValue(0) + self.view_stack.slideInWidget(self.list_view_scroll) diff --git a/rare/components/tabs/games/game_widgets/base_installed_widget.py b/rare/components/tabs/games/game_widgets/base_installed_widget.py index 7bab425e..e7c30b1d 100644 --- a/rare/components/tabs/games/game_widgets/base_installed_widget.py +++ b/rare/components/tabs/games/game_widgets/base_installed_widget.py @@ -217,3 +217,33 @@ class BaseInstalledWidget(QFrame): if error: QMessageBox.warning(self, "Error", error) self.game_running = False + + # From RareGame, added from sorting to work + @property + def is_non_asset(self) -> bool: + """! + @brief Property to report if a Game doesn't have assets + + Typically, games have assets, however some games that require + other launchers do not have them. Rare treats these games as installed + offering to execute their launcher. + + @return bool If the game doesn't have assets + """ + return not self.game.asset_infos + + @property + def is_installed(self) -> bool: + """! + @brief Property to report if a game is installed + + This returns True if InstalledGame data have been loaded for the game + or if the game is a game without assets, for example an Origin game. + + @return bool If the game should be considered installed + """ + return (self.igame is not None) or self.is_non_asset + + @property + def app_title(self) -> str: + return self.igame.title if self.igame is not None else self.game.app_title diff --git a/rare/components/tabs/games/game_widgets/base_uninstalled_widget.py b/rare/components/tabs/games/game_widgets/base_uninstalled_widget.py index ac1682d9..01860985 100644 --- a/rare/components/tabs/games/game_widgets/base_uninstalled_widget.py +++ b/rare/components/tabs/games/game_widgets/base_uninstalled_widget.py @@ -39,3 +39,33 @@ class BaseUninstalledWidget(QFrame): def install(self): self.show_uninstalled_info.emit(self.game) + + # From RareGame, added from sorting to work + @property + def is_non_asset(self) -> bool: + """! + @brief Property to report if a Game doesn't have assets + + Typically, games have assets, however some games that require + other launchers do not have them. Rare treats these games as installed + offering to execute their launcher. + + @return bool If the game doesn't have assets + """ + return not self.game.asset_infos + + @property + def is_installed(self) -> bool: + """! + @brief Property to report if a game is installed + + This returns True if InstalledGame data have been loaded for the game + or if the game is a game without assets, for example an Origin game. + + @return bool If the game should be considered installed + """ + return False or self.is_non_asset + + @property + def app_title(self) -> str: + return self.game.app_title diff --git a/rare/components/tabs/games/game_widgets/installed_icon_widget.py b/rare/components/tabs/games/game_widgets/installed_icon_widget.py index d30825d6..7b995089 100644 --- a/rare/components/tabs/games/game_widgets/installed_icon_widget.py +++ b/rare/components/tabs/games/game_widgets/installed_icon_widget.py @@ -20,7 +20,7 @@ class InstalledIconWidget(BaseInstalledWidget): def __init__(self, app_name, pixmap, game_utils): super(InstalledIconWidget, self).__init__(app_name, pixmap, game_utils) - self.setObjectName("game_widget_icon") + self.setObjectName(type(self).__name__) self.setContextMenuPolicy(Qt.ActionsContextMenu) layout = QVBoxLayout() diff --git a/rare/components/tabs/games/game_widgets/installing_game_widget.py b/rare/components/tabs/games/game_widgets/installing_game_widget.py index 6868d113..f2104e13 100644 --- a/rare/components/tabs/games/game_widgets/installing_game_widget.py +++ b/rare/components/tabs/games/game_widgets/installing_game_widget.py @@ -17,7 +17,7 @@ class InstallingGameWidget(QFrame): layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) self.setFixedWidth(ImageSize.Display.size.width()) - self.setObjectName("game_widget_icon") + self.setObjectName(type(self).__name__) self.core = LegendaryCoreSingleton() self.image_manager = ImageManagerSingleton() diff --git a/rare/components/tabs/games/game_widgets/uninstalled_icon_widget.py b/rare/components/tabs/games/game_widgets/uninstalled_icon_widget.py index 5ea06958..f3daf240 100644 --- a/rare/components/tabs/games/game_widgets/uninstalled_icon_widget.py +++ b/rare/components/tabs/games/game_widgets/uninstalled_icon_widget.py @@ -14,12 +14,12 @@ from rare.widgets.elide_label import ElideLabel logger = getLogger("Uninstalled") -class IconWidgetUninstalled(BaseUninstalledWidget): +class UninstalledIconWidget(BaseUninstalledWidget): def __init__(self, game: Game, core: LegendaryCore, pixmap): - super(IconWidgetUninstalled, self).__init__(game, core, pixmap) + super(UninstalledIconWidget, self).__init__(game, core, pixmap) layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) - self.setObjectName("game_widget_icon") + self.setObjectName(type(self).__name__) layout.addWidget(self.image) miniwidget = QWidget(self) diff --git a/rare/components/tabs/games/game_widgets/uninstalled_list_widget.py b/rare/components/tabs/games/game_widgets/uninstalled_list_widget.py index f0853b28..69fb100e 100644 --- a/rare/components/tabs/games/game_widgets/uninstalled_list_widget.py +++ b/rare/components/tabs/games/game_widgets/uninstalled_list_widget.py @@ -11,9 +11,9 @@ from rare.components.tabs.games.game_widgets.base_uninstalled_widget import ( logger = getLogger("Game") -class ListWidgetUninstalled(BaseUninstalledWidget): +class UninstalledListWidget(BaseUninstalledWidget): def __init__(self, core: LegendaryCore, game, pixmap): - super(ListWidgetUninstalled, self).__init__(game, core, pixmap) + super(UninstalledListWidget, self).__init__(game, core, pixmap) self.setFrameStyle(self.StyledPanel) layout = QHBoxLayout() self.setLayout(layout) diff --git a/rare/components/tabs/shop/search_results.py b/rare/components/tabs/shop/search_results.py index 3d6ba6e1..763aac3e 100644 --- a/rare/components/tabs/shop/search_results.py +++ b/rare/components/tabs/shop/search_results.py @@ -12,7 +12,8 @@ from PyQt5.QtWidgets import ( QStackedWidget, ) -from rare.utils.extra_widgets import ImageLabel, FlowLayout, WaitingSpinner +from rare.utils.extra_widgets import ImageLabel, WaitingSpinner +from rare.widgets.flow_layout import FlowLayout class SearchResults(QStackedWidget): diff --git a/rare/components/tabs/shop/shop_widget.py b/rare/components/tabs/shop/shop_widget.py index 5cdf2465..36785a48 100644 --- a/rare/components/tabs/shop/shop_widget.py +++ b/rare/components/tabs/shop/shop_widget.py @@ -13,7 +13,8 @@ from PyQt5.QtWidgets import ( from legendary.core import LegendaryCore from rare.ui.components.tabs.store.store import Ui_ShopWidget -from rare.utils.extra_widgets import WaitingSpinner, FlowLayout, ButtonLineEdit +from rare.utils.extra_widgets import WaitingSpinner, ButtonLineEdit +from rare.widgets.flow_layout import FlowLayout from .constants import Constants from .game_widgets import GameWidget from .shop_api_core import ShopApiCore diff --git a/rare/ui/components/tabs/games/games_tab.py b/rare/ui/components/tabs/games/games_tab.py index 0057a2a4..12864975 100644 --- a/rare/ui/components/tabs/games/games_tab.py +++ b/rare/ui/components/tabs/games/games_tab.py @@ -2,43 +2,44 @@ # Form implementation generated from reading ui file 'rare/ui/components/tabs/games/games_tab.ui' # -# Created by: PyQt5 UI code generator 5.15.4 +# Created by: PyQt5 UI code generator 5.15.6 # # WARNING: Any manual changes made to this file will be lost when pyuic5 is # run again. Do not edit this file unless you know what you are doing. -from PyQt5 import QtCore, QtWidgets +from PyQt5 import QtCore, QtGui, QtWidgets class Ui_GamesTab(object): def setupUi(self, GamesTab): GamesTab.setObjectName("GamesTab") - GamesTab.resize(1071, 678) GamesTab.setWindowTitle("StackedWidget") self.games = QtWidgets.QWidget() self.games.setObjectName("games") self.verticalLayout = QtWidgets.QVBoxLayout(self.games) self.verticalLayout.setObjectName("verticalLayout") - self.game_list_scroll_area = QtWidgets.QScrollArea(self.games) - self.game_list_scroll_area.setWidgetResizable(True) - self.game_list_scroll_area.setObjectName("game_list_scroll_area") - self.scroll_widget = QtWidgets.QWidget() - self.scroll_widget.setGeometry(QtCore.QRect(0, 0, 1051, 658)) - self.scroll_widget.setObjectName("scroll_widget") - self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.scroll_widget) - self.verticalLayout_4.setObjectName("verticalLayout_4") - self.count_games_label = QtWidgets.QLabel(self.scroll_widget) - self.count_games_label.setText("") + self.library_frame = QtWidgets.QFrame(self.games) + self.library_frame.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.library_frame.setFrameShadow(QtWidgets.QFrame.Plain) + self.library_frame.setObjectName("library_frame") + self.library_frame_layout = QtWidgets.QVBoxLayout(self.library_frame) + self.library_frame_layout.setContentsMargins(0, 0, 0, 0) + self.library_frame_layout.setObjectName("library_frame_layout") + self.games_count_layout = QtWidgets.QHBoxLayout() + self.games_count_layout.setSizeConstraint(QtWidgets.QLayout.SetFixedSize) + self.games_count_layout.setContentsMargins(6, 6, 6, 6) + self.games_count_layout.setObjectName("games_count_layout") + self.count_games_label = QtWidgets.QLabel(self.library_frame) + self.count_games_label.setText("error") self.count_games_label.setObjectName("count_games_label") - self.verticalLayout_4.addWidget(self.count_games_label) - spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - self.verticalLayout_4.addItem(spacerItem) - self.game_list_scroll_area.setWidget(self.scroll_widget) - self.verticalLayout.addWidget(self.game_list_scroll_area) + self.games_count_layout.addWidget(self.count_games_label, 0, QtCore.Qt.AlignTop) + self.library_frame_layout.addLayout(self.games_count_layout) + self.verticalLayout.addWidget(self.library_frame) GamesTab.addWidget(self.games) self.retranslateUi(GamesTab) + GamesTab.setCurrentIndex(0) QtCore.QMetaObject.connectSlotsByName(GamesTab) def retranslateUi(self, GamesTab): diff --git a/rare/ui/components/tabs/games/games_tab.ui b/rare/ui/components/tabs/games/games_tab.ui index 08e73954..0ca0c88d 100644 --- a/rare/ui/components/tabs/games/games_tab.ui +++ b/rare/ui/components/tabs/games/games_tab.ui @@ -2,56 +2,62 @@ GamesTab - - - 0 - 0 - 1071 - 678 - - StackedWidget + + 0 + - - - true + + + QFrame::StyledPanel - - - - 0 - 0 - 1051 - 658 - + + QFrame::Plain + + + + 0 - - - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - + + 0 + + + 0 + + + 0 + + + + + QLayout::SetFixedSize + + + 6 + + + 6 + + + 6 + + + 6 + + + + + error + + + + + + diff --git a/rare/utils/extra_widgets.py b/rare/utils/extra_widgets.py index 7107b419..e933faf4 100644 --- a/rare/utils/extra_widgets.py +++ b/rare/utils/extra_widgets.py @@ -13,7 +13,6 @@ from PyQt5.QtCore import ( ) from PyQt5.QtGui import QMovie, QPixmap, QFontMetrics, QImage from PyQt5.QtWidgets import ( - QLayout, QStyle, QSizePolicy, QLabel, @@ -42,110 +41,6 @@ from rare.utils.utils import icon as qta_icon logger = getLogger("ExtraWidgets") -class FlowLayout(QLayout): - def __init__(self, parent=None, margin=-1, hspacing=-1, vspacing=-1): - super(FlowLayout, self).__init__(parent) - self._hspacing = hspacing - self._vspacing = vspacing - self._items = [] - self.setContentsMargins(margin, margin, margin, margin) - self.setObjectName(type(self).__name__) - - def __del__(self): - del self._items[:] - - def addItem(self, item): - self._items.append(item) - - def horizontalSpacing(self): - if self._hspacing >= 0: - return self._hspacing - else: - return self.smartSpacing(QStyle.PM_LayoutHorizontalSpacing) - - def verticalSpacing(self): - if self._vspacing >= 0: - return self._vspacing - else: - return self.smartSpacing(QStyle.PM_LayoutVerticalSpacing) - - def count(self): - return len(self._items) - - def itemAt(self, index): - if 0 <= index < len(self._items): - return self._items[index] - - def takeAt(self, index): - if 0 <= index < len(self._items): - return self._items.pop(index) - - def expandingDirections(self): - return Qt.Orientations(0) - - def hasHeightForWidth(self): - return True - - def heightForWidth(self, width): - return self.doLayout(QRect(0, 0, width, 0), True) - - def setGeometry(self, rect): - super(FlowLayout, self).setGeometry(rect) - self.doLayout(rect, False) - - def sizeHint(self): - return self.minimumSize() - - def minimumSize(self): - size = QSize() - for item in self._items: - size = size.expandedTo(item.minimumSize()) - left, top, right, bottom = self.getContentsMargins() - size += QSize(left + right, top + bottom) - return size - - def doLayout(self, rect, testonly): - left, top, right, bottom = self.getContentsMargins() - effective = rect.adjusted(+left, +top, -right, -bottom) - x = effective.x() - y = effective.y() - lineheight = 0 - for item in self._items: - widget = item.widget() - if not widget.isVisible(): - continue - hspace = self.horizontalSpacing() - if hspace == -1: - hspace = widget.style().layoutSpacing( - QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Horizontal - ) - vspace = self.verticalSpacing() - if vspace == -1: - vspace = widget.style().layoutSpacing( - QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical - ) - nextX = x + item.sizeHint().width() + hspace - if nextX - hspace > effective.right() and lineheight > 0: - x = effective.x() - y = y + lineheight + vspace - nextX = x + item.sizeHint().width() + hspace - lineheight = 0 - if not testonly: - item.setGeometry(QRect(QPoint(x, y), item.sizeHint())) - x = nextX - lineheight = max(lineheight, item.sizeHint().height()) - return y + lineheight - rect.y() + bottom - - def smartSpacing(self, pm): - parent = self.parent() - if parent is None: - return -1 - elif parent.isWidgetType(): - return parent.style().pixelMetric(pm, None, parent) - else: - return parent.spacing() - - class IndicatorReasons: dir_not_empty = QCoreApplication.translate("IndicatorReasons", "Directory is not empty") wrong_format = QCoreApplication.translate("IndicatorReasons", "Given text has wrong format") diff --git a/rare/widgets/flow_layout.py b/rare/widgets/flow_layout.py new file mode 100644 index 00000000..b5191f45 --- /dev/null +++ b/rare/widgets/flow_layout.py @@ -0,0 +1,115 @@ +from PyQt5.QtCore import ( + Qt, + QRect, + QSize, + QPoint, +) +from PyQt5.QtWidgets import ( + QLayout, + QStyle, + QSizePolicy, +) + + +class FlowLayout(QLayout): + def __init__(self, parent=None, margin=-1, hspacing=-1, vspacing=-1): + super(FlowLayout, self).__init__(parent) + self._hspacing = hspacing + self._vspacing = vspacing + self._items = [] + self.setContentsMargins(margin, margin, margin, margin) + self.setObjectName(type(self).__name__) + + def __del__(self): + del self._items[:] + + def addItem(self, item): + self._items.append(item) + + def horizontalSpacing(self): + if self._hspacing >= 0: + return self._hspacing + else: + return self.smartSpacing(QStyle.PM_LayoutHorizontalSpacing) + + def verticalSpacing(self): + if self._vspacing >= 0: + return self._vspacing + else: + return self.smartSpacing(QStyle.PM_LayoutVerticalSpacing) + + def count(self): + return len(self._items) + + def itemAt(self, index): + if 0 <= index < len(self._items): + return self._items[index] + + def takeAt(self, index): + if 0 <= index < len(self._items): + return self._items.pop(index) + + def expandingDirections(self): + return Qt.Orientations(0) + + def hasHeightForWidth(self): + return True + + def heightForWidth(self, width): + return self.doLayout(QRect(0, 0, width, 0), True) + + def setGeometry(self, rect): + super(FlowLayout, self).setGeometry(rect) + self.doLayout(rect, False) + + def sizeHint(self): + return self.minimumSize() + + def minimumSize(self): + size = QSize() + for item in self._items: + size = size.expandedTo(item.minimumSize()) + left, top, right, bottom = self.getContentsMargins() + size += QSize(left + right, top + bottom) + return size + + def doLayout(self, rect, testonly): + left, top, right, bottom = self.getContentsMargins() + effective = rect.adjusted(+left, +top, -right, -bottom) + x = effective.x() + y = effective.y() + lineheight = 0 + for item in self._items: + widget = item.widget() + if not widget.isVisible(): + continue + hspace = self.horizontalSpacing() + if hspace == -1: + hspace = widget.style().layoutSpacing( + QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Horizontal + ) + vspace = self.verticalSpacing() + if vspace == -1: + vspace = widget.style().layoutSpacing( + QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical + ) + nextX = x + item.sizeHint().width() + hspace + if nextX - hspace > effective.right() and lineheight > 0: + x = effective.x() + y = y + lineheight + vspace + nextX = x + item.sizeHint().width() + hspace + lineheight = 0 + if not testonly: + item.setGeometry(QRect(QPoint(x, y), item.sizeHint())) + x = nextX + lineheight = max(lineheight, item.sizeHint().height()) + return y + lineheight - rect.y() + bottom + + def smartSpacing(self, pm): + parent = self.parent() + if parent is None: + return -1 + elif parent.isWidgetType(): + return parent.style().pixelMetric(pm, None, parent) + else: + return parent.spacing() diff --git a/rare/widgets/library_layout.py b/rare/widgets/library_layout.py index f34422e7..529b00f1 100644 --- a/rare/widgets/library_layout.py +++ b/rare/widgets/library_layout.py @@ -7,9 +7,10 @@ from PyQt5.QtCore import ( ) from PyQt5.QtWidgets import ( QSizePolicy, + QWidget, ) -from rare.utils.extra_widgets import FlowLayout +from .flow_layout import FlowLayout class LibraryLayout(FlowLayout): @@ -133,3 +134,14 @@ class LibraryLayout(FlowLayout): def sort(self, key: Callable): self._items.sort(key=key) self.setGeometry(self.parent().rect()) + + # These are used to pop and insert the installing widget, remove them when no longer needed + def remove(self, name: str) -> QWidget: + widget = next(filter(lambda x: x.widget().objectName() == name, self._items), None) + self._items.remove(widget) + self.setGeometry(self.parent().rect()) + return widget + + def insert(self, index: int, widget: QWidget): + self._items.insert(index, widget) + self.setGeometry(self.parent().rect()) diff --git a/rare/widgets/sliding_stack.py b/rare/widgets/sliding_stack.py new file mode 100644 index 00000000..00654f4e --- /dev/null +++ b/rare/widgets/sliding_stack.py @@ -0,0 +1,138 @@ +from PyQt5.QtCore import ( + pyqtSlot, + QEvent, + QEasingCurve, + QParallelAnimationGroup, + QAbstractAnimation, + QPropertyAnimation, + Qt, + QPoint, +) +from PyQt5.QtWidgets import QStackedWidget, QGestureEvent, QSwipeGesture + + +class SlidingStackedWidget(QStackedWidget): + """ + Taken from: https://stackoverflow.com/a/52597972 + """ + + def __init__(self, parent=None): + super(SlidingStackedWidget, self).__init__(parent) + + self.m_direction = Qt.Horizontal + self.m_speed = 500 + self.m_animationtype = QEasingCurve.OutBack + self.m_now = 0 + self.m_next = 0 + self.m_wrap = False + self.m_pnow = QPoint(0, 0) + self.m_active = False + + def setDirection(self, direction: Qt.Orientation) -> None: + self.m_direction = direction + + def setSpeed(self, speed: int) -> None: + self.m_speed = speed + + def setAnimation(self, animationtype: QEasingCurve.Type) -> None: + self.m_animationtype = animationtype + + def setWrap(self, wrap: bool) -> None: + self.m_wrap = wrap + + @pyqtSlot() + def slideInPrev(self): + now = self.currentIndex() + if self.m_wrap or now > 0: + self.slideInIndex(now - 1) + + @pyqtSlot() + def slideInNext(self): + now = self.currentIndex() + if self.m_wrap or now < (self.count() - 1): + self.slideInIndex(now + 1) + + def slideInIndex(self, idx): + if idx > (self.count() - 1): + idx = idx % self.count() + elif idx < 0: + idx = (idx + self.count()) % self.count() + self.slideInWidget(self.widget(idx)) + + def slideInWidget(self, newwidget): + if self.m_active: + return + + self.m_active = True + + _now = self.currentIndex() + _next = self.indexOf(newwidget) + + if _now == _next: + self.m_active = False + return + + offsetx, offsety = self.frameRect().width(), self.frameRect().height() + self.widget(_next).setGeometry(self.frameRect()) + + if not self.m_direction == Qt.Horizontal: + if _now < _next: + offsetx, offsety = 0, -offsety + else: + offsetx = 0 + else: + if _now < _next: + offsetx, offsety = -offsetx, 0 + else: + offsety = 0 + + pnext = self.widget(_next).pos() + pnow = self.widget(_now).pos() + self.m_pnow = pnow + + offset = QPoint(offsetx, offsety) + self.widget(_next).move(pnext - offset) + self.widget(_next).show() + self.widget(_next).raise_() + + animgroup = QParallelAnimationGroup(self, finished=self.animationDoneSlot) + + for index, start, end in zip((_now, _next), (pnow, pnext - offset), (pnow + offset, pnext)): + animation = QPropertyAnimation( + self.widget(index), + b"pos", + duration=self.m_speed, + easingCurve=self.m_animationtype, + startValue=start, + endValue=end, + ) + animgroup.addAnimation(animation) + + self.m_next = _next + self.m_now = _now + self.m_active = True + animgroup.start(QAbstractAnimation.DeleteWhenStopped) + + @pyqtSlot() + def animationDoneSlot(self): + self.setCurrentIndex(self.m_next) + self.widget(self.m_now).hide() + self.widget(self.m_now).move(self.m_pnow) + self.m_active = False + + def event(self, e: QEvent): + if e.type() == QEvent.Gesture: + return self.gestureEvent(QGestureEvent(e)) + return super(SlidingStackedWidget, self).event(e) + + def gestureEvent(self, e: QGestureEvent): + if swipe := e.gesture(Qt.SwipeGesture): + self.swipeTriggered(swipe) + return True + + def swipeTriggered(self, g: QSwipeGesture): + if g.state() == Qt.GestureFinished: + if g.horizontalDirection() == QSwipeGesture.Left: + self.slideInPrev() + else: + self.slideInNext()