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"

{self.game.app_title}

", parent=miniwidget) + self.title_label.setAlignment(Qt.AlignTop) self.title_label.setObjectName("game_widget") minilayout.addWidget(self.title_label, stretch=2) diff --git a/rare/components/tabs/games/game_widgets/installed_list_widget.py b/rare/components/tabs/games/game_widgets/installed_list_widget.py index 25f92462..0950a61d 100644 --- a/rare/components/tabs/games/game_widgets/installed_list_widget.py +++ b/rare/components/tabs/games/game_widgets/installed_list_widget.py @@ -19,6 +19,7 @@ class InstalledListWidget(BaseInstalledWidget): def __init__(self, app_name, pixmap, game_utils): super(InstalledListWidget, self).__init__(app_name, pixmap, game_utils) + self.setFrameStyle(self.StyledPanel) self.dev = self.game.metadata["developer"] if self.igame: self.size = self.igame.install_size 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 93753ec8..6868d113 100644 --- a/rare/components/tabs/games/game_widgets/installing_game_widget.py +++ b/rare/components/tabs/games/game_widgets/installing_game_widget.py @@ -1,6 +1,6 @@ from PyQt5.QtCore import Qt from PyQt5.QtGui import QPixmap -from PyQt5.QtWidgets import QVBoxLayout, QHBoxLayout, QGroupBox, QWidget +from PyQt5.QtWidgets import QVBoxLayout, QHBoxLayout, QWidget, QFrame from legendary.models.game import Game from rare.shared import LegendaryCoreSingleton @@ -9,14 +9,15 @@ from rare.widgets.elide_label import ElideLabel from .library_widget import LibraryWidget -class InstallingGameWidget(QGroupBox): +class InstallingGameWidget(QFrame): game: Game = None def __init__(self): super(InstallingGameWidget, self).__init__() layout = QVBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) + self.setFixedWidth(ImageSize.Display.size.width()) self.setObjectName("game_widget_icon") - self.setContentsMargins(0, 0, 0, 0) self.core = LegendaryCoreSingleton() self.image_manager = ImageManagerSingleton() @@ -34,6 +35,7 @@ class InstallingGameWidget(QGroupBox): miniwidget.setLayout(minilayout) self.title_label = ElideLabel(f"

Error

", parent=miniwidget) + self.title_label.setAlignment(Qt.AlignTop) self.title_label.setObjectName("game_widget") minilayout.addWidget(self.title_label, stretch=2) 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 0bd959ec..5ea06958 100644 --- a/rare/components/tabs/games/game_widgets/uninstalled_icon_widget.py +++ b/rare/components/tabs/games/game_widgets/uninstalled_icon_widget.py @@ -18,6 +18,7 @@ class IconWidgetUninstalled(BaseUninstalledWidget): def __init__(self, game: Game, core: LegendaryCore, pixmap): super(IconWidgetUninstalled, self).__init__(game, core, pixmap) layout = QVBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) self.setObjectName("game_widget_icon") layout.addWidget(self.image) @@ -29,6 +30,7 @@ class IconWidgetUninstalled(BaseUninstalledWidget): miniwidget.setLayout(minilayout) self.title_label = ElideLabel(f"

{game.app_title}

", parent=miniwidget) + self.title_label.setAlignment(Qt.AlignTop) self.title_label.setObjectName("game_widget") minilayout.addWidget(self.title_label, stretch=2) 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 870b6184..f0853b28 100644 --- a/rare/components/tabs/games/game_widgets/uninstalled_list_widget.py +++ b/rare/components/tabs/games/game_widgets/uninstalled_list_widget.py @@ -14,12 +14,13 @@ logger = getLogger("Game") class ListWidgetUninstalled(BaseUninstalledWidget): def __init__(self, core: LegendaryCore, game, pixmap): super(ListWidgetUninstalled, self).__init__(game, core, pixmap) - self.layout = QHBoxLayout() - self.setLayout(self.layout) - self.layout.addWidget(self.image) + self.setFrameStyle(self.StyledPanel) + layout = QHBoxLayout() + self.setLayout(layout) + layout.addWidget(self.image) self.child_layout = QVBoxLayout() - self.layout.addLayout(self.child_layout) + layout.addLayout(self.child_layout) self.title_label = QLabel(f"

{self.game.app_title}

") self.app_name_label = QLabel(f"App Name: {self.game.app_name}") @@ -31,5 +32,5 @@ class ListWidgetUninstalled(BaseUninstalledWidget): self.child_layout.addWidget(self.app_name_label) self.child_layout.addWidget(self.install_button) - self.layout.setAlignment(Qt.AlignLeft) + layout.setAlignment(Qt.AlignLeft) self.child_layout.setAlignment(Qt.AlignTop) diff --git a/rare/utils/extra_widgets.py b/rare/utils/extra_widgets.py index ba880244..7107b419 100644 --- a/rare/utils/extra_widgets.py +++ b/rare/utils/extra_widgets.py @@ -4,7 +4,6 @@ from typing import Callable, Tuple from PyQt5.QtCore import ( Qt, - QEvent, QCoreApplication, QRect, QSize, diff --git a/rare/widgets/library_layout.py b/rare/widgets/library_layout.py new file mode 100644 index 00000000..f34422e7 --- /dev/null +++ b/rare/widgets/library_layout.py @@ -0,0 +1,135 @@ +from typing import Callable + +from PyQt5.QtCore import ( + Qt, + QRect, + QPoint, +) +from PyQt5.QtWidgets import ( + QSizePolicy, +) + +from rare.utils.extra_widgets import FlowLayout + + +class LibraryLayout(FlowLayout): + def __init__(self, parent=None, margin=6, spacing=11): + super(LibraryLayout, self).__init__(parent=parent, margin=margin, hspacing=spacing, vspacing=spacing) + + # def event(self, e: QEvent) -> None: + # if e.type() == QEvent.ShowToParent or e.type() == QEvent.HideToParent: + # self.doLayout(self.parent().rect(), False) + # e.accept() + + def expandingDirections(self): + return Qt.Orientations(Qt.Horizontal | Qt.Vertical) + + def setGeometry(self, rect): + super(FlowLayout, self).setGeometry(rect) + self.doLayout(rect, False) + + def doLayout(self, rect, testonly): + """! + @brief Arranges the widgets for this layout + +
+        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())