From 085f0dd26c6a100de1b22eecff663a80cc9a58ab Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Thu, 23 Jan 2025 21:13:08 -0800 Subject: [PATCH 1/5] reduce unnecessary CardItem creation in ViewZone addCard process (#5513) --- cockatrice/src/game/zones/card_zone.cpp | 2 +- cockatrice/src/game/zones/view_zone.cpp | 51 ++++++++++++++++++++----- cockatrice/src/game/zones/view_zone.h | 1 + 3 files changed, 44 insertions(+), 10 deletions(-) diff --git a/cockatrice/src/game/zones/card_zone.cpp b/cockatrice/src/game/zones/card_zone.cpp index f444cb464..1db741754 100644 --- a/cockatrice/src/game/zones/card_zone.cpp +++ b/cockatrice/src/game/zones/card_zone.cpp @@ -141,7 +141,7 @@ void CardZone::addCard(CardItem *card, const bool reorganize, const int x, const } for (auto *view : views) { - if (view->getIsReversed() || (x <= view->getCards().size()) || (view->getNumberCards() == -1)) { + if (view->prepareAddCard(x)) { view->addCard(new CardItem(player, nullptr, card->getName(), card->getProviderId(), card->getId()), reorganize, x, y); } diff --git a/cockatrice/src/game/zones/view_zone.cpp b/cockatrice/src/game/zones/view_zone.cpp index bd7ddd966..3cd2be8a3 100644 --- a/cockatrice/src/game/zones/view_zone.cpp +++ b/cockatrice/src/game/zones/view_zone.cpp @@ -282,6 +282,46 @@ void ZoneViewZone::setPileView(int _pileView) reorganizeCards(); } +/** + * Checks if inserting a card at the given position requires an actual new card to be created and added to the view. + * Also does any cardId updates that would be required if a card is inserted in that position. + * + * Note that this method can end up modifying the cardIds despite returning false. + * (for example, if the card is inserted into a hidden portion of the deck while the view is reversed) + * + * Make sure to call this method once before calling addCard(), so that you skip creating a new CardItem and calling + * addCard() if it's not required. + * + * @param x The position to insert the card at. + * @return Whether to proceed with calling addCard. + */ +bool ZoneViewZone::prepareAddCard(int x) +{ + bool doInsert = false; + if (!isReversed) { + if (x <= cards.size() || cards.size() == -1) { + doInsert = true; + } + } else { + // map x (which is in origZone indexes) to this viewZone's cardList index + int firstId = cards.isEmpty() ? origZone->getCards().size() : cards.front()->getId(); + int insertionIndex = x - firstId; + if (insertionIndex >= 0) { + // card was put into a portion of the deck that's in the view + doInsert = true; + } else { + // card was put into a portion of the deck that's not in the view; update ids but don't insert card + updateCardIds(ADD_CARD); + } + } + + return doInsert; +} + +/** + * Make sure prepareAddCard() was called before calling addCard(). + * This method assumes we already checked that the card is being inserted into the visible portion + */ void ZoneViewZone::addCardImpl(CardItem *card, int x, int /*y*/) { if (!isReversed) { @@ -295,15 +335,8 @@ void ZoneViewZone::addCardImpl(CardItem *card, int x, int /*y*/) // map x (which is in origZone indexes) to this viewZone's cardList index int firstId = cards.isEmpty() ? origZone->getCards().size() : cards.front()->getId(); int insertionIndex = x - firstId; - if (insertionIndex >= 0) { - // card was put into a portion of the deck that's in the view - // qMin to prevent out-of-bounds error when bottoming a card that is already in the view - cards.insert(qMin(insertionIndex, cards.size()), card); - } else { - // card was put into a portion of the deck that's not in the view - updateCardIds(ADD_CARD); - return; - } + // qMin to prevent out-of-bounds error when bottoming a card that is already in the view + cards.insert(qMin(insertionIndex, cards.size()), card); } card->setParentItem(this); diff --git a/cockatrice/src/game/zones/view_zone.h b/cockatrice/src/game/zones/view_zone.h index 0f6de060b..41ed99609 100644 --- a/cockatrice/src/game/zones/view_zone.h +++ b/cockatrice/src/game/zones/view_zone.h @@ -70,6 +70,7 @@ public: void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; void reorganizeCards() override; void initializeCards(const QList &cardList = QList()); + bool prepareAddCard(int x); void removeCard(int position); int getNumberCards() const { From e8b1e3ef0c085f958fa6840b2901e50327373ad2 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Fri, 24 Jan 2025 18:08:28 -0800 Subject: [PATCH 2/5] don't autoclose card view if single card gets dragged into same zone (#5517) * rename canResize param to toNewZone * pass toNewZone down * don't autoclose card view if card gets dragged into same zone --- cockatrice/src/game/zones/card_zone.cpp | 4 ++-- cockatrice/src/game/zones/table_zone.cpp | 4 ++-- cockatrice/src/game/zones/table_zone.h | 4 ++-- cockatrice/src/game/zones/view_zone.cpp | 13 +++++++++++-- cockatrice/src/game/zones/view_zone.h | 2 +- 5 files changed, 18 insertions(+), 9 deletions(-) diff --git a/cockatrice/src/game/zones/card_zone.cpp b/cockatrice/src/game/zones/card_zone.cpp index 1db741754..f3ebbd214 100644 --- a/cockatrice/src/game/zones/card_zone.cpp +++ b/cockatrice/src/game/zones/card_zone.cpp @@ -173,7 +173,7 @@ CardItem *CardZone::getCard(int cardId, const QString &cardName) return c; } -CardItem *CardZone::takeCard(int position, int cardId, bool /*canResize*/) +CardItem *CardZone::takeCard(int position, int cardId, bool toNewZone) { if (position == -1) { // position == -1 means either that the zone is indexed by card id @@ -190,7 +190,7 @@ CardItem *CardZone::takeCard(int position, int cardId, bool /*canResize*/) return nullptr; for (auto *view : views) { - view->removeCard(position); + view->removeCard(position, toNewZone); } CardItem *c = cards.takeAt(position); diff --git a/cockatrice/src/game/zones/table_zone.cpp b/cockatrice/src/game/zones/table_zone.cpp index ee1d0219d..686cb3588 100644 --- a/cockatrice/src/game/zones/table_zone.cpp +++ b/cockatrice/src/game/zones/table_zone.cpp @@ -244,10 +244,10 @@ void TableZone::toggleTapped() player->sendGameCommand(player->prepareGameCommand(cmdList)); } -CardItem *TableZone::takeCard(int position, int cardId, bool canResize) +CardItem *TableZone::takeCard(int position, int cardId, bool toNewZone) { CardItem *result = CardZone::takeCard(position, cardId); - if (canResize) + if (toNewZone) resizeToContents(); return result; } diff --git a/cockatrice/src/game/zones/table_zone.h b/cockatrice/src/game/zones/table_zone.h index 4653b564a..271276967 100644 --- a/cockatrice/src/game/zones/table_zone.h +++ b/cockatrice/src/game/zones/table_zone.h @@ -147,10 +147,10 @@ public: @param position card position @param cardId id of card to take - @param canResize defaults to true + @param toNewZone Whether the destination of the card is not the same as the starting zone. Defaults to true @return CardItem that has been removed */ - CardItem *takeCard(int position, int cardId, bool canResize = true) override; + CardItem *takeCard(int position, int cardId, bool toNewZone = true) override; /** Resizes the TableZone in case CardItems are within or diff --git a/cockatrice/src/game/zones/view_zone.cpp b/cockatrice/src/game/zones/view_zone.cpp index 3cd2be8a3..d86e71f90 100644 --- a/cockatrice/src/game/zones/view_zone.cpp +++ b/cockatrice/src/game/zones/view_zone.cpp @@ -315,6 +315,11 @@ bool ZoneViewZone::prepareAddCard(int x) } } + // autoclose check is done both here and in removeCard + if (cards.isEmpty() && !doInsert && SettingsCache::instance().getCloseEmptyCardView()) { + close(); + } + return doInsert; } @@ -365,7 +370,7 @@ void ZoneViewZone::handleDropEvent(const QList &dragItems, player->sendGameCommand(cmd); } -void ZoneViewZone::removeCard(int position) +void ZoneViewZone::removeCard(int position, bool toNewZone) { if (isReversed) { position -= cards.first()->getId(); @@ -382,7 +387,11 @@ void ZoneViewZone::removeCard(int position) CardItem *card = cards.takeAt(position); card->deleteLater(); - if (cards.isEmpty() && SettingsCache::instance().getCloseEmptyCardView()) { + // The toNewZone check is to prevent the view from auto-closing if the view contains only a single card and that + // card gets dragged within the view. + // Another autoclose check is done in prepareAddCard so that the view autocloses if the last card was moved to an + // unrevealed portion of the same zone. + if (cards.isEmpty() && SettingsCache::instance().getCloseEmptyCardView() && toNewZone) { close(); return; } diff --git a/cockatrice/src/game/zones/view_zone.h b/cockatrice/src/game/zones/view_zone.h index 41ed99609..3c69317aa 100644 --- a/cockatrice/src/game/zones/view_zone.h +++ b/cockatrice/src/game/zones/view_zone.h @@ -71,7 +71,7 @@ public: void reorganizeCards() override; void initializeCards(const QList &cardList = QList()); bool prepareAddCard(int x); - void removeCard(int position); + void removeCard(int position, bool toNewZone); int getNumberCards() const { return numberCards; From f428148f64f7ddeabb307b622466b409cc336c04 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Fri, 24 Jan 2025 19:16:40 -0800 Subject: [PATCH 3/5] Allow offline Deck Storage tab (#5518) * make deck storage tab no longer close on disconnect * add method for clearing remote decklist model * handle connect/disconnect in deck storage tab --- .../src/client/tabs/tab_deck_storage.cpp | 39 ++++++++++++++++++- cockatrice/src/client/tabs/tab_deck_storage.h | 9 ++++- cockatrice/src/client/tabs/tab_supervisor.cpp | 10 ++--- .../remote/remote_decklist_tree_widget.cpp | 18 ++++++--- .../remote/remote_decklist_tree_widget.h | 2 + 5 files changed, 63 insertions(+), 15 deletions(-) diff --git a/cockatrice/src/client/tabs/tab_deck_storage.cpp b/cockatrice/src/client/tabs/tab_deck_storage.cpp index 4435e5a22..9d35819fc 100644 --- a/cockatrice/src/client/tabs/tab_deck_storage.cpp +++ b/cockatrice/src/client/tabs/tab_deck_storage.cpp @@ -4,7 +4,6 @@ #include "../../server/pending_command.h" #include "../../server/remote/remote_decklist_tree_widget.h" #include "../../settings/cache_settings.h" -#include "../game_logic/abstract_client.h" #include "../get_text_with_max.h" #include "decklist.h" #include "pb/command_deck_del.pb.h" @@ -31,7 +30,9 @@ #include #include -TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, AbstractClient *_client) +TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, + AbstractClient *_client, + const ServerInfo_User *currentUserInfo) : Tab(_tabSupervisor), client(_client) { localDirModel = new QFileSystemModel(this); @@ -151,6 +152,10 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, AbstractClient *_c QWidget *mainWidget = new QWidget(this); mainWidget->setLayout(hbox); setCentralWidget(mainWidget); + + connect(client, &AbstractClient::userInfoChanged, this, &TabDeckStorage::handleConnected); + connect(client, &AbstractClient::statusChanged, this, &TabDeckStorage::handleConnectionChanged); + setRemoteEnabled(currentUserInfo && currentUserInfo->user_level() & ServerInfo_User::IsRegistered); } void TabDeckStorage::retranslateUi() @@ -187,6 +192,36 @@ QString TabDeckStorage::getTargetPath() const } } +void TabDeckStorage::handleConnected(const ServerInfo_User &userInfo) +{ + setRemoteEnabled(userInfo.user_level() & ServerInfo_User::IsRegistered); +} + +/** + * This is only responsible for handling the disconnect. The connect is already handled elsewhere + */ +void TabDeckStorage::handleConnectionChanged(ClientStatus status) +{ + if (status == StatusDisconnected) { + setRemoteEnabled(false); + } +} + +void TabDeckStorage::setRemoteEnabled(bool enabled) +{ + aUpload->setEnabled(enabled); + aOpenRemoteDeck->setEnabled(enabled); + aDownload->setEnabled(enabled); + aNewFolder->setEnabled(enabled); + aDeleteRemoteDeck->setEnabled(enabled); + + if (enabled) { + serverDirView->refreshTree(); + } else { + serverDirView->clearTree(); + } +} + void TabDeckStorage::actLocalDoubleClick(const QModelIndex &curLeft) { if (!localDirModel->isDir(curLeft)) { diff --git a/cockatrice/src/client/tabs/tab_deck_storage.h b/cockatrice/src/client/tabs/tab_deck_storage.h index 774b9f609..748852ec2 100644 --- a/cockatrice/src/client/tabs/tab_deck_storage.h +++ b/cockatrice/src/client/tabs/tab_deck_storage.h @@ -2,8 +2,10 @@ #define TAB_DECK_STORAGE_H #include "../../server/remote/remote_decklist_tree_widget.h" +#include "../game_logic/abstract_client.h" #include "tab.h" +class ServerInfo_User; class AbstractClient; class QTreeView; class QFileSystemModel; @@ -31,12 +33,17 @@ private: QAction *aOpenRemoteDeck, *aDownload, *aNewFolder, *aDeleteRemoteDeck; QString getTargetPath() const; + void setRemoteEnabled(bool enabled); + void uploadDeck(const QString &filePath, const QString &targetPath); void deleteRemoteDeck(const RemoteDeckList_TreeModel::Node *node); void downloadNodeAtIndex(const QModelIndex &curLeft, const QModelIndex &curRight); private slots: + void handleConnected(const ServerInfo_User &userInfo); + void handleConnectionChanged(ClientStatus status); + void actLocalDoubleClick(const QModelIndex &curLeft); void actOpenLocalDeck(); @@ -63,7 +70,7 @@ private slots: void deleteDeckFinished(const Response &response, const CommandContainer &commandContainer); public: - TabDeckStorage(TabSupervisor *_tabSupervisor, AbstractClient *_client); + TabDeckStorage(TabSupervisor *_tabSupervisor, AbstractClient *_client, const ServerInfo_User *currentUserInfo); void retranslateUi() override; QString getTabText() const override { diff --git a/cockatrice/src/client/tabs/tab_supervisor.cpp b/cockatrice/src/client/tabs/tab_supervisor.cpp index c71d37a46..a704fb8b1 100644 --- a/cockatrice/src/client/tabs/tab_supervisor.cpp +++ b/cockatrice/src/client/tabs/tab_supervisor.cpp @@ -279,6 +279,7 @@ void TabSupervisor::initStartupTabs() addDeckEditorTab(nullptr); checkAndTrigger(aTabVisualDeckStorage, SettingsCache::instance().getTabVisualDeckStorageOpen()); + checkAndTrigger(aTabDeckStorage, SettingsCache::instance().getTabDeckStorageOpen()); } /** @@ -334,6 +335,7 @@ void TabSupervisor::resetTabsMenu() tabsMenu->addAction(aTabDeckEditor); tabsMenu->addSeparator(); tabsMenu->addAction(aTabVisualDeckStorage); + tabsMenu->addAction(aTabDeckStorage); } void TabSupervisor::start(const ServerInfo_User &_userInfo) @@ -355,10 +357,8 @@ void TabSupervisor::start(const ServerInfo_User &_userInfo) updatePingTime(0, -1); if (userInfo->user_level() & ServerInfo_User::IsRegistered) { - tabsMenu->addAction(aTabDeckStorage); tabsMenu->addAction(aTabReplays); - checkAndTrigger(aTabDeckStorage, SettingsCache::instance().getTabDeckStorageOpen()); checkAndTrigger(aTabReplays, SettingsCache::instance().getTabReplaysOpen()); } @@ -379,7 +379,6 @@ void TabSupervisor::startLocal(const QList &_clients) resetTabsMenu(); tabAccount = nullptr; - tabDeckStorage = nullptr; tabReplays = nullptr; tabAdmin = nullptr; tabLog = nullptr; @@ -416,9 +415,6 @@ void TabSupervisor::stop() if (tabServer) { tabServer->closeRequest(true); } - if (tabDeckStorage) { - tabDeckStorage->closeRequest(true); - } if (tabReplays) { tabReplays->closeRequest(true); } @@ -508,7 +504,7 @@ void TabSupervisor::actTabDeckStorage(bool checked) { SettingsCache::instance().setTabDeckStorageOpen(checked); if (checked && !tabDeckStorage) { - tabDeckStorage = new TabDeckStorage(this, client); + tabDeckStorage = new TabDeckStorage(this, client, userInfo); connect(tabDeckStorage, &TabDeckStorage::openDeckEditor, this, &TabSupervisor::addDeckEditorTab); myAddTab(tabDeckStorage, aTabDeckStorage); connect(tabDeckStorage, &Tab::closed, this, [this] { diff --git a/cockatrice/src/server/remote/remote_decklist_tree_widget.cpp b/cockatrice/src/server/remote/remote_decklist_tree_widget.cpp index 314d7687b..b694a2fdb 100644 --- a/cockatrice/src/server/remote/remote_decklist_tree_widget.cpp +++ b/cockatrice/src/server/remote/remote_decklist_tree_widget.cpp @@ -270,15 +270,18 @@ void RemoteDeckList_TreeModel::refreshTree() client->sendCommand(pend); } +void RemoteDeckList_TreeModel::clearTree() +{ + beginResetModel(); + root->clearTree(); + endResetModel(); +} + void RemoteDeckList_TreeModel::deckListFinished(const Response &r) { const Response_DeckList &resp = r.GetExtension(Response_DeckList::ext); - beginResetModel(); - - root->clearTree(); - - endResetModel(); + clearTree(); ServerInfo_DeckStorage_TreeItem tempRoot; tempRoot.set_id(0); @@ -361,3 +364,8 @@ void RemoteDeckList_TreeWidget::refreshTree() { treeModel->refreshTree(); } + +void RemoteDeckList_TreeWidget::clearTree() +{ + treeModel->clearTree(); +} diff --git a/cockatrice/src/server/remote/remote_decklist_tree_widget.h b/cockatrice/src/server/remote/remote_decklist_tree_widget.h index 59a3e4950..5fdb100de 100644 --- a/cockatrice/src/server/remote/remote_decklist_tree_widget.h +++ b/cockatrice/src/server/remote/remote_decklist_tree_widget.h @@ -106,6 +106,7 @@ public: DirectoryNode *addNamedFolderToTree(const QString &name, DirectoryNode *parent); void removeNode(Node *node); void refreshTree(); + void clearTree(); }; class RemoteDeckList_TreeWidget : public QTreeView @@ -127,6 +128,7 @@ public: void addFolderToTree(const QString &name, RemoteDeckList_TreeModel::DirectoryNode *parent); void removeNode(RemoteDeckList_TreeModel::Node *node); void refreshTree(); + void clearTree(); }; #endif From 4e9615709195df75483f163eb877f75fe66485a2 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Sat, 25 Jan 2025 04:17:39 +0100 Subject: [PATCH 4/5] Flow Layout fixes (#5515) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Flow Layout fixes. * Remove some comments. --------- Co-authored-by: Lukas BrĂ¼bach --- cockatrice/CMakeLists.txt | 2 - .../src/client/ui/layouts/flow_layout.cpp | 486 +++++++++++++++--- .../src/client/ui/layouts/flow_layout.h | 12 +- .../ui/layouts/horizontal_flow_layout.cpp | 142 ----- .../ui/layouts/horizontal_flow_layout.h | 19 - .../ui/layouts/vertical_flow_layout.cpp | 144 ------ .../client/ui/layouts/vertical_flow_layout.h | 19 - .../general/layout_containers/flow_widget.cpp | 85 ++- .../general/layout_containers/flow_widget.h | 10 +- .../printing_selector/printing_selector.cpp | 2 +- .../deck_preview_deck_tags_display_widget.cpp | 2 +- .../visual_deck_storage_tag_filter_widget.cpp | 2 +- .../visual_deck_storage_widget.cpp | 2 +- 13 files changed, 465 insertions(+), 462 deletions(-) delete mode 100644 cockatrice/src/client/ui/layouts/horizontal_flow_layout.cpp delete mode 100644 cockatrice/src/client/ui/layouts/horizontal_flow_layout.h delete mode 100644 cockatrice/src/client/ui/layouts/vertical_flow_layout.cpp delete mode 100644 cockatrice/src/client/ui/layouts/vertical_flow_layout.h diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 27709eb72..0fc6e78b3 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -63,8 +63,6 @@ set(cockatrice_SOURCES src/game/filters/filter_tree.cpp src/game/filters/filter_tree_model.cpp src/client/ui/layouts/flow_layout.cpp - src/client/ui/layouts/horizontal_flow_layout.cpp - src/client/ui/layouts/vertical_flow_layout.cpp src/client/ui/widgets/general/layout_containers/flow_widget.cpp src/game/game_scene.cpp src/game/game_selector.cpp diff --git a/cockatrice/src/client/ui/layouts/flow_layout.cpp b/cockatrice/src/client/ui/layouts/flow_layout.cpp index f1a776c47..6e00fdb68 100644 --- a/cockatrice/src/client/ui/layouts/flow_layout.cpp +++ b/cockatrice/src/client/ui/layouts/flow_layout.cpp @@ -20,8 +20,12 @@ * @param hSpacing The horizontal spacing between items. * @param vSpacing The vertical spacing between items. */ -FlowLayout::FlowLayout(QWidget *parent, const int margin, const int hSpacing, const int vSpacing) - : QLayout(parent), horizontalMargin(hSpacing), verticalMargin(vSpacing) +FlowLayout::FlowLayout(QWidget *parent, + const Qt::Orientation _flowDirection, + const int margin, + const int hSpacing, + const int vSpacing) + : QLayout(parent), flowDirection(_flowDirection), horizontalMargin(hSpacing), verticalMargin(vSpacing) { setContentsMargins(margin, margin, margin, margin); } @@ -62,27 +66,51 @@ bool FlowLayout::hasHeightForWidth() const */ int FlowLayout::heightForWidth(const int width) const { - int height = 0; - int rowWidth = 0; - int rowHeight = 0; + if (flowDirection == Qt::Vertical) { + int height = 0; + int rowWidth = 0; + int rowHeight = 0; - for (const QLayoutItem *item : items) { - if (item == nullptr || item->isEmpty()) { - continue; - } + for (const QLayoutItem *item : items) { + if (item == nullptr || item->isEmpty()) { + continue; + } - int itemWidth = item->sizeHint().width() + horizontalSpacing(); - if (rowWidth + itemWidth > width) { // Start a new row if the row width exceeds available width - height += rowHeight + verticalSpacing(); - rowWidth = itemWidth; - rowHeight = item->sizeHint().height() + verticalSpacing(); - } else { - rowWidth += itemWidth; - rowHeight = qMax(rowHeight, item->sizeHint().height()); + int itemWidth = item->sizeHint().width() + horizontalSpacing(); + if (rowWidth + itemWidth > width) { + height += rowHeight + verticalSpacing(); + rowWidth = itemWidth; + rowHeight = item->sizeHint().height(); + } else { + rowWidth += itemWidth; + rowHeight = qMax(rowHeight, item->sizeHint().height()); + } } + height += rowHeight; // Add height of the last row + return height; + } else { + int width = 0; + int colWidth = 0; + int colHeight = 0; + + for (const QLayoutItem *item : items) { + if (item == nullptr || item->isEmpty()) { + continue; + } + + int itemHeight = item->sizeHint().height(); + if (colHeight + itemHeight > width) { + width += colWidth; + colHeight = itemHeight; + colWidth = item->sizeHint().width(); + } else { + colHeight += itemHeight; + colWidth = qMax(colWidth, item->sizeHint().width()); + } + } + width += colWidth; // Add width of the last column + return width; } - height += rowHeight; // Add the final row's height - return height; } /** @@ -93,132 +121,420 @@ void FlowLayout::setGeometry(const QRect &rect) { QLayout::setGeometry(rect); // Sets the geometry of the layout based on the given rectangle. - int left, top, right, bottom; - getContentsMargins(&left, &top, &right, &bottom); // Retrieves the layout's content margins. + if (flowDirection == Qt::Horizontal) { + // If we have a parent scroll area, we're clamped to that, else we use our own rectangle. + const int availableWidth = getParentScrollAreaWidth() == 0 ? rect.width() : getParentScrollAreaWidth(); - // Adjust the rectangle to exclude margins. - const QRect adjustedRect = rect.adjusted(+left, +top, -right, -bottom); + const int totalHeight = layoutAllRows(rect.x(), rect.y(), availableWidth); - // Calculate the available width for items, considering either the adjusted rectangle's width - // or the parent scroll area width, if applicable. - const int availableWidth = qMax(adjustedRect.width(), getParentScrollAreaWidth()); + if (QWidget *parentWidgetPtr = parentWidget()) { + parentWidgetPtr->setFixedSize(availableWidth, totalHeight); + } + } else { + const int availableHeight = qMax(rect.height(), getParentScrollAreaHeight()); - // Arrange all rows of items within the available width and get the total height used. - const int totalHeight = layoutAllRows(adjustedRect.x(), adjustedRect.y(), availableWidth); + const int totalWidth = layoutAllColumns(rect.x(), rect.y(), availableHeight); - // If the layout's parent is a QWidget, update its minimum size to ensure it can accommodate - // the arranged items' dimensions. - if (QWidget *parentWidgetPtr = parentWidget()) { - parentWidgetPtr->setMinimumSize(availableWidth, totalHeight); + if (QWidget *parentWidgetPtr = parentWidget()) { + parentWidgetPtr->setFixedSize(totalWidth, availableHeight); + } } } /** - * @brief Arranges items in rows based on the available width. - * Items are added to a row until the row's width exceeds `availableWidth`. - * Then, a new row is started. - * @param originX The starting x-coordinate for the row layout. - * @param originY The starting y-coordinate for the row layout. - * @param availableWidth The available width to lay out items. - * @return The y-coordinate of the final row's end position. + * @brief Lays out items into rows according to the available width, starting from a given origin. + * Each row is arranged within `availableWidth`, wrapping to a new row as necessary. + * @param originX The x-coordinate for the layout start position. + * @param originY The y-coordinate for the layout start position. + * @param availableWidth The width within which each row is constrained. + * @return The total height after arranging all rows. */ int FlowLayout::layoutAllRows(const int originX, const int originY, const int availableWidth) { - QVector rowItems; // Temporary storage for items in the current row. - int currentXPosition = originX; // Tracks the x-coordinate for placing items in the current row. - int currentYPosition = originY; // Tracks the y-coordinate, updated after each row. + QVector rowItems; // Holds items for the current row + int currentXPosition = originX; // Tracks the x-coordinate while placing items + int currentYPosition = originY; // Tracks the y-coordinate, moving down after each row - int rowHeight = 0; // Tracks the maximum height of items in the current row. + int rowHeight = 0; // Tracks the maximum height of items in the current row - // Iterate through all layout items to arrange them. for (QLayoutItem *item : items) { if (item == nullptr || item->isEmpty()) { continue; } - QSize itemSize = item->sizeHint(); // The suggested size for the item. - const int itemWidth = itemSize.width() + horizontalSpacing(); + QSize itemSize = item->sizeHint(); // The suggested size for the current item + int itemWidth = itemSize.width() + horizontalSpacing(); // Item width plus spacing - // Check if the item fits in the current row's remaining width. + // Check if the current item fits in the remaining width of the current row if (currentXPosition + itemWidth > availableWidth) { - // If not, layout the current row and start a new row. + // If not, layout the current row and start a new row layoutSingleRow(rowItems, originX, currentYPosition); - rowItems.clear(); // Clear the temporary storage for the new row. - currentXPosition = originX; // Reset x-position to the start of the new row. - currentYPosition += rowHeight + verticalSpacing(); // Move y-position down for the new row. - rowHeight = 0; // Reset row height for the new row. + rowItems.clear(); // Reset the list for the new row + currentXPosition = originX; // Reset x-position to the row's start + currentYPosition += rowHeight + verticalSpacing(); // Move y-position down to the next row + rowHeight = 0; // Reset row height for the new row } - // Add the item to the current row. + // Add the item to the current row rowItems.append(item); - rowHeight = qMax(rowHeight, itemSize.height()); // Update the row height to the tallest item. - currentXPosition += itemSize.width() + horizontalSpacing(); // Move x-position for the next item. + rowHeight = qMax(rowHeight, itemSize.height()); // Update the row's height to the tallest item + currentXPosition += itemWidth + horizontalSpacing(); // Move x-position for the next item } - // Layout the final row if there are remaining items. + // Layout the final row if there are any remaining items layoutSingleRow(rowItems, originX, currentYPosition); - currentYPosition += rowHeight; // Add the final row's height - return currentYPosition; + // Return the total height used, including the last row's height + return currentYPosition + rowHeight; } /** - * @brief Helper function for arranging a single row of items within specified bounds. - * @param rowItems Items to be arranged in the row. - * @param x The x-coordinate for starting the row. - * @param y The y-coordinate for starting the row. + * @brief Arranges a single row of items within specified x and y starting positions. + * @param rowItems A list of items to be arranged in the row. + * @param x The starting x-coordinate for the row. + * @param y The starting y-coordinate for the row. */ void FlowLayout::layoutSingleRow(const QVector &rowItems, int x, const int y) { - // Iterate through each item in the row and position it. for (QLayoutItem *item : rowItems) { if (item == nullptr || item->isEmpty()) { continue; } - QSize itemMaxSize = item->widget()->maximumSize(); // Get the item's maximum allowable size. - // Constrain the item's width and height to its size hint or maximum size. - int itemWidth = qMin(item->sizeHint().width(), itemMaxSize.width()); - int itemHeight = qMin(item->sizeHint().height(), itemMaxSize.height()); - // Set the item's geometry based on the calculated size and position. + // Get the maximum allowed size for the item + QSize itemMaxSize = item->widget()->maximumSize(); + // Constrain the item's width and height to its size hint or maximum size + const int itemWidth = qMin(item->sizeHint().width(), itemMaxSize.width()); + const int itemHeight = qMin(item->sizeHint().height(), itemMaxSize.height()); + // Set the item's geometry based on the computed size and position item->setGeometry(QRect(QPoint(x, y), QSize(itemWidth, itemHeight))); - // Move the x-position for the next item, including horizontal spacing. + // Move the x-position to the right, leaving space for horizontal spacing x += itemWidth + horizontalSpacing(); } } /** - * @brief Returns the preferred size for this layout. - * @return The maximum of all item size hints as a QSize. + * @brief Lays out items into columns according to the available height, starting from a given origin. + * Each column is arranged within `availableHeight`, wrapping to a new column as necessary. + * @param originX The x-coordinate for the layout start position. + * @param originY The y-coordinate for the layout start position. + * @param availableHeight The height within which each column is constrained. + * @return The total width after arranging all columns. */ -QSize FlowLayout::sizeHint() const +int FlowLayout::layoutAllColumns(const int originX, const int originY, const int availableHeight) { - QSize size; - for (const QLayoutItem *item : items) { - if (item != nullptr && !item->isEmpty()) { - size = size.expandedTo(item->sizeHint()); + QVector colItems; // Holds items for the current column + int currentXPosition = originX; // Tracks the x-coordinate while placing items + int currentYPosition = originY; // Tracks the y-coordinate, resetting for each new column + + int colWidth = 0; // Tracks the maximum width of items in the current column + + for (QLayoutItem *item : items) { + if (item == nullptr || item->isEmpty()) { + continue; } + + QSize itemSize = item->sizeHint(); // The suggested size for the current item + + // Check if the current item fits in the remaining height of the current column + if (currentYPosition + itemSize.height() > availableHeight) { + // If not, layout the current column and start a new column + layoutSingleColumn(colItems, currentXPosition, originY); + colItems.clear(); // Reset the list for the new column + currentYPosition = originY; // Reset y-position to the column's start + currentXPosition += colWidth; // Move x-position to the next column + colWidth = 0; // Reset column width for the new column + } + + // Add the item to the current column + colItems.append(item); + colWidth = qMax(colWidth, itemSize.width()); // Update the column's width to the widest item + currentYPosition += itemSize.height(); // Move y-position for the next item } - return size.isValid() ? size : QSize(0, 0); + + // Layout the final column if there are any remaining items + layoutSingleColumn(colItems, currentXPosition, originY); + + // Return the total width used, including the last column's width + return currentXPosition + colWidth; } /** - * @brief Returns the minimum size required to display all layout items. - * @return The minimum QSize needed by the layout. + * @brief Arranges a single column of items within specified x and y starting positions. + * @param colItems A list of items to be arranged in the column. + * @param x The starting x-coordinate for the column. + * @param y The starting y-coordinate for the column. + */ +void FlowLayout::layoutSingleColumn(const QVector &colItems, const int x, int y) +{ + for (QLayoutItem *item : colItems) { + if (item == nullptr) { + qCDebug(FlowLayoutLog) << "Item is null."; + continue; + } + + if (item->isEmpty()) { + qCDebug(FlowLayoutLog) << "Skipping empty item."; + continue; + } + + // Debugging: Print the item's widget class name and size hint + QWidget *widget = item->widget(); + if (widget) { + qCDebug(FlowLayoutLog) << "Widget class:" << widget->metaObject()->className(); + qCDebug(FlowLayoutLog) << "Widget size hint:" << widget->sizeHint(); + qCDebug(FlowLayoutLog) << "Widget maximum size:" << widget->maximumSize(); + qCDebug(FlowLayoutLog) << "Widget minimum size:" << widget->minimumSize(); + + // Debugging: Print child widgets + const QObjectList &children = widget->children(); + qCDebug(FlowLayoutLog) << "Child widgets:"; + for (QObject *child : children) { + if (QWidget *childWidget = qobject_cast(child)) { + qCDebug(FlowLayoutLog) << " - Child widget class:" << childWidget->metaObject()->className(); + qCDebug(FlowLayoutLog) << " Size hint:" << childWidget->sizeHint(); + qCDebug(FlowLayoutLog) << " Maximum size:" << childWidget->maximumSize(); + } + } + } else { + qCDebug(FlowLayoutLog) << "Item does not have a widget."; + } + + // Get the maximum allowed size for the item + QSize itemMaxSize = widget->maximumSize(); + // Constrain the item's width and height to its size hint or maximum size + const int itemWidth = qMin(item->sizeHint().width(), itemMaxSize.width()); + const int itemHeight = qMin(item->sizeHint().height(), itemMaxSize.height()); + // Debugging: Print the computed geometry + qCDebug(FlowLayoutLog) << "Computed geometry: x=" << x << ", y=" << y << ", width=" << itemWidth + << ", height=" << itemHeight; + + // Set the item's geometry based on the computed size and position + item->setGeometry(QRect(QPoint(x, y), QSize(itemWidth, itemHeight))); + + // Move the y-position down by the item's height to place the next item below + y += itemHeight; + } +} + +/** + * @brief Calculates the preferred size of the layout based on the flow direction. + * @return A QSize representing the ideal dimensions of the layout. + */ +QSize FlowLayout::sizeHint() const +{ + if (flowDirection == Qt::Horizontal) { + return calculateSizeHintHorizontal(); + } else { + return calculateSizeHintVertical(); + } +} + +/** + * @brief Calculates the minimum size required by the layout based on the flow direction. + * @return A QSize representing the minimum required dimensions. */ QSize FlowLayout::minimumSize() const { - QSize size; + if (flowDirection == Qt::Horizontal) { + return calculateMinimumSizeHorizontal(); + } else { + return calculateMinimumSizeVertical(); + } +} + +/** + * @brief Calculates the size hint for horizontal flow direction. + * @return A QSize representing the preferred dimensions. + */ +QSize FlowLayout::calculateSizeHintHorizontal() const +{ + int maxWidth = 0; // Tracks the maximum width needed + int totalHeight = 0; // Tracks the total height across all rows + int rowHeight = 0; // Tracks the height of the current row + int currentWidth = 0; // Tracks the current row's width + + const int availableWidth = getParentScrollAreaWidth() == 0 ? parentWidget()->width() : getParentScrollAreaWidth(); + + qCDebug(FlowLayoutLog) << "Calculating horizontal size hint. Available width:" << availableWidth; + for (const QLayoutItem *item : items) { - if (item != nullptr && !item->isEmpty()) { - size = size.expandedTo(item->minimumSize()); + if (!item || item->isEmpty()) { + qCDebug(FlowLayoutLog) << "Skipping empty item."; + continue; } + + QSize itemSize = item->sizeHint(); + int itemWidth = itemSize.width() + horizontalSpacing(); + qCDebug(FlowLayoutLog) << "Processing item. Size:" << itemSize << "Width with spacing:" << itemWidth; + + if (currentWidth + itemWidth > availableWidth) { + qCDebug(FlowLayoutLog) << "Row overflow. Current width:" << currentWidth << "Row height:" << rowHeight; + maxWidth = qMax(maxWidth, currentWidth); + totalHeight += rowHeight + verticalSpacing(); + qCDebug(FlowLayoutLog) << "Updated total height:" << totalHeight << "Max width so far:" << maxWidth; + + currentWidth = 0; + rowHeight = 0; + } + + currentWidth += itemWidth; + rowHeight = qMax(rowHeight, itemSize.height()); + qCDebug(FlowLayoutLog) << "Updated current width:" << currentWidth << "Updated row height:" << rowHeight; } - size.setWidth(qMin(size.width(), getParentScrollAreaWidth())); - size.setHeight(qMin(size.height(), getParentScrollAreaHeight())); + // Account for the final row + maxWidth = qMax(maxWidth, currentWidth); + totalHeight += rowHeight; + qCDebug(FlowLayoutLog) << "Final total height:" << totalHeight << "Final max width:" << maxWidth; - return size.isValid() ? size : QSize(0, 0); + return QSize(maxWidth, totalHeight); +} + +/** + * @brief Calculates the minimum size for horizontal flow direction. + * @return A QSize representing the minimum required dimensions. + */ +QSize FlowLayout::calculateMinimumSizeHorizontal() const +{ + int maxWidth = 0; // Tracks the maximum width of a row + int totalHeight = 0; // Tracks the total height across all rows + int rowHeight = 0; // Tracks the height of the current row + int currentWidth = 0; // Tracks the current row's width + + const int availableWidth = getParentScrollAreaWidth() == 0 ? parentWidget()->width() : getParentScrollAreaWidth(); + + qCDebug(FlowLayoutLog) << "Calculating horizontal minimum size. Available width:" << availableWidth; + + for (const QLayoutItem *item : items) { + if (!item || item->isEmpty()) { + qCDebug(FlowLayoutLog) << "Skipping empty item."; + continue; + } + + QSize itemMinSize = item->minimumSize(); + int itemWidth = itemMinSize.width() + horizontalSpacing(); + qCDebug(FlowLayoutLog) << "Processing item. Minimum size:" << itemMinSize << "Width with spacing:" << itemWidth; + + if (currentWidth + itemWidth > availableWidth) { + qCDebug(FlowLayoutLog) << "Row overflow. Current width:" << currentWidth << "Row height:" << rowHeight; + maxWidth = qMax(maxWidth, currentWidth); + totalHeight += rowHeight + verticalSpacing(); + qCDebug(FlowLayoutLog) << "Updated total height:" << totalHeight << "Max width so far:" << maxWidth; + + currentWidth = 0; + rowHeight = 0; + } + + currentWidth += itemWidth; + rowHeight = qMax(rowHeight, itemMinSize.height()); + qCDebug(FlowLayoutLog) << "Updated current width:" << currentWidth << "Updated row height:" << rowHeight; + } + + // Account for the final row + maxWidth = qMax(maxWidth, currentWidth); + totalHeight += rowHeight; + qCDebug(FlowLayoutLog) << "Final total height:" << totalHeight << "Final max width:" << maxWidth; + + return QSize(maxWidth, totalHeight); +} + +/** + * @brief Calculates the size hint for vertical flow direction. + * @return A QSize representing the preferred dimensions. + */ +QSize FlowLayout::calculateSizeHintVertical() const +{ + int totalWidth = 0; + int maxHeight = 0; + int colWidth = 0; + int currentHeight = 0; + + const int availableHeight = qMax(parentWidget()->height(), getParentScrollAreaHeight()); + + qCDebug(FlowLayoutLog) << "Calculating vertical size hint. Available height:" << availableHeight; + + for (const QLayoutItem *item : items) { + if (!item || item->isEmpty()) { + qCDebug(FlowLayoutLog) << "Skipping empty item."; + continue; + } + + QSize itemSize = item->sizeHint(); + qCDebug(FlowLayoutLog) << "Processing item. Size:" << itemSize; + + if (currentHeight + itemSize.height() > availableHeight) { + qCDebug(FlowLayoutLog) << "Column overflow. Current height:" << currentHeight + << "Column width:" << colWidth; + totalWidth += colWidth + horizontalSpacing(); + maxHeight = qMax(maxHeight, currentHeight); + qCDebug(FlowLayoutLog) << "Updated total width:" << totalWidth << "Max height so far:" << maxHeight; + + currentHeight = 0; + colWidth = 0; + } + + currentHeight += itemSize.height() + verticalSpacing(); + colWidth = qMax(colWidth, itemSize.width()); + qCDebug(FlowLayoutLog) << "Updated current height:" << currentHeight << "Updated column width:" << colWidth; + } + + // Account for the final column + totalWidth += colWidth; + maxHeight = qMax(maxHeight, currentHeight); + qCDebug(FlowLayoutLog) << "Final total width:" << totalWidth << "Final max height:" << maxHeight; + + return QSize(totalWidth, maxHeight); +} + +/** + * @brief Calculates the minimum size for vertical flow direction. + * @return A QSize representing the minimum required dimensions. + */ +QSize FlowLayout::calculateMinimumSizeVertical() const +{ + int totalWidth = 0; // Tracks the total width across all columns + int maxHeight = 0; // Tracks the maximum height of a column + int colWidth = 0; // Tracks the width of the current column + int currentHeight = 0; // Tracks the current column's height + + const int availableHeight = qMax(parentWidget()->height(), getParentScrollAreaHeight()); + + qCDebug(FlowLayoutLog) << "Calculating vertical minimum size. Available height:" << availableHeight; + + for (const QLayoutItem *item : items) { + if (!item || item->isEmpty()) { + qCDebug(FlowLayoutLog) << "Skipping empty item."; + continue; + } + + QSize itemMinSize = item->minimumSize(); + int itemHeight = itemMinSize.height() + verticalSpacing(); + qCDebug(FlowLayoutLog) << "Processing item. Minimum size:" << itemMinSize + << "Height with spacing:" << itemHeight; + + if (currentHeight + itemHeight > availableHeight) { + qCDebug(FlowLayoutLog) << "Column overflow. Current height:" << currentHeight + << "Column width:" << colWidth; + totalWidth += colWidth + horizontalSpacing(); + maxHeight = qMax(maxHeight, currentHeight); + qCDebug(FlowLayoutLog) << "Updated total width:" << totalWidth << "Max height so far:" << maxHeight; + + currentHeight = 0; + colWidth = 0; + } + + currentHeight += itemHeight; + colWidth = qMax(colWidth, itemMinSize.width()); + qCDebug(FlowLayoutLog) << "Updated current height:" << currentHeight << "Updated column width:" << colWidth; + } + + // Account for the final column + totalWidth += colWidth; + maxHeight = qMax(maxHeight, currentHeight); + qCDebug(FlowLayoutLog) << "Final total width:" << totalWidth << "Final max height:" << maxHeight; + + return QSize(totalWidth, maxHeight); } /** diff --git a/cockatrice/src/client/ui/layouts/flow_layout.h b/cockatrice/src/client/ui/layouts/flow_layout.h index 6fde7b802..f87386c76 100644 --- a/cockatrice/src/client/ui/layouts/flow_layout.h +++ b/cockatrice/src/client/ui/layouts/flow_layout.h @@ -3,16 +3,22 @@ #include #include +#include #include #include +inline Q_LOGGING_CATEGORY(FlowLayoutLog, "flow_layout"); + class FlowLayout : public QLayout { public: explicit FlowLayout(QWidget *parent = nullptr); - FlowLayout(QWidget *parent, int margin, int hSpacing, int vSpacing); + FlowLayout(QWidget *parent, Qt::Orientation _flowDirection, int margin = 0, int hSpacing = 0, int vSpacing = 0); ~FlowLayout() override; + QSize calculateMinimumSizeHorizontal() const; + QSize calculateSizeHintVertical() const; + QSize calculateMinimumSizeVertical() const; void addItem(QLayoutItem *item) override; [[nodiscard]] int count() const override; [[nodiscard]] QLayoutItem *itemAt(int index) const override; @@ -31,11 +37,15 @@ public: void setGeometry(const QRect &rect) override; virtual int layoutAllRows(int originX, int originY, int availableWidth); virtual void layoutSingleRow(const QVector &rowItems, int x, int y); + int layoutAllColumns(int originX, int originY, int availableHeight); + void layoutSingleColumn(const QVector &colItems, int x, int y); [[nodiscard]] QSize sizeHint() const override; [[nodiscard]] QSize minimumSize() const override; + QSize calculateSizeHintHorizontal() const; protected: QList items; // List to store layout items + Qt::Orientation flowDirection; int horizontalMargin; int verticalMargin; }; diff --git a/cockatrice/src/client/ui/layouts/horizontal_flow_layout.cpp b/cockatrice/src/client/ui/layouts/horizontal_flow_layout.cpp deleted file mode 100644 index 16f409db7..000000000 --- a/cockatrice/src/client/ui/layouts/horizontal_flow_layout.cpp +++ /dev/null @@ -1,142 +0,0 @@ -#include "horizontal_flow_layout.h" - -/** - * @brief Constructs a HorizontalFlowLayout instance with the specified parent widget. - * This layout arranges items in columns within the given height, automatically adjusting its width. - * @param parent The parent widget to which this layout belongs. - * @param margin The layout margin. - * @param hSpacing The horizontal spacing between items. - * @param vSpacing The vertical spacing between items. - */ -HorizontalFlowLayout::HorizontalFlowLayout(QWidget *parent, const int margin, const int hSpacing, const int vSpacing) - : FlowLayout(parent, margin, hSpacing, vSpacing) -{ -} - -/** - * @brief Destructor for HorizontalFlowLayout, responsible for cleaning up layout items. - */ -HorizontalFlowLayout::~HorizontalFlowLayout() -{ - QLayoutItem *item; - while ((item = FlowLayout::takeAt(0))) { - delete item; - } -} - -/** - * @brief Calculates the required width to display all items, given a specified height. - * This method arranges items into columns and determines the total width needed. - * @param height The available height for arranging layout items. - * @return The total width required to fit all items, organized in columns constrained by the given height. - */ -int HorizontalFlowLayout::heightForWidth(const int height) const -{ - int width = 0; - int colWidth = 0; - int colHeight = 0; - - for (const QLayoutItem *item : items) { - if (item == nullptr || item->isEmpty()) { - continue; - } - - int itemHeight = item->sizeHint().height(); - if (colHeight + itemHeight > height) { - width += colWidth; - colHeight = itemHeight; - colWidth = item->sizeHint().width(); - } else { - colHeight += itemHeight; - colWidth = qMax(colWidth, item->sizeHint().width()); - } - } - width += colWidth; // Add width of the last column - return width; -} - -/** - * @brief Sets the geometry of the layout items, arranging them in columns within the given height. - * @param rect The rectangle area defining the layout space. - */ -void HorizontalFlowLayout::setGeometry(const QRect &rect) -{ - const int availableHeight = qMax(rect.height(), getParentScrollAreaHeight()); - - const int totalWidth = layoutAllColumns(rect.x(), rect.y(), availableHeight); - - if (QWidget *parentWidgetPtr = parentWidget()) { - parentWidgetPtr->setMinimumSize(totalWidth, availableHeight); - } -} - -/** - * @brief Lays out items into columns according to the available height, starting from a given origin. - * Each column is arranged within `availableHeight`, wrapping to a new column as necessary. - * @param originX The x-coordinate for the layout start position. - * @param originY The y-coordinate for the layout start position. - * @param availableHeight The height within which each column is constrained. - * @return The total width after arranging all columns. - */ -int HorizontalFlowLayout::layoutAllColumns(const int originX, const int originY, const int availableHeight) -{ - QVector colItems; // Holds items for the current column - int currentXPosition = originX; // Tracks the x-coordinate while placing items - int currentYPosition = originY; // Tracks the y-coordinate, resetting for each new column - - int colWidth = 0; // Tracks the maximum width of items in the current column - - for (QLayoutItem *item : items) { - if (item == nullptr || item->isEmpty()) { - continue; - } - - QSize itemSize = item->sizeHint(); // The suggested size for the current item - - // Check if the current item fits in the remaining height of the current column - if (currentYPosition + itemSize.height() > availableHeight) { - // If not, layout the current column and start a new column - layoutSingleColumn(colItems, currentXPosition, originY); - colItems.clear(); // Reset the list for the new column - currentYPosition = originY; // Reset y-position to the column's start - currentXPosition += colWidth; // Move x-position to the next column - colWidth = 0; // Reset column width for the new column - } - - // Add the item to the current column - colItems.append(item); - colWidth = qMax(colWidth, itemSize.width()); // Update the column's width to the widest item - currentYPosition += itemSize.height(); // Move y-position for the next item - } - - // Layout the final column if there are any remaining items - layoutSingleColumn(colItems, currentXPosition, originY); - - // Return the total width used, including the last column's width - return currentXPosition + colWidth; -} - -/** - * @brief Arranges a single column of items within specified x and y starting positions. - * @param colItems A list of items to be arranged in the column. - * @param x The starting x-coordinate for the column. - * @param y The starting y-coordinate for the column. - */ -void HorizontalFlowLayout::layoutSingleColumn(const QVector &colItems, const int x, int y) -{ - for (QLayoutItem *item : colItems) { - if (item != nullptr && item->isEmpty()) { - continue; - } - - // Get the maximum allowed size for the item - QSize itemMaxSize = item->widget()->maximumSize(); - // Constrain the item's width and height to its size hint or maximum size - const int itemWidth = qMin(item->sizeHint().width(), itemMaxSize.width()); - const int itemHeight = qMin(item->sizeHint().height(), itemMaxSize.height()); - // Set the item's geometry based on the computed size and position - item->setGeometry(QRect(QPoint(x, y), QSize(itemWidth, itemHeight))); - // Move the y-position down by the item's height to place the next item below - y += itemHeight; - } -} diff --git a/cockatrice/src/client/ui/layouts/horizontal_flow_layout.h b/cockatrice/src/client/ui/layouts/horizontal_flow_layout.h deleted file mode 100644 index f6aebb284..000000000 --- a/cockatrice/src/client/ui/layouts/horizontal_flow_layout.h +++ /dev/null @@ -1,19 +0,0 @@ -#ifndef HORIZONTAL_FLOW_LAYOUT_H -#define HORIZONTAL_FLOW_LAYOUT_H - -#include "flow_layout.h" - -class HorizontalFlowLayout : public FlowLayout -{ -public: - explicit HorizontalFlowLayout(QWidget *parent = nullptr, int margin = 0, int hSpacing = 0, int vSpacing = 0); - ~HorizontalFlowLayout() override; - - [[nodiscard]] int heightForWidth(int height) const override; - - void setGeometry(const QRect &rect) override; - int layoutAllColumns(int originX, int originY, int availableHeight); - static void layoutSingleColumn(const QVector &colItems, int x, int y); -}; - -#endif // HORIZONTAL_FLOW_LAYOUT_H \ No newline at end of file diff --git a/cockatrice/src/client/ui/layouts/vertical_flow_layout.cpp b/cockatrice/src/client/ui/layouts/vertical_flow_layout.cpp deleted file mode 100644 index 90e154662..000000000 --- a/cockatrice/src/client/ui/layouts/vertical_flow_layout.cpp +++ /dev/null @@ -1,144 +0,0 @@ -#include "vertical_flow_layout.h" - -/** - * @brief Constructs a VerticalFlowLayout instance with the specified parent widget. - * This layout arranges items in rows within the given width, automatically adjusting its height. - * @param parent The parent widget to which this layout belongs. - * @param margin The layout margin. - * @param hSpacing The horizontal spacing between items. - * @param vSpacing The vertical spacing between items. - */ -VerticalFlowLayout::VerticalFlowLayout(QWidget *parent, const int margin, const int hSpacing, const int vSpacing) - : FlowLayout(parent, margin, hSpacing, vSpacing) -{ -} - -/** - * @brief Destructor for VerticalFlowLayout, responsible for cleaning up layout items. - */ -VerticalFlowLayout::~VerticalFlowLayout() -{ - QLayoutItem *item; - while ((item = FlowLayout::takeAt(0))) { - delete item; - } -} - -/** - * @brief Calculates the required height to display all items, given a specified width. - * This method arranges items into rows and determines the total height needed. - * @param width The available width for arranging layout items. - * @return The total height required to fit all items, organized in rows constrained by the given width. - */ -int VerticalFlowLayout::heightForWidth(const int width) const -{ - int height = 0; - int rowWidth = 0; - int rowHeight = 0; - - for (const QLayoutItem *item : items) { - if (item == nullptr || item->isEmpty()) { - continue; - } - - int itemWidth = item->sizeHint().width() + horizontalSpacing(); - if (rowWidth + itemWidth > width) { - height += rowHeight + verticalSpacing(); - rowWidth = itemWidth; - rowHeight = item->sizeHint().height(); - } else { - rowWidth += itemWidth; - rowHeight = qMax(rowHeight, item->sizeHint().height()); - } - } - height += rowHeight; // Add height of the last row - return height; -} - -/** - * @brief Sets the geometry of the layout items, arranging them in rows within the given width. - * @param rect The rectangle area defining the layout space. - */ -void VerticalFlowLayout::setGeometry(const QRect &rect) -{ - // If we have a parent scroll area, we're clamped to that, else we use our own rectangle. - const int availableWidth = getParentScrollAreaWidth() == 0 ? rect.width() : getParentScrollAreaWidth(); - - const int totalHeight = layoutAllRows(rect.x(), rect.y(), availableWidth); - - if (QWidget *parentWidgetPtr = parentWidget()) { - parentWidgetPtr->setMinimumSize(availableWidth, totalHeight); - } -} - -/** - * @brief Lays out items into rows according to the available width, starting from a given origin. - * Each row is arranged within `availableWidth`, wrapping to a new row as necessary. - * @param originX The x-coordinate for the layout start position. - * @param originY The y-coordinate for the layout start position. - * @param availableWidth The width within which each row is constrained. - * @return The total height after arranging all rows. - */ -int VerticalFlowLayout::layoutAllRows(const int originX, const int originY, const int availableWidth) -{ - QVector rowItems; // Holds items for the current row - int currentXPosition = originX; // Tracks the x-coordinate while placing items - int currentYPosition = originY; // Tracks the y-coordinate, moving down after each row - - int rowHeight = 0; // Tracks the maximum height of items in the current row - - for (QLayoutItem *item : items) { - if (item == nullptr || item->isEmpty()) { - continue; - } - - QSize itemSize = item->sizeHint(); // The suggested size for the current item - int itemWidth = itemSize.width() + horizontalSpacing(); // Item width plus spacing - - // Check if the current item fits in the remaining width of the current row - if (currentXPosition + itemWidth > availableWidth) { - // If not, layout the current row and start a new row - layoutSingleRow(rowItems, originX, currentYPosition); - rowItems.clear(); // Reset the list for the new row - currentXPosition = originX; // Reset x-position to the row's start - currentYPosition += rowHeight + verticalSpacing(); // Move y-position down to the next row - rowHeight = 0; // Reset row height for the new row - } - - // Add the item to the current row - rowItems.append(item); - rowHeight = qMax(rowHeight, itemSize.height()); // Update the row's height to the tallest item - currentXPosition += itemWidth + horizontalSpacing(); // Move x-position for the next item - } - - // Layout the final row if there are any remaining items - layoutSingleRow(rowItems, originX, currentYPosition); - - // Return the total height used, including the last row's height - return currentYPosition + rowHeight; -} - -/** - * @brief Arranges a single row of items within specified x and y starting positions. - * @param rowItems A list of items to be arranged in the row. - * @param x The starting x-coordinate for the row. - * @param y The starting y-coordinate for the row. - */ -void VerticalFlowLayout::layoutSingleRow(const QVector &rowItems, int x, const int y) -{ - for (QLayoutItem *item : rowItems) { - if (item == nullptr || item->isEmpty()) { - continue; - } - - // Get the maximum allowed size for the item - QSize itemMaxSize = item->widget()->maximumSize(); - // Constrain the item's width and height to its size hint or maximum size - const int itemWidth = qMin(item->sizeHint().width(), itemMaxSize.width()); - const int itemHeight = qMin(item->sizeHint().height(), itemMaxSize.height()); - // Set the item's geometry based on the computed size and position - item->setGeometry(QRect(QPoint(x, y), QSize(itemWidth, itemHeight))); - // Move the x-position to the right, leaving space for horizontal spacing - x += itemWidth + horizontalSpacing(); - } -} diff --git a/cockatrice/src/client/ui/layouts/vertical_flow_layout.h b/cockatrice/src/client/ui/layouts/vertical_flow_layout.h deleted file mode 100644 index 5dcd4945c..000000000 --- a/cockatrice/src/client/ui/layouts/vertical_flow_layout.h +++ /dev/null @@ -1,19 +0,0 @@ -#ifndef VERTICAL_FLOW_LAYOUT_H -#define VERTICAL_FLOW_LAYOUT_H - -#include "flow_layout.h" - -class VerticalFlowLayout : public FlowLayout -{ -public: - explicit VerticalFlowLayout(QWidget *parent = nullptr, int margin = 0, int hSpacing = 0, int vSpacing = 0); - ~VerticalFlowLayout() override; - - [[nodiscard]] int heightForWidth(int width) const override; - - void setGeometry(const QRect &rect) override; - int layoutAllRows(int originX, int originY, int availableWidth) override; - void layoutSingleRow(const QVector &rowItems, int x, int y) override; -}; - -#endif // VERTICAL_FLOW_LAYOUT_H \ No newline at end of file diff --git a/cockatrice/src/client/ui/widgets/general/layout_containers/flow_widget.cpp b/cockatrice/src/client/ui/widgets/general/layout_containers/flow_widget.cpp index 958cc98be..a413d2927 100644 --- a/cockatrice/src/client/ui/widgets/general/layout_containers/flow_widget.cpp +++ b/cockatrice/src/client/ui/widgets/general/layout_containers/flow_widget.cpp @@ -5,10 +5,8 @@ #include "flow_widget.h" -#include "../../../layouts/horizontal_flow_layout.h" -#include "../../../layouts/vertical_flow_layout.h" - #include +#include #include #include #include @@ -21,41 +19,53 @@ * @param verticalPolicy The vertical scroll bar policy for the scroll area. */ FlowWidget::FlowWidget(QWidget *parent, + const Qt::Orientation _flowDirection, const Qt::ScrollBarPolicy horizontalPolicy, const Qt::ScrollBarPolicy verticalPolicy) - : QWidget(parent) + : QWidget(parent), flowDirection(_flowDirection) { // Main Widget and Layout - this->setMinimumSize(0, 0); - this->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - mainLayout = new QHBoxLayout(); + if (_flowDirection == Qt::Horizontal) { + this->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum); + this->setMinimumWidth(0); + } else { + this->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Expanding); + this->setMinimumHeight(0); + } + mainLayout = new QHBoxLayout(this); this->setLayout(mainLayout); - // Flow Layout inside the scroll area - container = new QWidget(); - - if (horizontalPolicy != Qt::ScrollBarAlwaysOff && verticalPolicy == Qt::ScrollBarAlwaysOff) { - flowLayout = new HorizontalFlowLayout(container); - } else if (horizontalPolicy == Qt::ScrollBarAlwaysOff && verticalPolicy != Qt::ScrollBarAlwaysOff) { - flowLayout = new VerticalFlowLayout(container); + if (horizontalPolicy != Qt::ScrollBarAlwaysOff || verticalPolicy != Qt::ScrollBarAlwaysOff) { + // Scroll Area, which should expand as much as possible, since it should be the only direct child widget. + scrollArea = new QScrollArea(this); + scrollArea->setWidgetResizable(true); + scrollArea->setMinimumSize(0, 0); + scrollArea->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + // Set scrollbar policies + scrollArea->setHorizontalScrollBarPolicy(horizontalPolicy); + scrollArea->setVerticalScrollBarPolicy(verticalPolicy); } else { - flowLayout = new FlowLayout(container, 0, 0, 0); + scrollArea = nullptr; } + // Flow Layout inside the scroll area + if (horizontalPolicy == Qt::ScrollBarAlwaysOff && verticalPolicy == Qt::ScrollBarAlwaysOff) { + container = new QWidget(this); + } else { + container = new QWidget(scrollArea); + } + + flowLayout = new FlowLayout(container, flowDirection); + container->setLayout(flowLayout); // The container should expand as much as possible, trusting the scrollArea to constrain it. - container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - container->setMinimumSize(0, 0); - - // Scroll Area, which should expand as much as possible, since it should be the only direct child widget. - scrollArea = new QScrollArea(); - scrollArea->setWidgetResizable(true); - scrollArea->setMinimumSize(0, 0); - scrollArea->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - - // Set scrollbar policies - scrollArea->setHorizontalScrollBarPolicy(horizontalPolicy); - scrollArea->setVerticalScrollBarPolicy(verticalPolicy); + if (_flowDirection == Qt::Horizontal) { + container->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + container->setMinimumWidth(0); + } else { + container->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + container->setMinimumHeight(0); + } // Use the FlowLayout container directly if we disable the ScrollArea if (horizontalPolicy == Qt::ScrollBarAlwaysOff && verticalPolicy == Qt::ScrollBarAlwaysOff) { @@ -75,15 +85,6 @@ FlowWidget::FlowWidget(QWidget *parent, */ void FlowWidget::addWidget(QWidget *widget_to_add) const { - // Adjust size policy if scrollbars are disabled - if (scrollArea->horizontalScrollBarPolicy() == Qt::ScrollBarAlwaysOff) { - widget_to_add->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred); - } - if (scrollArea->verticalScrollBarPolicy() == Qt::ScrollBarAlwaysOff) { - widget_to_add->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum); - } - - // Add the widget to the flow layout flowLayout->addWidget(widget_to_add); } @@ -106,15 +107,7 @@ void FlowWidget::clearLayout() delete item; // Delete the layout item } } else { - if (scrollArea->horizontalScrollBarPolicy() != Qt::ScrollBarAlwaysOff && - scrollArea->verticalScrollBarPolicy() == Qt::ScrollBarAlwaysOff) { - flowLayout = new HorizontalFlowLayout(container); - } else if (scrollArea->horizontalScrollBarPolicy() == Qt::ScrollBarAlwaysOff && - scrollArea->verticalScrollBarPolicy() != Qt::ScrollBarAlwaysOff) { - flowLayout = new VerticalFlowLayout(container); - } else { - flowLayout = new FlowLayout(container, 0, 0, 0); - } + flowLayout = new FlowLayout(container, flowDirection); container->setLayout(flowLayout); } } @@ -139,6 +132,8 @@ void FlowWidget::resizeEvent(QResizeEvent *event) // Ensure the scroll area and its content adjust correctly if (scrollArea != nullptr && scrollArea->widget() != nullptr) { scrollArea->widget()->adjustSize(); + } else { + container->adjustSize(); } } diff --git a/cockatrice/src/client/ui/widgets/general/layout_containers/flow_widget.h b/cockatrice/src/client/ui/widgets/general/layout_containers/flow_widget.h index a514b56a7..baed97b8d 100644 --- a/cockatrice/src/client/ui/widgets/general/layout_containers/flow_widget.h +++ b/cockatrice/src/client/ui/widgets/general/layout_containers/flow_widget.h @@ -3,15 +3,22 @@ #include "../../../layouts/flow_layout.h" #include +#include #include #include +inline Q_LOGGING_CATEGORY(FlowWidgetLog, "flow_widget"); +inline Q_LOGGING_CATEGORY(FlowWidgetSizeLog, "flow_widget.size"); + class FlowWidget final : public QWidget { Q_OBJECT public: - FlowWidget(QWidget *parent, Qt::ScrollBarPolicy horizontalPolicy, Qt::ScrollBarPolicy verticalPolicy); + FlowWidget(QWidget *parent, + Qt::Orientation orientation, + Qt::ScrollBarPolicy horizontalPolicy, + Qt::ScrollBarPolicy verticalPolicy); void addWidget(QWidget *widget_to_add) const; void removeWidget(QWidget *widgetToRemove) const; void clearLayout(); @@ -27,6 +34,7 @@ protected: void resizeEvent(QResizeEvent *event) override; private: + Qt::Orientation flowDirection; QHBoxLayout *mainLayout; FlowLayout *flowLayout; QWidget *container; diff --git a/cockatrice/src/client/ui/widgets/printing_selector/printing_selector.cpp b/cockatrice/src/client/ui/widgets/printing_selector/printing_selector.cpp index 9943b9403..4a3d9fe80 100644 --- a/cockatrice/src/client/ui/widgets/printing_selector/printing_selector.cpp +++ b/cockatrice/src/client/ui/widgets/printing_selector/printing_selector.cpp @@ -45,7 +45,7 @@ PrintingSelector::PrintingSelector(QWidget *parent, searchBar->setVisible(SettingsCache::instance().getPrintingSelectorSearchBarVisible()); layout->addWidget(searchBar); - flowWidget = new FlowWidget(this, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded); + flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded); layout->addWidget(flowWidget); cardSizeWidget = new CardSizeWidget(this, flowWidget, SettingsCache::instance().getPrintingSelectorCardSize()); diff --git a/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.cpp b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.cpp index f4cab85fe..7f2dfaab0 100644 --- a/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.cpp +++ b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.cpp @@ -23,7 +23,7 @@ DeckPreviewDeckTagsDisplayWidget::DeckPreviewDeckTagsDisplayWidget(DeckPreviewWi connect(deckLoader, &DeckList::deckTagsChanged, this, &DeckPreviewDeckTagsDisplayWidget::refreshTags); - flowWidget = new FlowWidget(this, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded); + flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded); for (const QString &tag : this->deckLoader->getTags()) { flowWidget->addWidget(new DeckPreviewTagDisplayWidget(this, tag)); } diff --git a/cockatrice/src/client/ui/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.cpp b/cockatrice/src/client/ui/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.cpp index bf22fda3d..87ffac90c 100644 --- a/cockatrice/src/client/ui/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.cpp +++ b/cockatrice/src/client/ui/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.cpp @@ -20,7 +20,7 @@ VisualDeckStorageTagFilterWidget::VisualDeckStorageTagFilterWidget(VisualDeckSto setFixedHeight(100); - auto *flowWidget = new FlowWidget(this, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded); + auto *flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded); layout->addWidget(flowWidget); } diff --git a/cockatrice/src/client/ui/widgets/visual_deck_storage/visual_deck_storage_widget.cpp b/cockatrice/src/client/ui/widgets/visual_deck_storage/visual_deck_storage_widget.cpp index 124b35c34..53c65f30d 100644 --- a/cockatrice/src/client/ui/widgets/visual_deck_storage/visual_deck_storage_widget.cpp +++ b/cockatrice/src/client/ui/widgets/visual_deck_storage/visual_deck_storage_widget.cpp @@ -37,7 +37,7 @@ VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(pare layout->addLayout(searchAndSortLayout); layout->addWidget(tagFilterWidget); - flowWidget = new FlowWidget(this, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded); + flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded); layout->addWidget(flowWidget); cardSizeWidget = new CardSizeWidget(this, flowWidget, SettingsCache::instance().getVisualDeckStorageCardSize()); From ce416df3fb0368c256f45f30af9c04868a6b32ff Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Sat, 25 Jan 2025 04:20:30 +0100 Subject: [PATCH 5/5] Add a dialog to prompt user to convert to .cod format if trying to apply tags to a .txt deck. (#5514) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add a dialog to prompt user to convert to .cod format if trying to apply tags to a .txt deck. * Lint mocks. * Address comments, move dialog to appropriate folder. * Unlint. --------- Co-authored-by: Lukas BrĂ¼bach --- cockatrice/CMakeLists.txt | 1 + .../deck_preview_tag_addition_widget.cpp | 51 +++++++++++++++++-- .../deck_preview/deck_preview_widget.cpp | 8 ++- .../deck_preview/deck_preview_widget.h | 1 + cockatrice/src/deck/deck_loader.cpp | 48 +++++++++++++++++ cockatrice/src/deck/deck_loader.h | 1 + .../dlg_convert_deck_to_cod_format.cpp | 40 +++++++++++++++ .../dialogs/dlg_convert_deck_to_cod_format.h | 29 +++++++++++ cockatrice/src/dialogs/dlg_settings.cpp | 13 +++++ cockatrice/src/dialogs/dlg_settings.h | 2 + cockatrice/src/settings/cache_settings.cpp | 15 ++++++ cockatrice/src/settings/cache_settings.h | 12 +++++ dbconverter/src/mocks.cpp | 7 +++ tests/carddatabase/mocks.cpp | 7 +++ 14 files changed, 229 insertions(+), 6 deletions(-) create mode 100644 cockatrice/src/dialogs/dlg_convert_deck_to_cod_format.cpp create mode 100644 cockatrice/src/dialogs/dlg_convert_deck_to_cod_format.h diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 0fc6e78b3..7881d330b 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -35,6 +35,7 @@ set(cockatrice_SOURCES src/deck/deck_list_model.cpp src/deck/deck_stats_interface.cpp src/dialogs/dlg_connect.cpp + src/dialogs/dlg_convert_deck_to_cod_format.cpp src/dialogs/dlg_create_token.cpp src/dialogs/dlg_create_game.cpp src/dialogs/dlg_edit_avatar.cpp diff --git a/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_addition_widget.cpp b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_addition_widget.cpp index 3572daaee..a188f23b6 100644 --- a/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_addition_widget.cpp +++ b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_addition_widget.cpp @@ -1,5 +1,7 @@ #include "deck_preview_tag_addition_widget.h" +#include "../../../../../dialogs/dlg_convert_deck_to_cod_format.h" +#include "../../../../../settings/cache_settings.h" #include "deck_preview_tag_dialog.h" #include @@ -40,11 +42,50 @@ void DeckPreviewTagAdditionWidget::mousePressEvent(QMouseEvent *event) QStringList knownTags = parent->parent->parent->gatherAllTagsFromFlowWidget(); QStringList activeTags = parent->deckLoader->getTags(); - DeckPreviewTagDialog dialog(knownTags, activeTags); - if (dialog.exec() == QDialog::Accepted) { - QStringList updatedTags = dialog.getActiveTags(); - parent->deckLoader->setTags(updatedTags); - parent->deckLoader->saveToFile(parent->parent->filePath, DeckLoader::CockatriceFormat); + bool canAddTags = true; + + if (parent->deckLoader->getLastFileFormat() != DeckLoader::CockatriceFormat) { + canAddTags = false; + // Retrieve saved preference if the prompt is disabled + if (!SettingsCache::instance().getVisualDeckStoragePromptForConversion()) { + if (SettingsCache::instance().getVisualDeckStorageAlwaysConvert()) { + parent->deckLoader->convertToCockatriceFormat(parent->parent->filePath); + parent->parent->filePath = parent->deckLoader->getLastFileName(); + parent->parent->refreshBannerCardText(); + canAddTags = true; + } + } else { + // Show the dialog to the user + DialogConvertDeckToCodFormat conversionDialog(parent); + if (conversionDialog.exec() == QDialog::Accepted) { + parent->deckLoader->convertToCockatriceFormat(parent->parent->filePath); + parent->parent->filePath = parent->deckLoader->getLastFileName(); + parent->parent->refreshBannerCardText(); + canAddTags = true; + + if (conversionDialog.dontAskAgain()) { + SettingsCache::instance().setVisualDeckStoragePromptForConversion(Qt::CheckState::Unchecked); + SettingsCache::instance().setVisualDeckStorageAlwaysConvert(Qt::CheckState::Checked); + } + } else { + SettingsCache::instance().setVisualDeckStorageAlwaysConvert(Qt::CheckState::Unchecked); + + if (conversionDialog.dontAskAgain()) { + SettingsCache::instance().setVisualDeckStoragePromptForConversion(Qt::CheckState::Unchecked); + } else { + SettingsCache::instance().setVisualDeckStoragePromptForConversion(Qt::CheckState::Checked); + } + } + } + } + + if (canAddTags) { + DeckPreviewTagDialog dialog(knownTags, activeTags); + if (dialog.exec() == QDialog::Accepted) { + QStringList updatedTags = dialog.getActiveTags(); + parent->deckLoader->setTags(updatedTags); + parent->deckLoader->saveToFile(parent->parent->filePath, DeckLoader::CockatriceFormat); + } } } diff --git a/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp index 18bc771d1..59e02ccbc 100644 --- a/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp +++ b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp @@ -17,7 +17,7 @@ DeckPreviewWidget::DeckPreviewWidget(VisualDeckStorageWidget *_parent, const QSt deckLoader = new DeckLoader(); connect(deckLoader, &DeckLoader::loadFinished, this, &DeckPreviewWidget::initializeUi); - deckLoader->loadFromFileAsync(filePath, DeckLoader::CockatriceFormat, false); + deckLoader->loadFromFileAsync(filePath, DeckLoader::getFormatFromName(filePath), false); bannerCardDisplayWidget = new DeckPreviewCardPictureWidget(this); @@ -88,6 +88,12 @@ void DeckPreviewWidget::setFilePath(const QString &_filePath) filePath = _filePath; } +void DeckPreviewWidget::refreshBannerCardText() +{ + bannerCardDisplayWidget->setOverlayText( + deckLoader->getName().isEmpty() ? QFileInfo(deckLoader->getLastFileName()).fileName() : deckLoader->getName()); +} + void DeckPreviewWidget::imageClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance) { Q_UNUSED(instance); diff --git a/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h index 689c94b18..91a2060a3 100644 --- a/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h +++ b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h @@ -33,6 +33,7 @@ signals: public slots: void setFilePath(const QString &filePath); + void refreshBannerCardText(); void imageClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance); void imageDoubleClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance); void initializeUi(bool deckLoadSuccess); diff --git a/cockatrice/src/deck/deck_loader.cpp b/cockatrice/src/deck/deck_loader.cpp index bfb8711ea..ed1d8fa71 100644 --- a/cockatrice/src/deck/deck_loader.cpp +++ b/cockatrice/src/deck/deck_loader.cpp @@ -6,6 +6,7 @@ #include "decklist.h" #include +#include #include #include #include @@ -481,6 +482,53 @@ void DeckLoader::saveToStream_DeckZoneCards(QTextStream &out, } } +bool DeckLoader::convertToCockatriceFormat(QString fileName) +{ + // Change the file extension to .cod + QFileInfo fileInfo(fileName); + QString newFileName = QDir::toNativeSeparators(fileInfo.path() + "/" + fileInfo.completeBaseName() + ".cod"); + + // Open the new file for writing + QFile file(newFileName); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { + qCWarning(DeckLoaderLog) << "Failed to open file for writing:" << newFileName; + return false; + } + + bool result = false; + + // Perform file modifications based on the detected format + switch (getFormatFromName(fileName)) { + case PlainTextFormat: + // Save in Cockatrice's native format + result = saveToFile_Native(&file); + break; + case CockatriceFormat: + qCInfo(DeckLoaderLog) << "File is already in Cockatrice format. No conversion needed."; + result = true; + break; + default: + qCWarning(DeckLoaderLog) << "Unsupported file format for conversion:" << fileName; + result = false; + break; + } + + file.close(); + + // Delete the old file if conversion was successful + if (result) { + if (!QFile::remove(fileName)) { + qCWarning(DeckLoaderLog) << "Failed to delete original file:" << fileName; + } else { + qCInfo(DeckLoaderLog) << "Original file deleted successfully:" << fileName; + } + lastFileName = newFileName; + lastFileFormat = CockatriceFormat; + } + + return result; +} + QString DeckLoader::getCardZoneFromName(QString cardName, QString currentZoneName) { CardInfoPtr card = CardDatabaseManager::getInstance()->getCard(cardName); diff --git a/cockatrice/src/deck/deck_loader.h b/cockatrice/src/deck/deck_loader.h index 0e9dfa374..d9863ac3e 100644 --- a/cockatrice/src/deck/deck_loader.h +++ b/cockatrice/src/deck/deck_loader.h @@ -59,6 +59,7 @@ public: // overload bool saveToStream_Plain(QTextStream &out, bool addComments = true, bool addSetNameAndNumber = true); + bool convertToCockatriceFormat(QString fileName); protected: void saveToStream_DeckHeader(QTextStream &out); diff --git a/cockatrice/src/dialogs/dlg_convert_deck_to_cod_format.cpp b/cockatrice/src/dialogs/dlg_convert_deck_to_cod_format.cpp new file mode 100644 index 000000000..198fa259b --- /dev/null +++ b/cockatrice/src/dialogs/dlg_convert_deck_to_cod_format.cpp @@ -0,0 +1,40 @@ +#include "dlg_convert_deck_to_cod_format.h" + +#include +#include +#include +#include + +DialogConvertDeckToCodFormat::DialogConvertDeckToCodFormat(QWidget *parent) : QDialog(parent) +{ + layout = new QVBoxLayout(this); + label = new QLabel(); + layout->addWidget(label); + + dontAskAgainCheckbox = new QCheckBox(this); + layout->addWidget(dontAskAgainCheckbox); + + buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + layout->addWidget(buttonBox); + + connect(buttonBox, &QDialogButtonBox::accepted, this, [this]() { accept(); }); + + connect(buttonBox, &QDialogButtonBox::rejected, this, [this]() { reject(); }); + + setLayout(layout); + retranslateUi(); +} + +void DialogConvertDeckToCodFormat::retranslateUi() +{ + setWindowTitle(tr("Deck Format Conversion")); + label->setText( + tr("You tried to add a tag to a .txt format deck.\n Tags can only be added to .cod format decks.\n Do " + "you want to convert the deck to the .cod format?")); + dontAskAgainCheckbox->setText(tr("Remember and automatically apply choice in the future")); +} + +bool DialogConvertDeckToCodFormat::dontAskAgain() const +{ + return dontAskAgainCheckbox->isChecked(); +} diff --git a/cockatrice/src/dialogs/dlg_convert_deck_to_cod_format.h b/cockatrice/src/dialogs/dlg_convert_deck_to_cod_format.h new file mode 100644 index 000000000..88a19591e --- /dev/null +++ b/cockatrice/src/dialogs/dlg_convert_deck_to_cod_format.h @@ -0,0 +1,29 @@ +#ifndef DIALOG_CONVERT_DECK_TO_COD_FORMAT_H +#define DIALOG_CONVERT_DECK_TO_COD_FORMAT_H + +#include +#include +#include +#include +#include + +class DialogConvertDeckToCodFormat : public QDialog +{ + Q_OBJECT + +public: + explicit DialogConvertDeckToCodFormat(QWidget *parent); + void retranslateUi(); + + bool dontAskAgain() const; + +private: + QVBoxLayout *layout; + QLabel *label; + QCheckBox *dontAskAgainCheckbox; + QDialogButtonBox *buttonBox; + + Q_DISABLE_COPY(DialogConvertDeckToCodFormat) +}; + +#endif // DIALOG_CONVERT_DECK_TO_COD_FORMAT_H diff --git a/cockatrice/src/dialogs/dlg_settings.cpp b/cockatrice/src/dialogs/dlg_settings.cpp index 496aa7ee6..8010eaf39 100644 --- a/cockatrice/src/dialogs/dlg_settings.cpp +++ b/cockatrice/src/dialogs/dlg_settings.cpp @@ -573,6 +573,15 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage() connect(&useTearOffMenusCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(), [](const QT_STATE_CHANGED_T state) { SettingsCache::instance().setUseTearOffMenus(state == Qt::Checked); }); + visualDeckStoragePromptForConversionCheckBox.setChecked( + SettingsCache::instance().getVisualDeckStoragePromptForConversion()); + connect(&visualDeckStoragePromptForConversionCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(), + &SettingsCache::setVisualDeckStoragePromptForConversion); + + visualDeckStorageAlwaysConvertCheckBox.setChecked(SettingsCache::instance().getVisualDeckStorageAlwaysConvert()); + connect(&visualDeckStorageAlwaysConvertCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(), + &SettingsCache::setVisualDeckStorageAlwaysConvert); + auto *generalGrid = new QGridLayout; generalGrid->addWidget(&doubleClickToPlayCheckBox, 0, 0); generalGrid->addWidget(&clickPlaysAllSelectedCheckBox, 1, 0); @@ -580,6 +589,8 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage() generalGrid->addWidget(&closeEmptyCardViewCheckBox, 3, 0); generalGrid->addWidget(&annotateTokensCheckBox, 4, 0); generalGrid->addWidget(&useTearOffMenusCheckBox, 5, 0); + generalGrid->addWidget(&visualDeckStoragePromptForConversionCheckBox, 6, 0); + generalGrid->addWidget(&visualDeckStorageAlwaysConvertCheckBox, 7, 0); generalGroupBox = new QGroupBox; generalGroupBox->setLayout(generalGrid); @@ -658,6 +669,8 @@ void UserInterfaceSettingsPage::retranslateUi() closeEmptyCardViewCheckBox.setText(tr("Close card view window when last card is removed")); annotateTokensCheckBox.setText(tr("Annotate card text on tokens")); useTearOffMenusCheckBox.setText(tr("Use tear-off menus, allowing right click menus to persist on screen")); + visualDeckStoragePromptForConversionCheckBox.setText(tr("Prompt before converting .txt decks to .cod format")); + visualDeckStorageAlwaysConvertCheckBox.setText(tr("Always convert if not prompted")); notificationsGroupBox->setTitle(tr("Notifications settings")); notificationsEnabledCheckBox.setText(tr("Enable notifications in taskbar")); specNotificationsEnabledCheckBox.setText(tr("Notify in the taskbar for game events while you are spectating")); diff --git a/cockatrice/src/dialogs/dlg_settings.h b/cockatrice/src/dialogs/dlg_settings.h index 489d798e9..ad9429cd2 100644 --- a/cockatrice/src/dialogs/dlg_settings.h +++ b/cockatrice/src/dialogs/dlg_settings.h @@ -142,6 +142,8 @@ private: QCheckBox closeEmptyCardViewCheckBox; QCheckBox annotateTokensCheckBox; QCheckBox useTearOffMenusCheckBox; + QCheckBox visualDeckStoragePromptForConversionCheckBox; + QCheckBox visualDeckStorageAlwaysConvertCheckBox; QCheckBox tapAnimationCheckBox; QCheckBox openDeckInNewTabCheckBox; QLabel rewindBufferingMsLabel; diff --git a/cockatrice/src/settings/cache_settings.cpp b/cockatrice/src/settings/cache_settings.cpp index 329c08a20..5bf114706 100644 --- a/cockatrice/src/settings/cache_settings.cpp +++ b/cockatrice/src/settings/cache_settings.cpp @@ -274,6 +274,9 @@ SettingsCache::SettingsCache() settings->value("interface/visualdeckstoragedrawunusedcoloridentities", true).toBool(); visualDeckStorageUnusedColorIdentitiesOpacity = settings->value("interface/visualdeckstorageunusedcoloridentitiesopacity", 15).toInt(); + visualDeckStoragePromptForConversion = + settings->value("interface/visualdeckstoragepromptforconversion", true).toBool(); + visualDeckStorageAlwaysConvert = settings->value("interface/visualdeckstoragealwaysconvert", false).toBool(); horizontalHand = settings->value("hand/horizontal", true).toBool(); invertVerticalCoordinate = settings->value("table/invert_vertical", false).toBool(); minPlayersForMultiColumnLayout = settings->value("interface/min_players_multicolumn", 4).toInt(); @@ -699,6 +702,18 @@ void SettingsCache::setVisualDeckStorageUnusedColorIdentitiesOpacity(int _visual visualDeckStorageUnusedColorIdentitiesOpacity); } +void SettingsCache::setVisualDeckStoragePromptForConversion(QT_STATE_CHANGED_T _visualDeckStoragePromptForConversion) +{ + visualDeckStoragePromptForConversion = _visualDeckStoragePromptForConversion; + settings->setValue("interface/visualdeckstoragepromptforconversion", visualDeckStoragePromptForConversion); +} + +void SettingsCache::setVisualDeckStorageAlwaysConvert(QT_STATE_CHANGED_T _visualDeckStorageAlwaysConvert) +{ + visualDeckStorageAlwaysConvert = _visualDeckStorageAlwaysConvert; + settings->setValue("interface/visualdeckstoragealwaysconvert", visualDeckStorageAlwaysConvert); +} + void SettingsCache::setHorizontalHand(QT_STATE_CHANGED_T _horizontalHand) { horizontalHand = static_cast(_horizontalHand); diff --git a/cockatrice/src/settings/cache_settings.h b/cockatrice/src/settings/cache_settings.h index 7b5649904..50c970a6b 100644 --- a/cockatrice/src/settings/cache_settings.h +++ b/cockatrice/src/settings/cache_settings.h @@ -133,6 +133,8 @@ private: int visualDeckStorageCardSize; bool visualDeckStorageDrawUnusedColorIdentities; int visualDeckStorageUnusedColorIdentitiesOpacity; + bool visualDeckStoragePromptForConversion; + bool visualDeckStorageAlwaysConvert; bool horizontalHand; bool invertVerticalCoordinate; int minPlayersForMultiColumnLayout; @@ -424,6 +426,14 @@ public: { return visualDeckStorageUnusedColorIdentitiesOpacity; } + bool getVisualDeckStoragePromptForConversion() const + { + return visualDeckStoragePromptForConversion; + } + bool getVisualDeckStorageAlwaysConvert() const + { + return visualDeckStorageAlwaysConvert; + } bool getHorizontalHand() const { return horizontalHand; @@ -748,6 +758,8 @@ public slots: void setVisualDeckStorageCardSize(int _visualDeckStorageCardSize); void setVisualDeckStorageDrawUnusedColorIdentities(QT_STATE_CHANGED_T _visualDeckStorageDrawUnusedColorIdentities); void setVisualDeckStorageUnusedColorIdentitiesOpacity(int _visualDeckStorageUnusedColorIdentitiesOpacity); + void setVisualDeckStoragePromptForConversion(QT_STATE_CHANGED_T _visualDeckStoragePromptForConversion); + void setVisualDeckStorageAlwaysConvert(QT_STATE_CHANGED_T _visualDeckStorageAlwaysConvert); void setHorizontalHand(QT_STATE_CHANGED_T _horizontalHand); void setInvertVerticalCoordinate(QT_STATE_CHANGED_T _invertVerticalCoordinate); void setMinPlayersForMultiColumnLayout(int _minPlayersForMultiColumnLayout); diff --git a/dbconverter/src/mocks.cpp b/dbconverter/src/mocks.cpp index 00336305a..a34dcc191 100644 --- a/dbconverter/src/mocks.cpp +++ b/dbconverter/src/mocks.cpp @@ -222,6 +222,13 @@ void SettingsCache::setVisualDeckStorageUnusedColorIdentitiesOpacity( int /* _visualDeckStorageUnusedColorIdentitiesOpacity */) { } +void SettingsCache::setVisualDeckStoragePromptForConversion( + QT_STATE_CHANGED_T /* _visualDeckStoragePromptForConversion */) +{ +} +void SettingsCache::setVisualDeckStorageAlwaysConvert(QT_STATE_CHANGED_T /* _visualDeckStorageAlwaysConvert */) +{ +} void SettingsCache::setHorizontalHand(QT_STATE_CHANGED_T /* _horizontalHand */) { } diff --git a/tests/carddatabase/mocks.cpp b/tests/carddatabase/mocks.cpp index 358b8361d..40e8fc5a9 100644 --- a/tests/carddatabase/mocks.cpp +++ b/tests/carddatabase/mocks.cpp @@ -226,6 +226,13 @@ void SettingsCache::setVisualDeckStorageUnusedColorIdentitiesOpacity( int /* _visualDeckStorageUnusedColorIdentitiesOpacity */) { } +void SettingsCache::setVisualDeckStoragePromptForConversion( + QT_STATE_CHANGED_T /* _visualDeckStoragePromptForConversion */) +{ +} +void SettingsCache::setVisualDeckStorageAlwaysConvert(QT_STATE_CHANGED_T /* _visualDeckStorageAlwaysConvert */) +{ +} void SettingsCache::setHorizontalHand(QT_STATE_CHANGED_T /* _horizontalHand */) { }