From 08ab130c5ca9297a71c3d122dc8548eab32dc4aa Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Mon, 20 Jun 2022 12:44:57 +0300 Subject: [PATCH] LibraryLayout from #196 Introduces the LibraryLayout from #196. This layout distributes the available space in either horizontal side and in-between the widgets. Known issues: When searching for a game, it will re-align visible widgets, effectively centering the results. This is because the search and grouping functions are interleaved. #196 handles it differently by adjusting the opacity and re-ordering of the irrelevant widgets. Signed-off-by: loathingKernel <142770+loathingKernel@users.noreply.github.com> --- rare/components/tabs/games/__init__.py | 5 +- rare/components/tabs/games/game_utils.py | 1 + .../game_widgets/base_installed_widget.py | 5 +- .../game_widgets/base_uninstalled_widget.py | 5 +- .../game_widgets/installed_icon_widget.py | 3 + .../game_widgets/installed_list_widget.py | 1 + .../game_widgets/installing_game_widget.py | 8 +- .../game_widgets/uninstalled_icon_widget.py | 2 + .../game_widgets/uninstalled_list_widget.py | 11 +- rare/utils/extra_widgets.py | 1 - rare/widgets/library_layout.py | 135 ++++++++++++++++++ 11 files changed, 159 insertions(+), 18 deletions(-) create mode 100644 rare/widgets/library_layout.py diff --git a/rare/components/tabs/games/__init__.py b/rare/components/tabs/games/__init__.py index 67a62ca9..e26914e3 100644 --- a/rare/components/tabs/games/__init__.py +++ b/rare/components/tabs/games/__init__.py @@ -13,8 +13,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.utils.extra_widgets import FlowLayout -from .cloud_save_utils import CloudSaveUtils +from rare.widgets.library_layout import LibraryLayout from .cloud_save_utils import CloudSaveUtils from .game_info import GameInfoTabs from .game_info.uninstalled_info import UninstalledInfoTabs @@ -181,7 +180,7 @@ class GamesTab(QStackedWidget, Ui_GamesTab): def setup_game_list(self): self.icon_view = QWidget() - self.icon_view.setLayout(FlowLayout()) + self.icon_view.setLayout(LibraryLayout()) self.list_view = QWidget() self.list_view.setLayout(QVBoxLayout()) diff --git a/rare/components/tabs/games/game_utils.py b/rare/components/tabs/games/game_utils.py index d27dd313..34b80f56 100644 --- a/rare/components/tabs/games/game_utils.py +++ b/rare/components/tabs/games/game_utils.py @@ -33,6 +33,7 @@ class GameProcess(QObject): self.app_name = app_name self.on_startup = on_startup self.game = LegendaryCoreSingleton().get_game(app_name) + self.game_meta = RareGameMeta() self.socket = QLocalSocket() self.socket.connected.connect(self._socket_connected) self.socket.errorOccurred.connect(self._error_occurred) 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 811aec89..7bab425e 100644 --- a/rare/components/tabs/games/game_widgets/base_installed_widget.py +++ b/rare/components/tabs/games/game_widgets/base_installed_widget.py @@ -4,7 +4,7 @@ from logging import getLogger from PyQt5.QtCore import pyqtSignal, QProcess, QSettings, QStandardPaths, Qt, QByteArray from PyQt5.QtGui import QPixmap -from PyQt5.QtWidgets import QGroupBox, QMessageBox, QAction +from PyQt5.QtWidgets import QFrame, QMessageBox, QAction from rare.components.tabs.games.game_utils import GameUtils from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton, ArgumentsSingleton @@ -15,7 +15,7 @@ from rare.widgets.image_widget import ImageWidget logger = getLogger("Game") -class BaseInstalledWidget(QGroupBox): +class BaseInstalledWidget(QFrame): launch_signal = pyqtSignal(str, QProcess, list) show_info = pyqtSignal(str) finish_signal = pyqtSignal(str, int) @@ -83,7 +83,6 @@ class BaseInstalledWidget(QGroupBox): self.update_available = True self.data = QByteArray() - self.setContentsMargins(0, 0, 0, 0) self.settings = QSettings() self.setContextMenuPolicy(Qt.ActionsContextMenu) 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 8cb1eb8a..ac1682d9 100644 --- a/rare/components/tabs/games/game_widgets/base_uninstalled_widget.py +++ b/rare/components/tabs/games/game_widgets/base_uninstalled_widget.py @@ -1,7 +1,7 @@ from logging import getLogger from PyQt5.QtCore import pyqtSignal, Qt -from PyQt5.QtWidgets import QGroupBox, QAction +from PyQt5.QtWidgets import QFrame, QAction from legendary.models.game import Game from rare.shared.image_manager import ImageManagerSingleton, ImageSize @@ -10,7 +10,7 @@ from rare.widgets.image_widget import ImageWidget logger = getLogger("Uninstalled") -class BaseUninstalledWidget(QGroupBox): +class BaseUninstalledWidget(QFrame): show_uninstalled_info = pyqtSignal(Game) def __init__(self, game, core, pixmap): @@ -27,7 +27,6 @@ class BaseUninstalledWidget(QGroupBox): self.image.setPixmap(pixmap) self.installing = False self.setContextMenuPolicy(Qt.ActionsContextMenu) - self.setContentsMargins(0, 0, 0, 0) reload_image = QAction(self.tr("Reload Image"), self) reload_image.triggered.connect(self.reload_image) 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 5f39ebd1..d30825d6 100644 --- a/rare/components/tabs/games/game_widgets/installed_icon_widget.py +++ b/rare/components/tabs/games/game_widgets/installed_icon_widget.py @@ -24,6 +24,8 @@ class InstalledIconWidget(BaseInstalledWidget): self.setContextMenuPolicy(Qt.ActionsContextMenu) layout = QVBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) + self.setFixedWidth(ImageSize.Display.size.width()) self.core = LegendaryCoreSingleton() if self.update_available: @@ -42,6 +44,7 @@ class InstalledIconWidget(BaseInstalledWidget): miniwidget.setLayout(minilayout) self.title_label = ElideLabel(f"
+ Layout + +-----------------------------------------------+ + | margin (above first row only) | + |-----------------------------------------------| + | vspace (hspace) | + |-----------------------------------------------| + | hpadding | + |-----------------------------------------------| + | _ _ __ +--------+ _ __ +--------+ _ __ _ | + ||m||h|| h|| Widget ||h|| h|| Widget ||h|| h||m|| + ||a||s|| p|| ||s|| p|| ||s|| p||a|| + ||r||p|| a|| ||p|| a|| ||p|| a||r|| + ||g||a|| d|| ||a|| d|| ||a|| d||g|| + ||i||c|| d|| ||c|| d|| ||c|| d||i|| + ||n||e|| i|| ||e|| i|| ||e|| i||n|| + || || || n|| || || n|| || || n|| || + || || || g|| || || g|| || || g|| || + | - - -- +--------+ - -- +--------+ - -- - | + |-----------------------------------------------| + | vspace (hspace) | + |-----------------------------------------------| + | hpadding | + |-----------------------------------------------| + | margin (below last row only) | + +-----------------------------------------------+ + + margin: doesn't play a role in the code below, it only affects the + effective rectangle inside which we can layout + hspace: static padding between widgets (minimum distance between them) + hpadding: dynamic padding to fill the space when resizing ++ + @param self: object self-reference + @param rect: the area the widgets should occupy + @param testonly: only test the layout, don't arrange the items + + @return: the height of the layout + """ + + left, top, right, bottom = self.getContentsMargins() + effective = rect.adjusted(+left, +top, -right, -bottom) + x = effective.x() + y = effective.y() + lineheight = 0 + + if not self._items: + return y + lineheight - rect.y() + bottom + + widget = self._items[0].widget() + 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) + + # lk: get the remaining space after subtracting the space required for each widget and its static padding + # lk: also reserve space for the leading static padding + rem_hspace = (effective.width() - hspace) % (widget.size().width() + hspace) + # lk: the number of items and their static spacing that can be in the layout + hspace_items = (effective.width() - hspace) // (widget.size().width() + hspace) + + # lk: in case the visible items are less than the maximum possible widgets + visible_items = len([item for item in self._items if not item.isEmpty()]) + if visible_items < hspace_items: + hspace_items = visible_items + rem_hspace = (effective.width() - hspace) - ((widget.size().width() + hspace) * hspace_items) + + try: + # lk: the dynamic padding between each item, also account for the leading dynamic padding + hpadding = rem_hspace // (hspace_items + 1) + except ZeroDivisionError: + hpadding = 0 + + for item in self._items: + if item.isEmpty(): + continue + # lk: compute the location of the next widget + next_x = x + item.sizeHint().width() + hspace + hpadding + # lk: find out if there is enough space for the widget in this row + # lk: account for the leading static and dynamic padding too (at the start) + if next_x - hspace * 2 - hpadding * 2 > effective.right() and lineheight > 0: + x = effective.x() + # lk: find next vertical position, add static and dynamic padding + y = y + lineheight + vspace + hpadding + next_x = x + item.sizeHint().width() + hspace + hpadding + lineheight = 0 + # lk: add static and dynamic padding to the current widget + x = x + hspace + hpadding + if not testonly: + item.setGeometry(QRect(QPoint(x, y), item.sizeHint())) + x = next_x + lineheight = max(lineheight, item.sizeHint().height()) + return y + lineheight - rect.y() + bottom + + def sort(self, key: Callable): + self._items.sort(key=key) + self.setGeometry(self.parent().rect())