From dd8ac14f99a90bcd3c58133474c1eb5ec8b8a4be Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Sun, 12 Jan 2025 23:46:22 +0100 Subject: [PATCH] Visual deck storage v2 (#5427) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Restore some button states (ready/sideboard locked) to sensible defaults when unloading a deck. * Update last loaded timestamp in decklist file and then restore original last modified timestamp if a user requests a deck load. * Add some todos. * Loading a deck from local file dialog should swap out scenes, enable unload button. * Lint. * Shuffle some classes and signals around. * More sort options, sort widgets directly. * Banner cards should respect providerIds. * Properly updateSortOrder on load. * Add the color identity to the Deck Preview Widget. * Properly set sort indices. * Change replace visualDeckStorageWidget with deckView to be in deckSelectFinished so that it also works on remote deck load. * Include settings for unused color identities display. * Change opacity scaling. * Overload for Qt. * Lint. * Lint. * Include QMouseEvent * Template because MacOs. * Include a quick filter for color identities. * Include a quick filter for color identities. * Save some space. * Refactor DeckPreviewWidgets to reside in their own folder. * Add Deck Loader logging category. * Introduce a tagging system. * Add some more default tags. * Even more default tags. * Lint. * Lint a comma. * Remove extra set of braces. * Lint some stuff. * Refresh banner cards when tags are added. * Lint. * Wrestle with Qt Checkboxes. * Lint. * Adjust some sizes, relayout. * Address comments. * Lint. * Reorder kindred types. * Add a search bar for tags. * Remove close button (for now) and change "Add tags ..." to "Edit tags ..." * Retranslate window title for Deck Tag Manager Dialog. * Style tag addition widget to be consistent. * Lint. * Override paintEvent. * Override sizeHint --------- Co-authored-by: Lukas Brübach --- cockatrice/CMakeLists.txt | 10 + cockatrice/resources/config/qtlogging.ini | 3 +- .../src/client/tabs/tab_deck_editor.cpp | 45 ++-- .../src/client/tabs/tab_deck_storage.cpp | 2 +- cockatrice/src/client/tabs/tab_game.cpp | 26 +- cockatrice/src/client/tabs/tab_game.h | 2 +- .../tab_deck_storage_visual.cpp | 6 +- .../tab_deck_storage_visual.h | 4 +- .../deck_preview_card_picture_widget.cpp | 7 +- .../cards/deck_preview_card_picture_widget.h | 7 +- .../general/layout_containers/flow_widget.cpp | 5 + .../general/layout_containers/flow_widget.h | 1 + ...k_preview_color_identity_filter_widget.cpp | 201 ++++++++++++++++ ...eck_preview_color_identity_filter_widget.h | 62 +++++ .../deck_preview_color_identity_widget.cpp | 156 ++++++++++++ .../deck_preview_color_identity_widget.h | 42 ++++ .../deck_preview_deck_tags_display_widget.cpp | 31 +++ .../deck_preview_deck_tags_display_widget.h | 19 ++ .../deck_preview_tag_addition_widget.cpp | 97 ++++++++ .../deck_preview_tag_addition_widget.h | 33 +++ .../deck_preview/deck_preview_tag_dialog.cpp | 223 ++++++++++++++++++ .../deck_preview/deck_preview_tag_dialog.h | 39 +++ .../deck_preview_tag_display_widget.cpp | 110 +++++++++ .../deck_preview_tag_display_widget.h | 67 ++++++ .../deck_preview_tag_item_widget.cpp | 20 ++ .../deck_preview_tag_item_widget.h | 22 ++ .../deck_preview/deck_preview_widget.cpp | 93 ++++++++ .../deck_preview/deck_preview_widget.h | 40 ++++ .../visual_deck_storage_search_widget.cpp | 15 +- .../visual_deck_storage_search_widget.h | 2 +- .../visual_deck_storage_sort_widget.cpp | 93 ++++++++ .../visual_deck_storage_sort_widget.h | 42 ++++ .../visual_deck_storage_tag_filter_widget.cpp | 101 ++++++++ .../visual_deck_storage_tag_filter_widget.h | 23 ++ .../visual_deck_storage_widget.cpp | 132 ++++++----- .../visual_deck_storage_widget.h | 32 +-- cockatrice/src/deck/deck_loader.cpp | 68 +++++- cockatrice/src/deck/deck_loader.h | 3 +- cockatrice/src/dialogs/dlg_settings.cpp | 21 ++ cockatrice/src/dialogs/dlg_settings.h | 3 + cockatrice/src/settings/cache_settings.cpp | 18 ++ cockatrice/src/settings/cache_settings.h | 12 + common/decklist.cpp | 65 +++-- common/decklist.h | 33 ++- dbconverter/src/mocks.cpp | 8 + tests/carddatabase/mocks.cpp | 8 + 46 files changed, 1899 insertions(+), 153 deletions(-) create mode 100644 cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.cpp create mode 100644 cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.h create mode 100644 cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_widget.cpp create mode 100644 cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_widget.h create mode 100644 cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.cpp create mode 100644 cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.h create mode 100644 cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_addition_widget.cpp create mode 100644 cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_addition_widget.h create mode 100644 cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_dialog.cpp create mode 100644 cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_dialog.h create mode 100644 cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_display_widget.cpp create mode 100644 cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_display_widget.h create mode 100644 cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_item_widget.cpp create mode 100644 cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_item_widget.h create mode 100644 cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp create mode 100644 cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h create mode 100644 cockatrice/src/client/ui/widgets/visual_deck_storage/visual_deck_storage_sort_widget.cpp create mode 100644 cockatrice/src/client/ui/widgets/visual_deck_storage/visual_deck_storage_sort_widget.h create mode 100644 cockatrice/src/client/ui/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.cpp create mode 100644 cockatrice/src/client/ui/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.h diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index efdfdffb3..e65b784f8 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -162,8 +162,18 @@ set(cockatrice_SOURCES src/game/zones/view_zone.cpp src/client/tabs/visual_deck_storage/tab_deck_storage_visual.cpp src/client/ui/widgets/cards/deck_preview_card_picture_widget.cpp + src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_widget.cpp + src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.cpp + src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_addition_widget.cpp + src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_display_widget.cpp + src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_dialog.cpp + src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_item_widget.cpp + src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.cpp + src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp src/client/ui/widgets/visual_deck_storage/visual_deck_storage_widget.cpp src/client/ui/widgets/visual_deck_storage/visual_deck_storage_search_widget.cpp + src/client/ui/widgets/visual_deck_storage/visual_deck_storage_sort_widget.cpp + src/client/ui/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.cpp ${VERSION_STRING_CPP} ) diff --git a/cockatrice/resources/config/qtlogging.ini b/cockatrice/resources/config/qtlogging.ini index 325ac6536..d51809d58 100644 --- a/cockatrice/resources/config/qtlogging.ini +++ b/cockatrice/resources/config/qtlogging.ini @@ -1,2 +1,3 @@ [Rules] -picture_loader.debug = true \ No newline at end of file +picture_loader.debug = true +deck_loader.debug = true \ No newline at end of file diff --git a/cockatrice/src/client/tabs/tab_deck_editor.cpp b/cockatrice/src/client/tabs/tab_deck_editor.cpp index 60bc3de9b..1ce3083ef 100644 --- a/cockatrice/src/client/tabs/tab_deck_editor.cpp +++ b/cockatrice/src/client/tabs/tab_deck_editor.cpp @@ -823,7 +823,7 @@ void TabDeckEditor::updateBannerCardComboBox() bannerCardComboBox->clear(); // Prepare the new items with deduplication - QSet bannerCardSet; + QSet> bannerCardSet; InnerDecklistNode *listRoot = deckModel->getDeckList()->getRoot(); for (int i = 0; i < listRoot->size(); i++) { InnerDecklistNode *currentZone = dynamic_cast(listRoot->at(i)); @@ -833,23 +833,30 @@ void TabDeckEditor::updateBannerCardComboBox() continue; for (int k = 0; k < currentCard->getNumber(); ++k) { - CardInfoPtr info = CardDatabaseManager::getInstance()->getCard(currentCard->getName()); + CardInfoPtr info = CardDatabaseManager::getInstance()->getCardByNameAndProviderId( + currentCard->getName(), currentCard->getCardProviderId()); if (info) { - bannerCardSet.insert(currentCard->getName()); + bannerCardSet.insert( + QPair(currentCard->getName(), currentCard->getCardProviderId())); } } } } - // Convert the QSet to a sorted QStringList - QStringList bannerCardChoices; - for (const QString &entry : bannerCardSet) { - bannerCardChoices.append(entry); - } - bannerCardChoices.sort(Qt::CaseInsensitive); + QList> pairList = bannerCardSet.values(); - // Populate the combo box with new items - bannerCardComboBox->addItems(bannerCardChoices); + // Sort QList by the first() element of the QPair + std::sort(pairList.begin(), pairList.end(), [](const QPair &a, const QPair &b) { + return a.first.toLower() < b.first.toLower(); + }); + + for (const auto &pair : pairList) { + QVariantMap dataMap; + dataMap["name"] = pair.first; + dataMap["uuid"] = pair.second; + + bannerCardComboBox->addItem(pair.first, dataMap); + } // Try to restore the previous selection by finding the currentText int restoredIndex = bannerCardComboBox->findText(currentText); @@ -857,7 +864,7 @@ void TabDeckEditor::updateBannerCardComboBox() bannerCardComboBox->setCurrentIndex(restoredIndex); } else { // Add a placeholder "-" and set it as the current selection - int bannerIndex = bannerCardComboBox->findText(deckModel->getDeckList()->getBannerCard()); + int bannerIndex = bannerCardComboBox->findText(deckModel->getDeckList()->getBannerCard().first); if (bannerIndex != -1) { bannerCardComboBox->setCurrentIndex(bannerIndex); } else { @@ -872,8 +879,8 @@ void TabDeckEditor::updateBannerCardComboBox() void TabDeckEditor::setBannerCard(int /* changedIndex */) { - qDebug() << "Banner card was set to: " << bannerCardComboBox->currentText(); - deckModel->getDeckList()->setBannerCard(bannerCardComboBox->currentText()); + QVariantMap data = bannerCardComboBox->itemData(bannerCardComboBox->currentIndex()).toMap(); + deckModel->getDeckList()->setBannerCard(QPair(data["name"].toString(), data["uuid"].toString())); } void TabDeckEditor::updateCardInfo(CardInfoPtr _card) @@ -1043,11 +1050,11 @@ void TabDeckEditor::openDeckFromFile(const QString &fileName, DeckOpenLocation d DeckLoader::FileFormat fmt = DeckLoader::getFormatFromName(fileName); auto *l = new DeckLoader; - if (l->loadFromFile(fileName, fmt)) { + if (l->loadFromFile(fileName, fmt, true)) { SettingsCache::instance().recents().updateRecentlyOpenedDeckPaths(fileName); updateBannerCardComboBox(); - if (!l->getBannerCard().isEmpty()) { - bannerCardComboBox->setCurrentIndex(bannerCardComboBox->findText(l->getBannerCard())); + if (!l->getBannerCard().first.isEmpty()) { + bannerCardComboBox->setCurrentIndex(bannerCardComboBox->findText(l->getBannerCard().first)); } if (deckOpenLocation == NEW_TAB) { emit openDeckEditor(l); @@ -1542,13 +1549,13 @@ void TabDeckEditor::actDecrement() void TabDeckEditor::setDeck(DeckLoader *_deck) { - qDebug() << " ORIGINAL BANNER CARD " << _deck->getBannerCard(); + qDebug() << " ORIGINAL BANNER CARD " << _deck->getBannerCard().first; deckModel->setDeckList(_deck); nameEdit->setText(deckModel->getDeckList()->getName()); commentsEdit->setText(deckModel->getDeckList()->getComments()); qDebug() << deckModel->getDeckList()->getBannerCard() << " was the banner card"; - bannerCardComboBox->setCurrentText(deckModel->getDeckList()->getBannerCard()); + bannerCardComboBox->setCurrentText(deckModel->getDeckList()->getBannerCard().first); updateBannerCardComboBox(); updateHash(); deckModel->sort(deckView->header()->sortIndicatorSection(), deckView->header()->sortIndicatorOrder()); diff --git a/cockatrice/src/client/tabs/tab_deck_storage.cpp b/cockatrice/src/client/tabs/tab_deck_storage.cpp index 718eedb2a..4435e5a22 100644 --- a/cockatrice/src/client/tabs/tab_deck_storage.cpp +++ b/cockatrice/src/client/tabs/tab_deck_storage.cpp @@ -203,7 +203,7 @@ void TabDeckStorage::actOpenLocalDeck() QString filePath = localDirModel->filePath(curLeft); DeckLoader deckLoader; - if (!deckLoader.loadFromFile(filePath, DeckLoader::CockatriceFormat)) + if (!deckLoader.loadFromFile(filePath, DeckLoader::CockatriceFormat, true)) continue; SettingsCache::instance().recents().updateRecentlyOpenedDeckPaths(filePath); diff --git a/cockatrice/src/client/tabs/tab_game.cpp b/cockatrice/src/client/tabs/tab_game.cpp index 63e80d6f9..418ec8e71 100644 --- a/cockatrice/src/client/tabs/tab_game.cpp +++ b/cockatrice/src/client/tabs/tab_game.cpp @@ -145,7 +145,7 @@ DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent) deckView->setVisible(false); visualDeckStorageWidget = new VisualDeckStorageWidget(this); - connect(visualDeckStorageWidget, &VisualDeckStorageWidget::imageDoubleClicked, this, + connect(visualDeckStorageWidget, &VisualDeckStorageWidget::deckPreviewDoubleClicked, this, &DeckViewContainer::replaceDeckStorageWithDeckView); deckViewLayout = new QVBoxLayout; @@ -299,20 +299,12 @@ void TabGame::refreshShortcuts() } } -void DeckViewContainer::replaceDeckStorageWithDeckView(QMouseEvent *event, DeckPreviewCardPictureWidget *instance) +void DeckViewContainer::replaceDeckStorageWithDeckView(QMouseEvent *event, DeckPreviewWidget *instance) { Q_UNUSED(event); - QString fileName = instance->filePath; - DeckLoader::FileFormat fmt = DeckLoader::getFormatFromName(fileName); - QString deckString; - DeckLoader deck; + QString deckString = instance->deckLoader->writeToString_Native(); - bool error = !deck.loadFromFile(fileName, fmt); - if (!error) { - deckString = deck.writeToString_Native(); - error = deckString.length() > MAX_FILE_LENGTH; - } - if (error) { + if (deckString.length() > MAX_FILE_LENGTH) { QMessageBox::critical(this, tr("Error"), tr("The selected file could not be loaded.")); return; } @@ -335,6 +327,11 @@ void DeckViewContainer::unloadDeck() visualDeckStorageWidget->setVisible(true); deckViewLayout->update(); unloadDeckButton->setEnabled(false); + readyStartButton->setEnabled(false); + readyStartButton->setState(false); + sideboardLockButton->setEnabled(false); + sideboardLockButton->setState(false); + setReadyStart(false); } void DeckViewContainer::loadLocalDeck() @@ -352,7 +349,7 @@ void DeckViewContainer::loadDeckFromFile(const QString &filePath) QString deckString; DeckLoader deck; - bool error = !deck.loadFromFile(filePath, fmt); + bool error = !deck.loadFromFile(filePath, fmt, true); if (!error) { deckString = deck.writeToString_Native(); error = deckString.length() > MAX_FILE_LENGTH; @@ -390,6 +387,9 @@ void DeckViewContainer::deckSelectFinished(const Response &r) // TODO CHANGE THIS TO BE SELECTED BY UUID PictureLoader::cacheCardPixmaps(CardDatabaseManager::getInstance()->getCards(newDeck.getCardList())); setDeck(newDeck); + deckView->setVisible(true); + visualDeckStorageWidget->setVisible(false); + unloadDeckButton->setEnabled(true); } void DeckViewContainer::readyStart() diff --git a/cockatrice/src/client/tabs/tab_game.h b/cockatrice/src/client/tabs/tab_game.h index ec3366dda..0d81a4605 100644 --- a/cockatrice/src/client/tabs/tab_game.h +++ b/cockatrice/src/client/tabs/tab_game.h @@ -95,7 +95,7 @@ private: TabGame *parentGame; int playerId; private slots: - void replaceDeckStorageWithDeckView(QMouseEvent *event, DeckPreviewCardPictureWidget *instance); + void replaceDeckStorageWithDeckView(QMouseEvent *event, DeckPreviewWidget *instance); void loadLocalDeck(); void loadRemoteDeck(); void unloadDeck(); diff --git a/cockatrice/src/client/tabs/visual_deck_storage/tab_deck_storage_visual.cpp b/cockatrice/src/client/tabs/visual_deck_storage/tab_deck_storage_visual.cpp index 7858e4e45..2cd3f9794 100644 --- a/cockatrice/src/client/tabs/visual_deck_storage/tab_deck_storage_visual.cpp +++ b/cockatrice/src/client/tabs/visual_deck_storage/tab_deck_storage_visual.cpp @@ -49,7 +49,7 @@ TabDeckStorageVisual::TabDeckStorageVisual(TabSupervisor *_tabSupervisor, Abstra leftToolBar->addAction(aDeleteLocalDeck); visualDeckStorageWidget = new VisualDeckStorageWidget(this); - connect(visualDeckStorageWidget, &VisualDeckStorageWidget::imageDoubleClicked, this, + connect(visualDeckStorageWidget, &VisualDeckStorageWidget::deckPreviewDoubleClicked, this, &TabDeckStorageVisual::actOpenLocalDeck); // layout->addWidget(leftToolBar); @@ -74,11 +74,11 @@ QString TabDeckStorageVisual::getTargetPath() const return {}; } -void TabDeckStorageVisual::actOpenLocalDeck(QMouseEvent *event, DeckPreviewCardPictureWidget *instance) +void TabDeckStorageVisual::actOpenLocalDeck(QMouseEvent *event, DeckPreviewWidget *instance) { (void)event; DeckLoader deckLoader; - if (!deckLoader.loadFromFile(instance->filePath, DeckLoader::CockatriceFormat)) + if (!deckLoader.loadFromFile(instance->filePath, DeckLoader::CockatriceFormat, true)) return; emit openDeckEditor(&deckLoader); diff --git a/cockatrice/src/client/tabs/visual_deck_storage/tab_deck_storage_visual.h b/cockatrice/src/client/tabs/visual_deck_storage/tab_deck_storage_visual.h index 689061be9..93778b86a 100644 --- a/cockatrice/src/client/tabs/visual_deck_storage/tab_deck_storage_visual.h +++ b/cockatrice/src/client/tabs/visual_deck_storage/tab_deck_storage_visual.h @@ -33,8 +33,8 @@ public: } public slots: void cardUpdateFinished(int exitCode, QProcess::ExitStatus exitStatus); - void closeRequest(bool forced = false) override; - void actOpenLocalDeck(QMouseEvent *event, DeckPreviewCardPictureWidget *instance); + void closeRequest(bool forced) override; + void actOpenLocalDeck(QMouseEvent *event, DeckPreviewWidget *instance); void actDeleteLocalDeck(); signals: void openDeckEditor(const DeckLoader *deckLoader); diff --git a/cockatrice/src/client/ui/widgets/cards/deck_preview_card_picture_widget.cpp b/cockatrice/src/client/ui/widgets/cards/deck_preview_card_picture_widget.cpp index eff64e83d..85a8200c8 100644 --- a/cockatrice/src/client/ui/widgets/cards/deck_preview_card_picture_widget.cpp +++ b/cockatrice/src/client/ui/widgets/cards/deck_preview_card_picture_widget.cpp @@ -1,6 +1,7 @@ #include "deck_preview_card_picture_widget.h" #include +#include #include #include #include @@ -15,6 +16,7 @@ * @param outlineColor The color of the outline around the text. * @param fontSize The font size of the overlay text. * @param alignment The alignment of the text within the overlay. + * @param _deckLoader The Deck Loader holding the Deck associated with this preview. * * Sets the widget's size policy and default border style. */ @@ -46,8 +48,3 @@ void DeckPreviewCardPictureWidget::mouseDoubleClickEvent(QMouseEvent *event) emit imageDoubleClicked(lastMouseEvent, this); } } - -void DeckPreviewCardPictureWidget::setFilePath(const QString &_filePath) -{ - filePath = _filePath; -} diff --git a/cockatrice/src/client/ui/widgets/cards/deck_preview_card_picture_widget.h b/cockatrice/src/client/ui/widgets/cards/deck_preview_card_picture_widget.h index 25d9b16e9..3fafc73e0 100644 --- a/cockatrice/src/client/ui/widgets/cards/deck_preview_card_picture_widget.h +++ b/cockatrice/src/client/ui/widgets/cards/deck_preview_card_picture_widget.h @@ -12,22 +12,17 @@ class DeckPreviewCardPictureWidget final : public CardInfoPictureWithTextOverlay Q_OBJECT public: - explicit DeckPreviewCardPictureWidget(QWidget *parent = nullptr, + explicit DeckPreviewCardPictureWidget(QWidget *parent, bool hoverToZoomEnabled = false, const QColor &textColor = Qt::white, const QColor &outlineColor = Qt::black, int fontSize = 12, Qt::Alignment alignment = Qt::AlignCenter); - QString filePath; - signals: void imageClicked(QMouseEvent *event, DeckPreviewCardPictureWidget *instance); void imageDoubleClicked(QMouseEvent *event, DeckPreviewCardPictureWidget *instance); -public slots: - void setFilePath(const QString &filePath); - private: QTimer *singleClickTimer; QMouseEvent *lastMouseEvent = nullptr; // Store the last mouse event 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 9475cd598..958cc98be 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 @@ -87,6 +87,11 @@ void FlowWidget::addWidget(QWidget *widget_to_add) const flowLayout->addWidget(widget_to_add); } +void FlowWidget::removeWidget(QWidget *widgetToRemove) const +{ + flowLayout->removeWidget(widgetToRemove); +} + /** * @brief Clears all widgets from the flow layout. * 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 66a6566c3..a514b56a7 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 @@ -13,6 +13,7 @@ class FlowWidget final : public QWidget public: FlowWidget(QWidget *parent, Qt::ScrollBarPolicy horizontalPolicy, Qt::ScrollBarPolicy verticalPolicy); void addWidget(QWidget *widget_to_add) const; + void removeWidget(QWidget *widgetToRemove) const; void clearLayout(); [[nodiscard]] int count() const; [[nodiscard]] QLayoutItem *itemAt(int index) const; diff --git a/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.cpp b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.cpp new file mode 100644 index 000000000..9b92f8994 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.cpp @@ -0,0 +1,201 @@ +#include "deck_preview_color_identity_filter_widget.h" + +#include "deck_preview_widget.h" + +#include +#include + +DeckPreviewColorIdentityFilterCircleWidget::DeckPreviewColorIdentityFilterCircleWidget(QChar color, QWidget *parent) + : QWidget(parent), colorChar(color), isActive(false), circleDiameter(30) +{ + setFixedSize(circleDiameter, circleDiameter); +} + +void DeckPreviewColorIdentityFilterCircleWidget::setColorActive(bool active) +{ + if (isActive != active) { + isActive = active; + update(); + } +} + +bool DeckPreviewColorIdentityFilterCircleWidget::isColorActive() const +{ + return isActive; +} + +QChar DeckPreviewColorIdentityFilterCircleWidget::getColorChar() const +{ + return colorChar; +} + +void DeckPreviewColorIdentityFilterCircleWidget::paintEvent(QPaintEvent *event) +{ + Q_UNUSED(event); + + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + + QColor circleColor; + switch (colorChar.unicode()) { + case 'W': + circleColor = Qt::white; + break; + case 'U': + circleColor = QColor(0, 115, 230); + break; + case 'B': + circleColor = QColor(50, 50, 50); + break; + case 'R': + circleColor = QColor(230, 30, 30); + break; + case 'G': + circleColor = QColor(30, 180, 30); + break; + default: + circleColor = Qt::transparent; + break; + } + + if (!isActive) { + circleColor.setAlpha(100); // Dim inactive circles + } + + painter.setBrush(circleColor); + painter.setPen(Qt::black); + painter.drawEllipse(rect()); + + if (isActive) { + QFont font = painter.font(); + font.setBold(true); + font.setPointSize(circleDiameter / 3); + painter.setFont(font); + painter.setPen(colorChar.unicode() == 'B' ? Qt::white : Qt::black); + painter.drawText(rect(), Qt::AlignCenter, colorChar); + } +} + +void DeckPreviewColorIdentityFilterCircleWidget::mousePressEvent(QMouseEvent *event) +{ + Q_UNUSED(event); + isActive = !isActive; + emit colorToggled(colorChar, isActive); + update(); +} + +DeckPreviewColorIdentityFilterWidget::DeckPreviewColorIdentityFilterWidget(VisualDeckStorageWidget *parent) + : QWidget(parent), layout(new QHBoxLayout(this)) +{ + setLayout(layout); + layout->setSpacing(5); + layout->setContentsMargins(0, 0, 0, 0); + + QString fullColorIdentity = "WUBRG"; + for (const QChar &color : fullColorIdentity) { + auto *circle = new DeckPreviewColorIdentityFilterCircleWidget(color, this); + + layout->addWidget(circle); + + // Initialize the activeColors map + activeColors[color] = false; + + // Connect the color toggled signal + connect(circle, &DeckPreviewColorIdentityFilterCircleWidget::colorToggled, this, + &DeckPreviewColorIdentityFilterWidget::handleColorToggled); + } + + toggleButton = new QPushButton(this); + toggleButton->setCheckable(true); + layout->addWidget(toggleButton); + + // Connect the button's toggled signal + connect(toggleButton, &QPushButton::toggled, this, &DeckPreviewColorIdentityFilterWidget::updateFilterMode); + connect(this, &DeckPreviewColorIdentityFilterWidget::activeColorsChanged, parent, + &VisualDeckStorageWidget::refreshBannerCards); + connect(this, &DeckPreviewColorIdentityFilterWidget::filterModeChanged, parent, + &VisualDeckStorageWidget::refreshBannerCards); + + // Call retranslateUi to set the initial text + retranslateUi(); +} + +void DeckPreviewColorIdentityFilterWidget::retranslateUi() +{ + // Set the toggle button text based on the current mode + toggleButton->setText(exactMatchMode ? tr("Mode: Exact Match") : tr("Mode: Includes")); +} + +void DeckPreviewColorIdentityFilterWidget::handleColorToggled(QChar color, bool active) +{ + activeColors[color] = active; + emit activeColorsChanged(); +} + +void DeckPreviewColorIdentityFilterWidget::updateFilterMode(bool checked) +{ + exactMatchMode = checked; // Toggle between modes + retranslateUi(); // Update the button text + emit filterModeChanged(exactMatchMode); +} + +QList DeckPreviewColorIdentityFilterWidget::filterWidgets(QList &widgets) +{ + QList filteredWidgets; + + // Check if no colors are active + bool noColorsActive = true; + for (auto it = activeColors.constBegin(); it != activeColors.constEnd(); ++it) { + if (it.value()) { + noColorsActive = false; + break; + } + } + + // If no colors are active, return the unfiltered list of widgets + if (noColorsActive) { + return widgets; + } + + for (const auto &widget : widgets) { + QString colorIdentity = widget->getColorIdentity(); + + bool matchesFilter = true; + if (exactMatchMode) { + // Exact match mode: active colors must exactly match colorIdentity + + // Create a set of active colors + QSet activeColorSet; + for (auto it = activeColors.constBegin(); it != activeColors.constEnd(); ++it) { + if (it.value()) { + activeColorSet.insert(it.key().toUpper()); // Use uppercase for uniformity + } + } + + // Create a set of colors from the color identity string + QSet colorIdentitySet; + for (const QChar &color : colorIdentity) { + colorIdentitySet.insert(color.toUpper()); // Ensure case uniformity + } + + // Compare the sets: the sets must match exactly + if (activeColorSet != colorIdentitySet) { + matchesFilter = false; + } + } else { + // Includes mode: colorIdentity must contain all active colors + for (auto it = activeColors.constBegin(); it != activeColors.constEnd(); ++it) { + if (it.value() && !colorIdentity.contains(it.key())) { + matchesFilter = false; + break; + } + } + } + + if (matchesFilter) { + filteredWidgets << widget; + } + } + + return filteredWidgets; +} diff --git a/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.h b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.h new file mode 100644 index 000000000..e18bf8860 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.h @@ -0,0 +1,62 @@ +#ifndef DECK_PREVIEW_COLOR_IDENTITY_FILTER_WIDGET_H +#define DECK_PREVIEW_COLOR_IDENTITY_FILTER_WIDGET_H + +#include "../visual_deck_storage_widget.h" + +#include +#include +#include +#include +#include + +class DeckPreviewWidget; +class VisualDeckStorageWidget; + +class DeckPreviewColorIdentityFilterCircleWidget : public QWidget +{ + Q_OBJECT + +public: + explicit DeckPreviewColorIdentityFilterCircleWidget(QChar color, QWidget *parent = nullptr); + void setColorActive(bool active); + bool isColorActive() const; + QChar getColorChar() const; + +protected: + void paintEvent(QPaintEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; + +signals: + void colorToggled(QChar color, bool active); + +private: + QChar colorChar; + bool isActive; + int circleDiameter; +}; + +class DeckPreviewColorIdentityFilterWidget : public QWidget +{ + Q_OBJECT + +public: + explicit DeckPreviewColorIdentityFilterWidget(VisualDeckStorageWidget *parent); + void retranslateUi(); + QList filterWidgets(QList &widgets); + +signals: + void filterModeChanged(bool exactMatchMode); + void activeColorsChanged(); + +private slots: + void handleColorToggled(QChar color, bool active); + void updateFilterMode(bool checked); + +private: + QHBoxLayout *layout; + QPushButton *toggleButton; + QMap activeColors; + bool exactMatchMode = false; // Default to "includes" mode +}; + +#endif // DECK_PREVIEW_COLOR_IDENTITY_FILTER_WIDGET_H diff --git a/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_widget.cpp b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_widget.cpp new file mode 100644 index 000000000..2121f2a88 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_widget.cpp @@ -0,0 +1,156 @@ +#include "deck_preview_color_identity_widget.h" + +#include "../../../../../settings/cache_settings.h" + +#include +#include + +DeckPreviewColorCircleWidget::DeckPreviewColorCircleWidget(QChar color, QWidget *parent) + : QWidget(parent), colorChar(color), circleDiameter(0), isActive(false) +{ +} + +void DeckPreviewColorCircleWidget::resizeEvent(QResizeEvent *event) +{ + Q_UNUSED(event); + + // Get the parent of the DeckPreviewColorIdentityWidget + QWidget *identityParent = parentWidget() ? parentWidget()->parentWidget() : nullptr; + if (identityParent) { + // Calculate the circle diameter as 15% of the parent's height + int maxSize = identityParent->width() * 0.15; + circleDiameter = maxSize; + + // Update the widget size based on the diameter + updateGeometry(); // Request a resize based on sizeHint() + } + + update(); // Trigger a repaint +} + +QSize DeckPreviewColorCircleWidget::sizeHint() const +{ + // Return the size we calculated based on the parent widget + return QSize(circleDiameter, circleDiameter); +} + +QSize DeckPreviewColorCircleWidget::minimumSizeHint() const +{ + // Return the same value as sizeHint() for minimum size + return QSize(circleDiameter, circleDiameter); +} + +void DeckPreviewColorCircleWidget::paintEvent(QPaintEvent *event) +{ + Q_UNUSED(event); + + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + + // Calculate the circle's bounding rectangle + int x = (width() - circleDiameter) / 2; + int y = (height() - circleDiameter) / 2; + QRect circleRect(x, y, circleDiameter, circleDiameter); + + // Map color characters to their respective colors + QColor circleColor; + switch (colorChar.unicode()) { + case 'W': + circleColor = Qt::white; + break; + case 'U': + circleColor = QColor(0, 115, 230); + break; // Blue + case 'B': + circleColor = QColor(50, 50, 50); + break; // Black + case 'R': + circleColor = QColor(230, 30, 30); + break; // Red + case 'G': + circleColor = QColor(30, 180, 30); + break; // Green + default: + circleColor = Qt::transparent; + break; // Fallback + } + + if (SettingsCache::instance().getVisualDeckStorageDrawUnusedColorIdentities() || isActive) { + // Make the circle faint if it is not active + if (!isActive) { + circleColor.setAlpha(SettingsCache::instance().getVisualDeckStorageUnusedColorIdentitiesOpacity() / 100.0 * + 255.0); + } + + // Draw the circle + painter.setBrush(circleColor); + painter.setPen(Qt::black); + painter.drawEllipse(circleRect); + } + + // Draw the color character only if the circle is active + if (isActive) { + QFont font = painter.font(); + font.setBold(true); + font.setPointSize(circleDiameter * 0.4); // Adjust font size relative to circle diameter + painter.setFont(font); + if (colorChar.unicode() == 'B') { + painter.setPen(Qt::white); + } else { + painter.setPen(Qt::black); + } + + // Center the text within the circle + painter.drawText(circleRect, Qt::AlignCenter, colorChar); + } +} + +void DeckPreviewColorCircleWidget::setColorActive(bool active) +{ + isActive = active; + update(); // Redraw the circle with the new active state +} + +QChar DeckPreviewColorCircleWidget::getColorChar() const +{ + return colorChar; +} + +DeckPreviewColorIdentityWidget::DeckPreviewColorIdentityWidget(const QString &colorIdentity, QWidget *parent) + : QWidget(parent) +{ + QHBoxLayout *layout = new QHBoxLayout(this); + layout->setSpacing(5); + layout->setContentsMargins(0, 0, 0, 0); + setLayout(layout); + + setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + + // Define the full WUBRG set (White, Blue, Black, Red, Green) + QString fullColorIdentity = "WUBRG"; + + // Create and add a DeckPreviewColorCircleWidget for each color in WUBRG + for (const QChar &color : fullColorIdentity) { + auto *circle = new DeckPreviewColorCircleWidget(color, this); + layout->addWidget(circle); + } + + // Set any active colors from the input colorIdentity + for (const QChar &color : colorIdentity) { + for (DeckPreviewColorCircleWidget *circle : findChildren()) { + if (circle->getColorChar() == color) { + circle->setColorActive(true); // Mark the color as active + } + } + } +} + +void DeckPreviewColorIdentityWidget::resizeEvent(QResizeEvent *event) +{ + QWidget::resizeEvent(event); + + // Notify child widgets to update their sizes based on the new parent size + for (auto *circle : findChildren()) { + circle->updateGeometry(); // Request each circle to resize + } +} diff --git a/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_widget.h b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_widget.h new file mode 100644 index 000000000..0dd40df10 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_widget.h @@ -0,0 +1,42 @@ +#ifndef DECK_PREVIEW_COLOR_IDENTITY_WIDGET_H +#define DECK_PREVIEW_COLOR_IDENTITY_WIDGET_H + +#include +#include +#include +#include + +class DeckPreviewColorCircleWidget : public QWidget +{ + Q_OBJECT + +public: + explicit DeckPreviewColorCircleWidget(QChar color, QWidget *parent = nullptr); + + void setColorActive(bool active); + QChar getColorChar() const; + +protected: + void resizeEvent(QResizeEvent *event) override; + void paintEvent(QPaintEvent *event) override; + QSize sizeHint() const override; + QSize minimumSizeHint() const override; + +private: + QChar colorChar; + int circleDiameter; + bool isActive; +}; + +class DeckPreviewColorIdentityWidget : public QWidget +{ + Q_OBJECT + +public: + explicit DeckPreviewColorIdentityWidget(const QString &colorIdentity, QWidget *parent = nullptr); + +protected: + void resizeEvent(QResizeEvent *event) override; +}; + +#endif // DECK_PREVIEW_COLOR_IDENTITY_WIDGET_H 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 new file mode 100644 index 000000000..e849d2716 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.cpp @@ -0,0 +1,31 @@ +#include "deck_preview_deck_tags_display_widget.h" + +#include "../../general/layout_containers/flow_widget.h" +#include "deck_preview_tag_addition_widget.h" +#include "deck_preview_tag_display_widget.h" +#include "deck_preview_widget.h" + +#include +#include + +DeckPreviewDeckTagsDisplayWidget::DeckPreviewDeckTagsDisplayWidget(DeckPreviewWidget *_parent, DeckLoader *_deckLoader) + : QWidget(_parent), parent(_parent), deckLoader(_deckLoader) +{ + + setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + + // Create layout + auto *layout = new QHBoxLayout(this); + layout->setContentsMargins(5, 5, 5, 5); + layout->setSpacing(5); + + setFixedHeight(100); + + auto *flowWidget = new FlowWidget(this, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded); + + for (const QString &tag : this->deckLoader->getTags()) { + flowWidget->addWidget(new DeckPreviewTagDisplayWidget(this, tag)); + } + flowWidget->addWidget(new DeckPreviewTagAdditionWidget(this, tr("Edit tags ..."))); + layout->addWidget(flowWidget); +} \ No newline at end of file diff --git a/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.h b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.h new file mode 100644 index 000000000..4f62f764c --- /dev/null +++ b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.h @@ -0,0 +1,19 @@ +#ifndef DECK_PREVIEW_DECK_TAGS_DISPLAY_WIDGET_H +#define DECK_PREVIEW_DECK_TAGS_DISPLAY_WIDGET_H + +#include "../../../../../deck/deck_loader.h" +#include "deck_preview_widget.h" + +#include + +class DeckPreviewWidget; +class DeckPreviewDeckTagsDisplayWidget : public QWidget +{ + Q_OBJECT + +public: + explicit DeckPreviewDeckTagsDisplayWidget(DeckPreviewWidget *_parent, DeckLoader *_deckLoader); + DeckPreviewWidget *parent; + DeckLoader *deckLoader; +}; +#endif // DECK_PREVIEW_DECK_TAGS_DISPLAY_WIDGET_H 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 new file mode 100644 index 000000000..77eb4357e --- /dev/null +++ b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_addition_widget.cpp @@ -0,0 +1,97 @@ +#include "deck_preview_tag_addition_widget.h" + +#include "deck_preview_tag_dialog.h" + +#include +#include +#include +#include + +DeckPreviewTagAdditionWidget::DeckPreviewTagAdditionWidget(DeckPreviewDeckTagsDisplayWidget *_parent, + const QString &_tagName) + : QWidget(_parent), parent(_parent), tagName_(_tagName) +{ + // Create layout + auto *layout = new QHBoxLayout(this); + layout->setContentsMargins(5, 5, 5, 5); + layout->setSpacing(5); + + // Adjust widget size + setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); +} + +QSize DeckPreviewTagAdditionWidget::sizeHint() const +{ + // Calculate the size based on the tag name + QFontMetrics fm(font()); + int textWidth = fm.horizontalAdvance(tagName_); + int width = textWidth + 50; // Add extra padding + int height = fm.height() + 10; // Height based on font size + padding + + return QSize(width, height); +} + +void DeckPreviewTagAdditionWidget::mousePressEvent(QMouseEvent *event) +{ + if (event->button() == Qt::LeftButton) { + emit tagClicked(); + } + QWidget::mousePressEvent(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); + parent->parent->parent->refreshBannerCards(); + } +} + +void DeckPreviewTagAdditionWidget::paintEvent(QPaintEvent *event) +{ + QPainter painter(this); + + // Set background color + QColor backgroundColor = Qt::lightGray; + painter.setBrush(backgroundColor); + painter.setPen(Qt::NoPen); + + // Draw background + painter.drawRect(rect()); + + // Draw border + QColor borderColor = Qt::gray; + QPen borderPen(borderColor, 1); + painter.setPen(borderPen); + painter.drawRect(rect().adjusted(0, 0, -1, -1)); // Adjust for pen width + + // Calculate font size based on widget height + QFont font = painter.font(); + int fontSize = std::max(10, height() / 2); // Ensure a minimum font size of 10 + font.setPointSize(fontSize); + painter.setFont(font); + + // Calculate text rect with margin + int margin = 10; // Left and right margins + QRect textRect(margin, 0, width() - margin * 2, height()); + + // Draw the text with a black border for better legibility + painter.setPen(Qt::black); + + // Draw text border by offsetting + for (int dx = -1; dx <= 1; ++dx) { + for (int dy = -1; dy <= 1; ++dy) { + if (dx != 0 || dy != 0) { + painter.drawText(textRect.translated(dx, dy), Qt::AlignLeft | Qt::AlignVCenter, tagName_); + } + } + } + + // Draw the actual text + painter.setPen(Qt::white); + painter.drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, tagName_); + + QWidget::paintEvent(event); +} diff --git a/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_addition_widget.h b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_addition_widget.h new file mode 100644 index 000000000..14d51a556 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_addition_widget.h @@ -0,0 +1,33 @@ +#ifndef DECK_PREVIEW_TAG_ADDITION_WIDGET_H +#define DECK_PREVIEW_TAG_ADDITION_WIDGET_H + +#include "deck_preview_deck_tags_display_widget.h" + +#include +#include +#include + +class DeckPreviewTagAdditionWidget : public QWidget +{ + Q_OBJECT + +public: + explicit DeckPreviewTagAdditionWidget(DeckPreviewDeckTagsDisplayWidget *_parent, const QString &tagName); + QSize sizeHint() const override; + +signals: + void tagClicked(); // Emitted when the tag is clicked + void tagClosed(); // Emitted when the close button is clicked + +protected: + void mousePressEvent(QMouseEvent *event) override; + void paintEvent(QPaintEvent *event) override; + +private: + DeckPreviewDeckTagsDisplayWidget *parent; + QString tagName_; + QLabel *tagLabel_; + QPushButton *closeButton_; +}; + +#endif // DECK_PREVIEW_TAG_ADDITION_WIDGET_H diff --git a/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_dialog.cpp b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_dialog.cpp new file mode 100644 index 000000000..815545aab --- /dev/null +++ b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_dialog.cpp @@ -0,0 +1,223 @@ +#include "deck_preview_tag_dialog.h" + +#include "deck_preview_tag_item_widget.h" + +#include +#include +#include +#include +#include +#include +#include + +DeckPreviewTagDialog::DeckPreviewTagDialog(const QStringList &knownTags, const QStringList &activeTags, QWidget *parent) + : QDialog(parent), activeTags_(activeTags) +{ + resize(400, 500); + + QStringList defaultTags = { + // Strategies + "🏃️ Aggro", + "🧙‍️ Control", + "⚔️ Midrange", + "🌀 Combo", + "🪓 Mill", + "🔒 Stax", + "🗺️ Landfall", + "🛡️ Pillowfort", + "🌱 Ramp", + "⚡ Storm", + "💀 Aristocrats", + "☠️ Reanimator", + "👹 Sacrifice", + "🔥 Burn", + "🌟 Lifegain", + "🔮 Spellslinger", + "👥 Tokens", + "🎭 Blink", + "⏳ Time Manipulation", + "🌍 Domain", + "💫 Proliferate", + "📜 Saga", + "🎲 Chaos", + "🪄 Auras", + "🔫 Pingers", + + // Themes + "👑 Monarch", + "🚀 Vehicles", + "💉 Infect", + "🩸 Madness", + "🌀 Morph", + + // Card Types + "⚔️ Creature", + "💎 Artifact", + "🌔 Enchantment", + "📖 Sorcery", + "⚡ Instant", + "🌌 Planeswalker", + "🌏 Land", + "🪄 Aura", + + // Kindred Types + "🐉 Kindred", + "🧙 Humans", + "⚔️ Soldiers", + "🛡️ Knights", + "🎻 Bards", + "🧝 Elves", + "🌲 Dryads", + "😇 Angels", + "🎩 Wizards", + "🧛 Vampires", + "🦴 Skeletons", + "💀 Zombies", + "👹 Demons", + "👾 Eldrazi", + "🐉 Dragons", + "🐠 Merfolk", + "🦁 Cats", + "🐺 Wolves", + "🐺 Werewolves", + "🦇 Bats", + "🐀 Rats", + "🦅 Birds", + "🦗 Insects", + "🍄 Fungus", + "🐚 Sea Creatures", + "🐗 Boars", + "🦊 Foxes", + "🦄 Unicorns", + "🐘 Elephants", + "🐻 Bears", + "🦏 Rhinos", + "🦂 Scorpions", + }; + + // Merge knownTags with defaultTags, ensuring no duplicates + QStringList combinedTags = defaultTags + knownTags + activeTags; + combinedTags.removeDuplicates(); + + // Main layout + auto *mainLayout = new QVBoxLayout(this); + + // Filter bar + filterInput_ = new QLineEdit(this); + mainLayout->addWidget(filterInput_); + + connect(filterInput_, &QLineEdit::textChanged, this, &DeckPreviewTagDialog::filterTags); + + // Instruction label + instructionLabel = new QLabel(this); + instructionLabel->setWordWrap(true); + mainLayout->addWidget(instructionLabel); + + // Tag list view + tagListView_ = new QListWidget(this); + mainLayout->addWidget(tagListView_); + + // Populate combined tags + for (const auto &tag : combinedTags) { + auto *item = new QListWidgetItem(tagListView_); + auto *tagWidget = new DeckPreviewTagItemWidget(tag, activeTags.contains(tag), this); + tagListView_->addItem(item); + tagListView_->setItemWidget(item, tagWidget); + + connect(tagWidget->checkBox(), &QCheckBox::toggled, this, &DeckPreviewTagDialog::onCheckboxStateChanged); + } + + // Add tag input layout + auto *addTagLayout = new QHBoxLayout(); + newTagInput_ = new QLineEdit(this); + addTagButton_ = new QPushButton(this); + addTagLayout->addWidget(newTagInput_); + addTagLayout->addWidget(addTagButton_); + mainLayout->addLayout(addTagLayout); + + connect(addTagButton_, &QPushButton::clicked, this, &DeckPreviewTagDialog::addTag); + + // OK and Cancel buttons + auto *buttonLayout = new QHBoxLayout(); + okButton = new QPushButton(this); + cancelButton = new QPushButton(this); + buttonLayout->addStretch(); + buttonLayout->addWidget(okButton); + buttonLayout->addWidget(cancelButton); + mainLayout->addLayout(buttonLayout); + + connect(okButton, &QPushButton::clicked, this, &DeckPreviewTagDialog::accept); + connect(cancelButton, &QPushButton::clicked, this, &DeckPreviewTagDialog::reject); + retranslateUi(); +} + +void DeckPreviewTagDialog::retranslateUi() +{ + setWindowTitle(tr("Deck Tags Manager")); + instructionLabel->setText(tr("Manage your deck tags. Check or uncheck tags as needed, or add new ones:")); + newTagInput_->setPlaceholderText(tr("Add a new tag (e.g., Aggro️)")); + addTagButton_->setText(tr("Add Tag")); + filterInput_->setPlaceholderText(tr("Filter tags...")); + okButton->setText(tr("OK")); + cancelButton->setText(tr("Cancel")); +} + +QStringList DeckPreviewTagDialog::getActiveTags() const +{ + return activeTags_; +} + +void DeckPreviewTagDialog::addTag() +{ + QString newTag = newTagInput_->text().trimmed(); + if (newTag.isEmpty()) { + QMessageBox::warning(this, tr("Invalid Input"), tr("Tag name cannot be empty!")); + return; + } + + // Prevent duplicate tags + for (int i = 0; i < tagListView_->count(); ++i) { + auto *item = tagListView_->item(i); + auto *tagWidget = qobject_cast(tagListView_->itemWidget(item)); + if (tagWidget && tagWidget->checkBox()->text() == newTag) { + QMessageBox::warning(this, tr("Duplicate Tag"), tr("This tag already exists.")); + return; + } + } + + // Add the new tag + auto *item = new QListWidgetItem(tagListView_); + auto *tagWidget = new DeckPreviewTagItemWidget(newTag, true, this); + tagListView_->addItem(item); + tagListView_->setItemWidget(item, tagWidget); + activeTags_.append(newTag); + + connect(tagWidget->checkBox(), &QCheckBox::toggled, this, &DeckPreviewTagDialog::onCheckboxStateChanged); + + // Clear the input field + newTagInput_->clear(); +} + +void DeckPreviewTagDialog::filterTags(const QString &text) +{ + for (int i = 0; i < tagListView_->count(); ++i) { + auto *item = tagListView_->item(i); + auto *tagWidget = qobject_cast(tagListView_->itemWidget(item)); + if (tagWidget) { + bool matches = tagWidget->checkBox()->text().contains(text, Qt::CaseInsensitive); + item->setHidden(!matches); + } + } +} + +void DeckPreviewTagDialog::onCheckboxStateChanged() +{ + activeTags_.clear(); + for (int i = 0; i < tagListView_->count(); ++i) { + auto *item = tagListView_->item(i); + auto *tagWidget = qobject_cast(tagListView_->itemWidget(item)); + if (tagWidget && tagWidget->checkBox()->isChecked()) { + activeTags_.append(tagWidget->checkBox()->text()); + } + } +} diff --git a/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_dialog.h b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_dialog.h new file mode 100644 index 000000000..8b528d445 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_dialog.h @@ -0,0 +1,39 @@ +#ifndef DECK_PREVIEW_TAG_DIALOG_H +#define DECK_PREVIEW_TAG_DIALOG_H + +#include +#include +#include +#include +#include +#include +#include + +class DeckPreviewTagDialog : public QDialog +{ + Q_OBJECT + +public: + explicit DeckPreviewTagDialog(const QStringList &knownTags, + const QStringList &activeTags, + QWidget *parent = nullptr); + QStringList getActiveTags() const; + void filterTags(const QString &text); + +private slots: + void addTag(); + void onCheckboxStateChanged(); + void retranslateUi(); + +private: + QLabel *instructionLabel; + QListWidget *tagListView_; + QLineEdit *filterInput_; + QLineEdit *newTagInput_; + QPushButton *addTagButton_; + QPushButton *okButton; + QPushButton *cancelButton; + QStringList activeTags_; +}; + +#endif // DECK_PREVIEW_TAG_DIALOG_H diff --git a/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_display_widget.cpp b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_display_widget.cpp new file mode 100644 index 000000000..324cc7288 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_display_widget.cpp @@ -0,0 +1,110 @@ +#include "deck_preview_tag_display_widget.h" + +#include +#include +#include +#include + +DeckPreviewTagDisplayWidget::DeckPreviewTagDisplayWidget(QWidget *parent, const QString &_tagName) + : QWidget(parent), tagName(_tagName), isSelected(false) +{ + // Create layout + auto *layout = new QHBoxLayout(this); + layout->setContentsMargins(5, 5, 5, 5); + layout->setSpacing(5); + + // Add a stretch spacer for text and close button separation + // layout->addStretch(); // Ensures the close button stays at the far-right side + + // Create close button + // closeButton = new QPushButton("x", this); + // closeButton->setFixedSize(16, 16); // Small square button + // closeButton->setFocusPolicy(Qt::NoFocus); + + // Set font for close button to ensure the "x" appears correctly + // QFont closeButtonFont = closeButton->font(); + // closeButtonFont.setPointSize(10); // Adjust the size to make the "x" clear + // closeButton->setFont(closeButtonFont); + + // layout->addWidget(closeButton); + + // Adjust widget size + setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); + + // Connect close button to the remove signal + // connect(closeButton, &QPushButton::clicked, this, &DeckPreviewTagDisplayWidget::tagClosed); +} + +QSize DeckPreviewTagDisplayWidget::sizeHint() const +{ + // Calculate the size based on the tag name and close button + QFontMetrics fm(font()); + int textWidth = fm.horizontalAdvance(tagName); + int width = textWidth + 50; // Add extra padding + int height = fm.height() + 10; // Height based on font size + padding + + return QSize(width, height); +} + +void DeckPreviewTagDisplayWidget::mousePressEvent(QMouseEvent *event) +{ + if (event->button() == Qt::LeftButton) { + setSelected(!isSelected); + emit tagClicked(); + } + QWidget::mousePressEvent(event); +} + +void DeckPreviewTagDisplayWidget::setSelected(bool selected) +{ + isSelected = selected; + update(); // Trigger a repaint +} + +void DeckPreviewTagDisplayWidget::paintEvent(QPaintEvent *event) +{ + QPainter painter(this); + + // Set background color + QColor backgroundColor = isSelected ? QColor(173, 216, 230) : Qt::white; + painter.setBrush(backgroundColor); + painter.setPen(Qt::NoPen); + + // Draw background + painter.drawRect(rect()); + + // Draw border + QColor borderColor = isSelected ? Qt::blue : Qt::gray; + QPen borderPen(borderColor, isSelected ? 2 : 1); + painter.setPen(borderPen); + painter.drawRect(rect().adjusted(0, 0, -1, -1)); // Adjust for pen width + + // Calculate font size based on widget height + QFont font = painter.font(); + int fontSize = std::max(10, height() / 2); // Ensure a minimum font size of 10 + font.setPointSize(fontSize); + painter.setFont(font); + + // Calculate text rect to avoid overlap with the close button + // int closeButtonWidth = closeButton->width(); + int margin = 10; // Left and right margins + QRect textRect(margin, 0, width() - margin * 2, height()); + + // Draw the text with a black border for better legibility + painter.setPen(Qt::black); + + // Draw text border by offsetting + for (int dx = -1; dx <= 1; ++dx) { + for (int dy = -1; dy <= 1; ++dy) { + if (dx != 0 || dy != 0) { + painter.drawText(textRect.translated(dx, dy), Qt::AlignLeft | Qt::AlignVCenter, tagName); + } + } + } + + // Draw the actual text + painter.setPen(Qt::white); + painter.drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, tagName); + + QWidget::paintEvent(event); +} diff --git a/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_display_widget.h b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_display_widget.h new file mode 100644 index 000000000..10cc39baf --- /dev/null +++ b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_display_widget.h @@ -0,0 +1,67 @@ +#ifndef DECK_PREVIEW_TAG_DISPLAY_WIDGET_H +#define DECK_PREVIEW_TAG_DISPLAY_WIDGET_H + +#include +#include +#include +#include + +class DeckPreviewTagDisplayWidget : public QWidget +{ + Q_OBJECT + +public: + /** + * @brief Constructor for DeckPreviewTagDisplayWidget. + * @param parent The parent widget. + * @param tagName The name of the tag to display. + */ + explicit DeckPreviewTagDisplayWidget(QWidget *parent = nullptr, const QString &tagName = ""); + QSize sizeHint() const override; + QString getTagName() const + { + return tagName; + } + bool getSelected() const + { + return isSelected; + } + + /** + * @brief Sets the selected state of the tag. + * @param selected True if the tag is selected, false otherwise. + */ + void setSelected(bool selected); + +signals: + /** + * @brief Emitted when the tag is clicked. + */ + void tagClicked(); + + /** + * @brief Emitted when the close button is clicked. + */ + void tagClosed(); + +protected: + /** + * @brief Custom paint event for drawing the widget. + * @param event The paint event. + */ + void paintEvent(QPaintEvent *event) override; + + /** + * @brief Custom mouse press event handler. + * @param event The mouse event. + */ + void mousePressEvent(QMouseEvent *event) override; + +private: + QLabel *tagLabel; ///< Label for displaying the tag name. + QPushButton *closeButton; ///< Button to close/remove the tag. + QString tagName; ///< The name of the tag. + bool isSelected; ///< Indicates whether the tag is selected. +}; + +#endif // DECK_PREVIEW_TAG_DISPLAY_WIDGET_H diff --git a/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_item_widget.cpp b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_item_widget.cpp new file mode 100644 index 000000000..d9ea9fc30 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_item_widget.cpp @@ -0,0 +1,20 @@ +#include "deck_preview_tag_item_widget.h" + +DeckPreviewTagItemWidget::DeckPreviewTagItemWidget(const QString &tagName, bool isChecked, QWidget *parent) + : QWidget(parent), checkBox_(new QCheckBox(this)) +{ + auto *layout = new QHBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(5); + + checkBox_->setText(tagName); // Set the tag name as the checkbox label + checkBox_->setChecked(isChecked); // Set the initial state of the checkbox + + layout->addWidget(checkBox_); // Add the checkbox to the layout + setLayout(layout); // Set the layout of this widget +} + +QCheckBox *DeckPreviewTagItemWidget::checkBox() const +{ + return checkBox_; // Return the checkbox widget +} diff --git a/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_item_widget.h b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_item_widget.h new file mode 100644 index 000000000..60cd8ba44 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_item_widget.h @@ -0,0 +1,22 @@ +#ifndef DECK_PREVIEW_TAG_ITEM_WIDGET_H +#define DECK_PREVIEW_TAG_ITEM_WIDGET_H + +#include +#include +#include + +class DeckPreviewTagItemWidget : public QWidget +{ + Q_OBJECT +public: + // Constructor: Initializes the tag item widget with a tag name and initial checkbox state + DeckPreviewTagItemWidget(const QString &tagName, bool isChecked, QWidget *parent = nullptr); + + // Accessor for the checkbox widget + QCheckBox *checkBox() const; + +private: + QCheckBox *checkBox_; // Checkbox to represent the tag's state +}; + +#endif // DECK_PREVIEW_TAG_ITEM_WIDGET_H 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 new file mode 100644 index 000000000..ab371bd32 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp @@ -0,0 +1,93 @@ +#include "deck_preview_widget.h" + +#include "../../../../../game/cards/card_database_manager.h" +#include "../../cards/deck_preview_card_picture_widget.h" +#include "deck_preview_deck_tags_display_widget.h" + +#include +#include +#include +#include + +DeckPreviewWidget::DeckPreviewWidget(VisualDeckStorageWidget *_parent, const QString &_filePath) + : QWidget(_parent), parent(_parent), filePath(_filePath) +{ + layout = new QVBoxLayout(this); + setLayout(layout); + + deckLoader = new DeckLoader(); + deckLoader->loadFromFile(filePath, DeckLoader::CockatriceFormat); + + auto bannerCard = deckLoader->getBannerCard().first.isEmpty() + ? CardInfoPtr() + : CardDatabaseManager::getInstance()->getCardByNameAndProviderId( + deckLoader->getBannerCard().first, deckLoader->getBannerCard().second); + + bannerCardDisplayWidget = new DeckPreviewCardPictureWidget(this); + + connect(bannerCardDisplayWidget, &DeckPreviewCardPictureWidget::imageClicked, this, + &DeckPreviewWidget::imageClickedEvent); + connect(bannerCardDisplayWidget, &DeckPreviewCardPictureWidget::imageDoubleClicked, this, + &DeckPreviewWidget::imageDoubleClickedEvent); + + bannerCardDisplayWidget->setCard(bannerCard); + bannerCardDisplayWidget->setOverlayText( + deckLoader->getName().isEmpty() ? QFileInfo(deckLoader->getLastFileName()).fileName() : deckLoader->getName()); + bannerCardDisplayWidget->setFontSize(24); + setFilePath(deckLoader->getLastFileName()); + + colorIdentityWidget = new DeckPreviewColorIdentityWidget(getColorIdentity()); + deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this, deckLoader); + + layout->addWidget(bannerCardDisplayWidget); + layout->addWidget(colorIdentityWidget); + layout->addWidget(deckTagsDisplayWidget); +} + +QString DeckPreviewWidget::getColorIdentity() +{ + QStringList cardList = deckLoader->getCardList(); + if (cardList.isEmpty()) { + return {}; + } + + QSet colorSet; // A set to collect unique color symbols (e.g., W, U, B, R, G) + + for (const QString &cardName : cardList) { + CardInfoPtr currentCard = CardDatabaseManager::getInstance()->getCard(cardName); + if (currentCard) { + QString colors = currentCard->getColors(); // Assuming this returns something like "WUB" + for (const QChar &color : colors) { + colorSet.insert(color); + } + } + } + + // Ensure the color identity is in WUBRG order + QString colorIdentity; + const QString wubrgOrder = "WUBRG"; + for (const QChar &color : wubrgOrder) { + if (colorSet.contains(color)) { + colorIdentity.append(color); + } + } + + return colorIdentity; +} + +void DeckPreviewWidget::setFilePath(const QString &_filePath) +{ + filePath = _filePath; +} + +void DeckPreviewWidget::imageClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance) +{ + Q_UNUSED(instance); + emit deckPreviewClicked(event, this); +} + +void DeckPreviewWidget::imageDoubleClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance) +{ + Q_UNUSED(instance); + emit deckPreviewDoubleClicked(event, this); +} \ No newline at end of file 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 new file mode 100644 index 000000000..8c9be2c8b --- /dev/null +++ b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h @@ -0,0 +1,40 @@ +#ifndef DECK_PREVIEW_WIDGET_H +#define DECK_PREVIEW_WIDGET_H + +#include "../../../../../deck/deck_loader.h" +#include "../../cards/deck_preview_card_picture_widget.h" +#include "../visual_deck_storage_widget.h" +#include "deck_preview_color_identity_widget.h" +#include "deck_preview_deck_tags_display_widget.h" + +#include +#include + +class VisualDeckStorageWidget; +class DeckPreviewDeckTagsDisplayWidget; +class DeckPreviewWidget final : public QWidget +{ + Q_OBJECT +public: + explicit DeckPreviewWidget(VisualDeckStorageWidget *_parent, const QString &_filePath); + QString getColorIdentity(); + + VisualDeckStorageWidget *parent; + QVBoxLayout *layout; + QString filePath; + DeckLoader *deckLoader; + DeckPreviewCardPictureWidget *bannerCardDisplayWidget; + DeckPreviewColorIdentityWidget *colorIdentityWidget; + DeckPreviewDeckTagsDisplayWidget *deckTagsDisplayWidget; + +signals: + void deckPreviewClicked(QMouseEvent *event, DeckPreviewWidget *instance); + void deckPreviewDoubleClicked(QMouseEvent *event, DeckPreviewWidget *instance); + +public slots: + void setFilePath(const QString &filePath); + void imageClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance); + void imageDoubleClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance); +}; + +#endif // DECK_PREVIEW_WIDGET_H diff --git a/cockatrice/src/client/ui/widgets/visual_deck_storage/visual_deck_storage_search_widget.cpp b/cockatrice/src/client/ui/widgets/visual_deck_storage/visual_deck_storage_search_widget.cpp index 383755175..397c3367b 100644 --- a/cockatrice/src/client/ui/widgets/visual_deck_storage/visual_deck_storage_search_widget.cpp +++ b/cockatrice/src/client/ui/widgets/visual_deck_storage/visual_deck_storage_search_widget.cpp @@ -38,22 +38,23 @@ QString VisualDeckStorageSearchWidget::getSearchText() return searchBar->text(); } -QStringList VisualDeckStorageSearchWidget::filterFiles(const QStringList &files, const QString &searchText) +QList VisualDeckStorageSearchWidget::filterFiles(QList &widgets, + const QString &searchText) { if (searchText.isEmpty() || searchText.isNull()) { - return files; + return widgets; } - QStringList filteredFiles; + QList filteredWidgets; - for (const auto &file : files) { - QFileInfo fileInfo(file); + for (const auto &file : widgets) { + QFileInfo fileInfo(file->filePath); QString fileName = fileInfo.fileName().toLower(); if (fileName.contains(searchText.toLower())) { - filteredFiles << file; + filteredWidgets << file; } } - return filteredFiles; + return filteredWidgets; } diff --git a/cockatrice/src/client/ui/widgets/visual_deck_storage/visual_deck_storage_search_widget.h b/cockatrice/src/client/ui/widgets/visual_deck_storage/visual_deck_storage_search_widget.h index e136c5491..880cc4a47 100644 --- a/cockatrice/src/client/ui/widgets/visual_deck_storage/visual_deck_storage_search_widget.h +++ b/cockatrice/src/client/ui/widgets/visual_deck_storage/visual_deck_storage_search_widget.h @@ -16,7 +16,7 @@ class VisualDeckStorageSearchWidget : public QWidget public: explicit VisualDeckStorageSearchWidget(VisualDeckStorageWidget *parent); QString getSearchText(); - QStringList filterFiles(const QStringList &files, const QString &searchText); + QList filterFiles(QList &widgets, const QString &searchText); private: QHBoxLayout *layout; diff --git a/cockatrice/src/client/ui/widgets/visual_deck_storage/visual_deck_storage_sort_widget.cpp b/cockatrice/src/client/ui/widgets/visual_deck_storage/visual_deck_storage_sort_widget.cpp new file mode 100644 index 000000000..f794f3016 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/visual_deck_storage/visual_deck_storage_sort_widget.cpp @@ -0,0 +1,93 @@ +#include "visual_deck_storage_sort_widget.h" + +#include "../../../../settings/cache_settings.h" + +/** + * @brief Constructs a PrintingSelectorCardSortWidget for searching cards by set name or set code. + * + * This widget provides a search bar that allows users to search for cards by either their set name + * or set code. It uses a debounced timer to trigger the search action after the user stops typing. + * + * @param parent The parent PrintingSelector widget that will handle the search results. + */ +VisualDeckStorageSortWidget::VisualDeckStorageSortWidget(VisualDeckStorageWidget *parent) + : parent(parent), sortOrder(Alphabetical) +{ + layout = new QHBoxLayout(this); + setLayout(layout); + + // Initialize the ComboBox + sortComboBox = new QComboBox(this); + layout->addWidget(sortComboBox); + + // Set the current sort order + sortComboBox->setCurrentIndex(SettingsCache::instance().getVisualDeckStorageSortingOrder()); + + // Connect sorting change signal to refresh the file list + connect(sortComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, + &VisualDeckStorageSortWidget::updateSortOrder); + connect(this, &VisualDeckStorageSortWidget::sortOrderChanged, parent, &VisualDeckStorageWidget::updateSortOrder); + + retranslateUi(); +} + +void VisualDeckStorageSortWidget::retranslateUi() +{ + // Block signals to avoid triggering unnecessary updates while changing text + sortComboBox->blockSignals(true); + + // Clear and repopulate the ComboBox with translated items + sortComboBox->clear(); + sortComboBox->addItem(tr("Sort Alphabetically (Deck Name)"), ByName); + sortComboBox->addItem(tr("Sort Alphabetically (Filename)"), Alphabetical); + sortComboBox->addItem(tr("Sort by Last Modified"), ByLastModified); + sortComboBox->addItem(tr("Sort by Last Loaded"), ByLastLoaded); + + // Restore the current index + sortComboBox->setCurrentIndex(SettingsCache::instance().getVisualDeckStorageSortingOrder()); + + // Re-enable signals + sortComboBox->blockSignals(false); +} + +void VisualDeckStorageSortWidget::showEvent(QShowEvent *event) +{ + QWidget::showEvent(event); + sortComboBox->setCurrentIndex(SettingsCache::instance().getVisualDeckStorageSortingOrder()); + updateSortOrder(); +} + +void VisualDeckStorageSortWidget::updateSortOrder() +{ + sortOrder = static_cast(sortComboBox->currentIndex()); + SettingsCache::instance().setVisualDeckStorageSortingOrder(sortComboBox->currentIndex()); + emit sortOrderChanged(); +} + +QList &VisualDeckStorageSortWidget::filterFiles(QList &widgets) +{ + // Sort the widgets list based on the current sort order + std::sort(widgets.begin(), widgets.end(), [this](DeckPreviewWidget *widget1, DeckPreviewWidget *widget2) { + if (!widget1 || !widget2) { + return false; // Handle null pointers gracefully + } + + QFileInfo info1(widget1->filePath); + QFileInfo info2(widget2->filePath); + + switch (sortOrder) { + case ByName: + return widget1->deckLoader->getName() < widget2->deckLoader->getName(); + case Alphabetical: + return info1.fileName().toLower() < info2.fileName().toLower(); + case ByLastModified: + return info1.lastModified() > info2.lastModified(); + case ByLastLoaded: + return widget1->deckLoader->getLastLoadedTimestamp() > widget2->deckLoader->getLastLoadedTimestamp(); + } + + return false; // Default case, no sorting applied + }); + + return widgets; +} diff --git a/cockatrice/src/client/ui/widgets/visual_deck_storage/visual_deck_storage_sort_widget.h b/cockatrice/src/client/ui/widgets/visual_deck_storage/visual_deck_storage_sort_widget.h new file mode 100644 index 000000000..25dccdca7 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/visual_deck_storage/visual_deck_storage_sort_widget.h @@ -0,0 +1,42 @@ +#ifndef VISUAL_DECK_STORAGE_SORT_WIDGET_H +#define VISUAL_DECK_STORAGE_SORT_WIDGET_H + +#include "visual_deck_storage_widget.h" + +#include +#include +#include + +class VisualDeckStorageWidget; +class VisualDeckStorageSortWidget : public QWidget +{ + Q_OBJECT + +public: + explicit VisualDeckStorageSortWidget(VisualDeckStorageWidget *parent); + void retranslateUi(); + void updateSortOrder(); + QString getSearchText(); + QList &filterFiles(QList &widgets); + +public slots: + void showEvent(QShowEvent *event) override; + +signals: + void sortOrderChanged(); + +private: + enum SortOrder + { + ByName, + Alphabetical, + ByLastModified, + ByLastLoaded, + }; + QHBoxLayout *layout; + VisualDeckStorageWidget *parent; + SortOrder sortOrder; // Current sorting option + QComboBox *sortComboBox; +}; + +#endif // VISUAL_DECK_STORAGE_SORT_WIDGET_H 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 new file mode 100644 index 000000000..57d963ab2 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.cpp @@ -0,0 +1,101 @@ +#include "visual_deck_storage_tag_filter_widget.h" + +#include "../general/layout_containers/flow_widget.h" +#include "deck_preview/deck_preview_tag_addition_widget.h" +#include "deck_preview/deck_preview_tag_display_widget.h" +#include "deck_preview/deck_preview_widget.h" + +#include +#include + +VisualDeckStorageTagFilterWidget::VisualDeckStorageTagFilterWidget(VisualDeckStorageWidget *_parent) + : QWidget(_parent), parent(_parent) +{ + + setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + + // Create layout + auto *layout = new QHBoxLayout(this); + layout->setContentsMargins(5, 5, 5, 5); + layout->setSpacing(5); + + setFixedHeight(100); + + auto *flowWidget = new FlowWidget(this, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded); + + layout->addWidget(flowWidget); +} + +QList +VisualDeckStorageTagFilterWidget::filterDecksBySelectedTags(const QList &deckPreviews) const +{ + // Collect selected tags from DeckPreviewTagDisplayWidget + QStringList selectedTags; + foreach (DeckPreviewTagDisplayWidget *tagWidget, findChildren()) { + if (tagWidget->getSelected()) { + selectedTags.append(tagWidget->getTagName()); + } + } + + // If no tags are selected, return all decks + if (selectedTags.isEmpty()) { + return deckPreviews; + } + + // Filter DeckPreviewWidgets that contain all of the selected tags + QList filteredDecks; + for (DeckPreviewWidget *deckPreview : deckPreviews) { + QStringList deckTags = deckPreview->deckLoader->getTags(); + + // Check if all selectedTags are in deckTags + bool allTagsPresent = std::all_of(selectedTags.begin(), selectedTags.end(), + [&deckTags](const QString &tag) { return deckTags.contains(tag); }); + + if (allTagsPresent) { + filteredDecks.append(deckPreview); + } + } + + return filteredDecks; +} + +void VisualDeckStorageTagFilterWidget::removeTagsNotInList(const QStringList &tags) +{ + // Iterate through all DeckPreviewTagDisplayWidgets + foreach (DeckPreviewTagDisplayWidget *tagWidget, findChildren()) { + // If the tag is not in the provided tags list, remove the widget + if (!tags.contains(tagWidget->getTagName())) { + auto *flowWidget = findChild(); + flowWidget->removeWidget(tagWidget); + tagWidget->deleteLater(); // Safely delete the widget + } + } +} + +void VisualDeckStorageTagFilterWidget::addTagsIfNotPresent(const QStringList &tags) +{ + for (const QString &tag : tags) { + addTagIfNotPresent(tag); + } +} + +void VisualDeckStorageTagFilterWidget::addTagIfNotPresent(const QString &tag) +{ + // Check if the tag already exists in the flow widget + bool tagExists = false; + foreach (DeckPreviewTagDisplayWidget *tagWidget, findChildren()) { + if (tagWidget->getTagName() == tag) { + tagExists = true; + break; + } + } + + // If the tag doesn't exist, add a new DeckPreviewTagDisplayWidget + if (!tagExists) { + auto *newTagWidget = new DeckPreviewTagDisplayWidget(this, tag); + connect(newTagWidget, &DeckPreviewTagDisplayWidget::tagClicked, parent, + &VisualDeckStorageWidget::refreshBannerCards); + auto *flowWidget = findChild(); + flowWidget->addWidget(newTagWidget); + } +} diff --git a/cockatrice/src/client/ui/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.h b/cockatrice/src/client/ui/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.h new file mode 100644 index 000000000..13fc7ef4d --- /dev/null +++ b/cockatrice/src/client/ui/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.h @@ -0,0 +1,23 @@ +#ifndef VISUAL_DECK_STORAGE_TAG_FILTER_WIDGET_H +#define VISUAL_DECK_STORAGE_TAG_FILTER_WIDGET_H + +#include "visual_deck_storage_widget.h" + +#include + +class VisualDeckStorageWidget; +class VisualDeckStorageTagFilterWidget : public QWidget +{ + Q_OBJECT + +public: + explicit VisualDeckStorageTagFilterWidget(VisualDeckStorageWidget *_parent); + void refreshTags(); + QList filterDecksBySelectedTags(const QList &deckPreviews) const; + void removeTagsNotInList(const QStringList &tags); + void addTagsIfNotPresent(const QStringList &tags); + void addTagIfNotPresent(const QString &tag); + VisualDeckStorageWidget *parent; +}; + +#endif // VISUAL_DECK_STORAGE_TAG_FILTER_WIDGET_H 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 3c3ca9cbf..7d16d7a3e 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 @@ -3,14 +3,17 @@ #include "../../../../deck/deck_loader.h" #include "../../../../game/cards/card_database_manager.h" #include "../../../../settings/cache_settings.h" +#include "deck_preview/deck_preview_widget.h" #include "visual_deck_storage_search_widget.h" +#include "visual_deck_storage_sort_widget.h" +#include "visual_deck_storage_tag_filter_widget.h" #include #include #include #include -VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(parent), sortOrder(Alphabetical) +VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(parent) { deckListModel = new DeckListModel(this); deckListModel->setObjectName("visualDeckModel"); @@ -18,27 +21,24 @@ VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(pare layout = new QVBoxLayout(); setLayout(layout); - // ComboBox for sorting options - sortComboBox = new QComboBox(this); - sortComboBox->addItem("Sort Alphabetically (Filename)", Alphabetical); - sortComboBox->addItem("Sort by Last Modified", ByLastModified); - sortComboBox->setCurrentIndex(SettingsCache::instance().getVisualDeckStorageSortingOrder()); + searchAndSortLayout = new QHBoxLayout(); + sortWidget = new VisualDeckStorageSortWidget(this); searchWidget = new VisualDeckStorageSearchWidget(this); + tagFilterWidget = new VisualDeckStorageTagFilterWidget(this); + deckPreviewColorIdentityFilterWidget = new DeckPreviewColorIdentityFilterWidget(this); - // Add combo box to the main layout - layout->addWidget(sortComboBox); - layout->addWidget(searchWidget); + searchAndSortLayout->addWidget(deckPreviewColorIdentityFilterWidget); + searchAndSortLayout->addWidget(sortWidget); + searchAndSortLayout->addWidget(searchWidget); + layout->addLayout(searchAndSortLayout); + layout->addWidget(tagFilterWidget); flowWidget = new FlowWidget(this, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded); layout->addWidget(flowWidget); cardSizeWidget = new CardSizeWidget(this, flowWidget, SettingsCache::instance().getVisualDeckStorageCardSize()); layout->addWidget(cardSizeWidget); - - // Connect sorting change signal to refresh the file list - connect(sortComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, - &VisualDeckStorageWidget::updateSortOrder); } void VisualDeckStorageWidget::showEvent(QShowEvent *event) @@ -49,24 +49,23 @@ void VisualDeckStorageWidget::showEvent(QShowEvent *event) void VisualDeckStorageWidget::updateSortOrder() { - sortOrder = static_cast(sortComboBox->currentData().toInt()); - SettingsCache::instance().setVisualDeckStorageSortingOrder(sortComboBox->currentData().toInt()); refreshBannerCards(); // Refresh the banner cards with the new sort order } -void VisualDeckStorageWidget::imageClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance) +void VisualDeckStorageWidget::deckPreviewClickedEvent(QMouseEvent *event, DeckPreviewWidget *instance) { - emit imageClicked(event, instance); + emit deckPreviewClicked(event, instance); } -void VisualDeckStorageWidget::imageDoubleClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance) +void VisualDeckStorageWidget::deckPreviewDoubleClickedEvent(QMouseEvent *event, DeckPreviewWidget *instance) { - emit imageDoubleClicked(event, instance); + emit deckPreviewDoubleClicked(event, instance); } void VisualDeckStorageWidget::refreshBannerCards() { QStringList allFiles; + QList allDecks; // QDirIterator with QDir::Files and QDir::NoSymLinks ensures only files are listed (no directories or symlinks) QDirIterator it(SettingsCache::instance().getDeckPath(), QDir::Files | QDir::NoSymLinks, @@ -76,46 +75,71 @@ void VisualDeckStorageWidget::refreshBannerCards() allFiles << it.next(); // Add each file path to the list } - // Sort files based on the current sort order - std::sort(allFiles.begin(), allFiles.end(), [this](const QString &file1, const QString &file2) { - QFileInfo info1(file1); - QFileInfo info2(file2); + foreach (const QString &file, allFiles) { + auto *display = new DeckPreviewWidget(this, file); - switch (sortOrder) { - case Alphabetical: - return info1.fileName().toLower() < info2.fileName().toLower(); - case ByLastModified: - return info1.lastModified() > info2.lastModified(); - } + connect(display, &DeckPreviewWidget::deckPreviewClicked, this, + &VisualDeckStorageWidget::deckPreviewClickedEvent); + connect(display, &DeckPreviewWidget::deckPreviewDoubleClicked, this, + &VisualDeckStorageWidget::deckPreviewDoubleClickedEvent); + connect(cardSizeWidget->getSlider(), &QSlider::valueChanged, display->bannerCardDisplayWidget, + &CardInfoPictureWidget::setScaleFactor); + display->bannerCardDisplayWidget->setScaleFactor(cardSizeWidget->getSlider()->value()); + allDecks.append(display); + } - return false; // Default case - }); + auto filteredByColorIdentity = + deckPreviewColorIdentityFilterWidget->filterWidgets(sortWidget->filterFiles(allDecks)); + auto filteredByTags = tagFilterWidget->filterDecksBySelectedTags(filteredByColorIdentity); + auto filteredFiles = searchWidget->filterFiles(filteredByTags, searchWidget->getSearchText()); - auto filteredFiles = searchWidget->filterFiles(allFiles, searchWidget->getSearchText()); + tagFilterWidget->removeTagsNotInList(gatherAllTags(filteredFiles)); + tagFilterWidget->addTagsIfNotPresent(gatherAllTags(filteredFiles)); flowWidget->clearLayout(); // Clear existing widgets in the flow layout - foreach (const QString &file, filteredFiles) { - auto deckLoader = new DeckLoader(); - deckLoader->loadFromFile(file, DeckLoader::CockatriceFormat); - deckListModel->setDeckList(new DeckLoader(*deckLoader)); - - auto *display = new DeckPreviewCardPictureWidget(flowWidget, false); - auto bannerCard = deckLoader->getBannerCard().isEmpty() - ? CardInfoPtr() - : CardDatabaseManager::getInstance()->getCard(deckLoader->getBannerCard()); - display->setCard(bannerCard); - display->setOverlayText(deckLoader->getName().isEmpty() ? QFileInfo(deckLoader->getLastFileName()).fileName() - : deckLoader->getName()); - display->setFontSize(24); - display->setFilePath(deckLoader->getLastFileName()); - - connect(display, &DeckPreviewCardPictureWidget::imageClicked, this, - &VisualDeckStorageWidget::imageClickedEvent); - connect(display, &DeckPreviewCardPictureWidget::imageDoubleClicked, this, - &VisualDeckStorageWidget::imageDoubleClickedEvent); - connect(cardSizeWidget->getSlider(), &QSlider::valueChanged, display, &CardInfoPictureWidget::setScaleFactor); - display->setScaleFactor(cardSizeWidget->getSlider()->value()); - flowWidget->addWidget(display); + foreach (DeckPreviewWidget *deck, filteredFiles) { + flowWidget->addWidget(deck); } + + emit bannerCardsRefreshed(); +} + +QStringList VisualDeckStorageWidget::gatherAllTagsFromFlowWidget() const +{ + QStringList allTags; + + if (flowWidget) { + // Iterate through all DeckPreviewWidgets + foreach (DeckPreviewWidget *display, flowWidget->findChildren()) { + // Get tags from each DeckPreviewWidget + QStringList tags = display->deckLoader->getTags(); + + // Add tags to the list while avoiding duplicates + allTags.append(tags); + } + } + + // Remove duplicates by calling 'removeDuplicates' + allTags.removeDuplicates(); + + return allTags; +} + +QStringList VisualDeckStorageWidget::gatherAllTags(const QList &allDecks) +{ + QStringList allTags; + + // Iterate through all decks provided as input + for (DeckPreviewWidget *deck : allDecks) { + QStringList tags = deck->deckLoader->getTags(); + + // Add tags to the list while avoiding duplicates + allTags.append(tags); + } + + // Remove duplicates + allTags.removeDuplicates(); + + return allTags; } diff --git a/cockatrice/src/client/ui/widgets/visual_deck_storage/visual_deck_storage_widget.h b/cockatrice/src/client/ui/widgets/visual_deck_storage/visual_deck_storage_widget.h index 9428fd663..4b5d37c5b 100644 --- a/cockatrice/src/client/ui/widgets/visual_deck_storage/visual_deck_storage_widget.h +++ b/cockatrice/src/client/ui/widgets/visual_deck_storage/visual_deck_storage_widget.h @@ -3,15 +3,20 @@ #include "../../../../deck/deck_list_model.h" #include "../../../../deck/deck_view.h" -#include "../../../ui/widgets/cards/deck_preview_card_picture_widget.h" #include "../../../ui/widgets/general/layout_containers/flow_widget.h" #include "../cards/card_size_widget.h" +#include "deck_preview/deck_preview_color_identity_filter_widget.h" +#include "deck_preview/deck_preview_widget.h" #include "visual_deck_storage_search_widget.h" +#include "visual_deck_storage_sort_widget.h" +#include "visual_deck_storage_tag_filter_widget.h" -#include #include class VisualDeckStorageSearchWidget; +class VisualDeckStorageSortWidget; +class VisualDeckStorageTagFilterWidget; +class DeckPreviewColorIdentityFilterWidget; class VisualDeckStorageWidget final : public QWidget { Q_OBJECT @@ -20,31 +25,30 @@ public: void retranslateUi(); public slots: - void imageClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance); - void imageDoubleClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance); + void deckPreviewClickedEvent(QMouseEvent *event, DeckPreviewWidget *instance); + void deckPreviewDoubleClickedEvent(QMouseEvent *event, DeckPreviewWidget *instance); void refreshBannerCards(); // Refresh the display of cards based on the current sorting option + QStringList gatherAllTagsFromFlowWidget() const; + QStringList gatherAllTags(const QList &allDecks); void showEvent(QShowEvent *event) override; void updateSortOrder(); signals: - void imageClicked(QMouseEvent *event, DeckPreviewCardPictureWidget *instance); - void imageDoubleClicked(QMouseEvent *event, DeckPreviewCardPictureWidget *instance); + void bannerCardsRefreshed(); + void deckPreviewClicked(QMouseEvent *event, DeckPreviewWidget *instance); + void deckPreviewDoubleClicked(QMouseEvent *event, DeckPreviewWidget *instance); private: - enum SortOrder - { - Alphabetical, - ByLastModified - }; - QVBoxLayout *layout; + QHBoxLayout *searchAndSortLayout; FlowWidget *flowWidget; DeckListModel *deckListModel; QMap cardContainers; - SortOrder sortOrder; // Current sorting option - QComboBox *sortComboBox; + VisualDeckStorageSortWidget *sortWidget; VisualDeckStorageSearchWidget *searchWidget; + VisualDeckStorageTagFilterWidget *tagFilterWidget; + DeckPreviewColorIdentityFilterWidget *deckPreviewColorIdentityFilterWidget; CardSizeWidget *cardSizeWidget; }; diff --git a/cockatrice/src/deck/deck_loader.cpp b/cockatrice/src/deck/deck_loader.cpp index e64ef7446..6dd5664d4 100644 --- a/cockatrice/src/deck/deck_loader.cpp +++ b/cockatrice/src/deck/deck_loader.cpp @@ -7,8 +7,12 @@ #include #include +#include #include #include +#include + +Q_LOGGING_CATEGORY(DeckLoaderLog, "deck_loader") const QStringList DeckLoader::fileNameFilters = QStringList() << QObject::tr("Common deck formats (*.cod *.dec *.dek *.txt *.mwDeck)") @@ -34,7 +38,7 @@ DeckLoader::DeckLoader(const DeckLoader &other) { } -bool DeckLoader::loadFromFile(const QString &fileName, FileFormat fmt) +bool DeckLoader::loadFromFile(const QString &fileName, FileFormat fmt, bool userRequest) { QFile file(fileName); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { @@ -48,9 +52,9 @@ bool DeckLoader::loadFromFile(const QString &fileName, FileFormat fmt) break; case CockatriceFormat: { result = loadFromFile_Native(&file); - qDebug() << "Loaded from" << fileName << "-" << result; + qCDebug(DeckLoaderLog) << "Loaded from" << fileName << "-" << result; if (!result) { - qDebug() << "Retying as plain format"; + qCDebug(DeckLoaderLog) << "Retrying as plain format"; file.seek(0); result = loadFromFile_Plain(&file); fmt = PlainTextFormat; @@ -65,11 +69,14 @@ bool DeckLoader::loadFromFile(const QString &fileName, FileFormat fmt) if (result) { lastFileName = fileName; lastFileFormat = fmt; + if (userRequest) { + updateLastLoadedTimestamp(fileName, fmt); + } emit deckLoaded(); } - qDebug() << "Deck was loaded -" << result; + qCDebug(DeckLoaderLog) << "Deck was loaded -" << result; return result; } @@ -110,6 +117,59 @@ bool DeckLoader::saveToFile(const QString &fileName, FileFormat fmt) return result; } +bool DeckLoader::updateLastLoadedTimestamp(const QString &fileName, FileFormat fmt) +{ + QFileInfo fileInfo(fileName); + if (!fileInfo.exists()) { + qCWarning(DeckLoaderLog) << "File does not exist:" << fileName; + return false; + } + + QDateTime originalTimestamp = fileInfo.lastModified(); + + // Open the file for writing + QFile file(fileName); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { + qCWarning(DeckLoaderLog) << "Failed to open file for writing:" << fileName; + return false; + } + + bool result = false; + + // Perform file modifications + switch (fmt) { + case PlainTextFormat: + break; + case CockatriceFormat: + setLastLoadedTimestamp(QDateTime::currentDateTime().toString()); + result = saveToFile_Native(&file); + break; + } + + file.close(); // Close the file to ensure changes are flushed + + if (result) { + lastFileName = fileName; + lastFileFormat = fmt; + + // Re-open the file and set the original timestamp + if (!file.open(QIODevice::ReadWrite)) { + qCWarning(DeckLoaderLog) << "Failed to re-open file to set timestamp:" << fileName; + return false; + } + + if (!file.setFileTime(originalTimestamp, QFileDevice::FileModificationTime)) { + qCWarning(DeckLoaderLog) << "Failed to set modification time for file:" << fileName; + file.close(); + return false; + } + + file.close(); + } + + return result; +} + // This struct is here to support the forEachCard function call, defined in decklist. It // requires a function to be called for each card, and passes an inner node and a card for // each card in the decklist. diff --git a/cockatrice/src/deck/deck_loader.h b/cockatrice/src/deck/deck_loader.h index 3f1ff1d28..741e5fe30 100644 --- a/cockatrice/src/deck/deck_loader.h +++ b/cockatrice/src/deck/deck_loader.h @@ -42,9 +42,10 @@ public: static FileFormat getFormatFromName(const QString &fileName); - bool loadFromFile(const QString &fileName, FileFormat fmt); + bool loadFromFile(const QString &fileName, FileFormat fmt, bool userRequest = false); bool loadFromRemote(const QString &nativeString, int remoteDeckId); bool saveToFile(const QString &fileName, FileFormat fmt); + bool updateLastLoadedTimestamp(const QString &fileName, FileFormat fmt); QString exportDeckToDecklist(); void resolveSetNameAndNumberToProviderID(); diff --git a/cockatrice/src/dialogs/dlg_settings.cpp b/cockatrice/src/dialogs/dlg_settings.cpp index 537944a0d..621d09612 100644 --- a/cockatrice/src/dialogs/dlg_settings.cpp +++ b/cockatrice/src/dialogs/dlg_settings.cpp @@ -350,9 +350,26 @@ AppearanceSettingsPage::AppearanceSettingsPage() connect(&showVisualDeckStorageOnLoadCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings, &SettingsCache::setVisualDeckStorageShowOnLoad); + visualDeckStorageDrawUnusedColorIdentitiesCheckBox.setChecked( + settings.getVisualDeckStorageDrawUnusedColorIdentities()); + connect(&visualDeckStorageDrawUnusedColorIdentitiesCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings, + &SettingsCache::setVisualDeckStorageDrawUnusedColorIdentities); + + visualDeckStorageUnusedColorIdentitiesOpacitySpinBox.setMinimum(0); + visualDeckStorageUnusedColorIdentitiesOpacitySpinBox.setMaximum(100); + visualDeckStorageUnusedColorIdentitiesOpacitySpinBox.setValue( + settings.getVisualDeckStorageUnusedColorIdentitiesOpacity()); + connect(&visualDeckStorageUnusedColorIdentitiesOpacitySpinBox, QOverload::of(&QSpinBox::valueChanged), + &settings, &SettingsCache::setVisualDeckStorageUnusedColorIdentitiesOpacity); + + visualDeckStorageUnusedColorIdentitiesOpacityLabel.setBuddy(&visualDeckStorageUnusedColorIdentitiesOpacitySpinBox); + auto *menuGrid = new QGridLayout; menuGrid->addWidget(&showShortcutsCheckBox, 0, 0); menuGrid->addWidget(&showVisualDeckStorageOnLoadCheckBox, 1, 0); + menuGrid->addWidget(&visualDeckStorageDrawUnusedColorIdentitiesCheckBox, 2, 0); + menuGrid->addWidget(&visualDeckStorageUnusedColorIdentitiesOpacityLabel, 3, 0); + menuGrid->addWidget(&visualDeckStorageUnusedColorIdentitiesOpacitySpinBox, 3, 1); menuGroupBox = new QGroupBox; menuGroupBox->setLayout(menuGrid); @@ -488,6 +505,10 @@ void AppearanceSettingsPage::retranslateUi() menuGroupBox->setTitle(tr("Menu settings")); showShortcutsCheckBox.setText(tr("Show keyboard shortcuts in right-click menus")); showVisualDeckStorageOnLoadCheckBox.setText(tr("Show visual deck storage on database load")); + visualDeckStorageDrawUnusedColorIdentitiesCheckBox.setText( + tr("Draw missing color identities in visual deck storage without color label")); + visualDeckStorageUnusedColorIdentitiesOpacityLabel.setText(tr("Missing color identity opacity")); + visualDeckStorageUnusedColorIdentitiesOpacitySpinBox.setSuffix("%"); cardsGroupBox->setTitle(tr("Card rendering")); displayCardNamesCheckBox.setText(tr("Display card names on cards having a picture")); diff --git a/cockatrice/src/dialogs/dlg_settings.h b/cockatrice/src/dialogs/dlg_settings.h index 0fb0d694b..5902a4a72 100644 --- a/cockatrice/src/dialogs/dlg_settings.h +++ b/cockatrice/src/dialogs/dlg_settings.h @@ -96,6 +96,9 @@ private: QLabel maxFontSizeForCardsLabel; QCheckBox showShortcutsCheckBox; QCheckBox showVisualDeckStorageOnLoadCheckBox; + QCheckBox visualDeckStorageDrawUnusedColorIdentitiesCheckBox; + QLabel visualDeckStorageUnusedColorIdentitiesOpacityLabel; + QSpinBox visualDeckStorageUnusedColorIdentitiesOpacitySpinBox; QCheckBox displayCardNamesCheckBox; QCheckBox autoRotateSidewaysLayoutCardsCheckBox; QCheckBox overrideAllCardArtWithPersonalPreferenceCheckBox; diff --git a/cockatrice/src/settings/cache_settings.cpp b/cockatrice/src/settings/cache_settings.cpp index 660b5935c..17083d443 100644 --- a/cockatrice/src/settings/cache_settings.cpp +++ b/cockatrice/src/settings/cache_settings.cpp @@ -262,6 +262,10 @@ SettingsCache::SettingsCache() visualDeckStorageCardSize = settings->value("cards/visualdeckstoragecardsize", 100).toInt(); visualDeckStorageShowOnLoad = settings->value("interface/visualdeckstorageshowonload", true).toBool(); visualDeckStorageSortingOrder = settings->value("interface/visualdeckstoragesortingorder", 0).toInt(); + visualDeckStorageDrawUnusedColorIdentities = + settings->value("interface/visualdeckstoragedrawunusedcoloridentities", true).toBool(); + visualDeckStorageUnusedColorIdentitiesOpacity = + settings->value("interface/visualdeckstorageunusedcoloridentitiesopacity", 15).toInt(); horizontalHand = settings->value("hand/horizontal", true).toBool(); invertVerticalCoordinate = settings->value("table/invert_vertical", false).toBool(); minPlayersForMultiColumnLayout = settings->value("interface/min_players_multicolumn", 4).toInt(); @@ -631,6 +635,20 @@ void SettingsCache::setVisualDeckStorageShowOnLoad(QT_STATE_CHANGED_T _visualDec settings->setValue("interface/visualdeckstorageshowonload", visualDeckStorageShowOnLoad); } +void SettingsCache::setVisualDeckStorageDrawUnusedColorIdentities( + QT_STATE_CHANGED_T _visualDeckStorageDrawUnusedColorIdentities) +{ + visualDeckStorageDrawUnusedColorIdentities = _visualDeckStorageDrawUnusedColorIdentities; + settings->setValue("cards/visualdeckstoragedrawunusedcoloridentities", visualDeckStorageDrawUnusedColorIdentities); +} + +void SettingsCache::setVisualDeckStorageUnusedColorIdentitiesOpacity(int _visualDeckStorageUnusedColorIdentitiesOpacity) +{ + visualDeckStorageUnusedColorIdentitiesOpacity = _visualDeckStorageUnusedColorIdentitiesOpacity; + settings->setValue("cards/visualdeckstorageunusedcoloridentitiesopacity", + visualDeckStorageUnusedColorIdentitiesOpacity); +} + 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 63ab91e13..781c18d31 100644 --- a/cockatrice/src/settings/cache_settings.h +++ b/cockatrice/src/settings/cache_settings.h @@ -126,6 +126,8 @@ private: bool printingSelectorNavigationButtonsVisible; int visualDeckStorageSortingOrder; int visualDeckStorageCardSize; + bool visualDeckStorageDrawUnusedColorIdentities; + int visualDeckStorageUnusedColorIdentitiesOpacity; bool visualDeckStorageShowOnLoad; bool horizontalHand; bool invertVerticalCoordinate; @@ -381,6 +383,14 @@ public: { return visualDeckStorageCardSize; } + bool getVisualDeckStorageDrawUnusedColorIdentities() const + { + return visualDeckStorageDrawUnusedColorIdentities; + } + int getVisualDeckStorageUnusedColorIdentitiesOpacity() const + { + return visualDeckStorageUnusedColorIdentitiesOpacity; + } bool getVisualDeckStorageShowOnLoad() const { return visualDeckStorageShowOnLoad; @@ -695,6 +705,8 @@ public slots: void setPrintingSelectorNavigationButtonsVisible(QT_STATE_CHANGED_T _navigationButtonsVisible); void setVisualDeckStorageSortingOrder(int _visualDeckStorageSortingOrder); void setVisualDeckStorageCardSize(int _visualDeckStorageCardSize); + void setVisualDeckStorageDrawUnusedColorIdentities(QT_STATE_CHANGED_T _visualDeckStorageDrawUnusedColorIdentities); + void setVisualDeckStorageUnusedColorIdentitiesOpacity(int _visualDeckStorageUnusedColorIdentitiesOpacity); void setVisualDeckStorageShowOnLoad(QT_STATE_CHANGED_T _visualDeckStorageShowOnLoad); void setHorizontalHand(QT_STATE_CHANGED_T _horizontalHand); void setInvertVerticalCoordinate(QT_STATE_CHANGED_T _invertVerticalCoordinate); diff --git a/common/decklist.cpp b/common/decklist.cpp index 5c34fdb06..164ccdfc6 100644 --- a/common/decklist.cpp +++ b/common/decklist.cpp @@ -368,7 +368,8 @@ DeckList::DeckList() // TODO: https://qt-project.org/doc/qt-4.8/qobject.html#no-copy-constructor-or-assignment-operator DeckList::DeckList(const DeckList &other) - : QObject(), name(other.name), comments(other.comments), bannerCard(other.bannerCard), deckHash(other.deckHash) + : QObject(), name(other.name), comments(other.comments), bannerCard(other.bannerCard), deckHash(other.deckHash), + lastLoadedTimestamp(other.lastLoadedTimestamp) { root = new InnerDecklistNode(other.getRoot()); @@ -419,25 +420,37 @@ bool DeckList::readElement(QXmlStreamReader *xml) { const QString childName = xml->name().toString(); if (xml->isStartElement()) { - if (childName == "deckname") + if (childName == "lastLoadedTimestamp") { + lastLoadedTimestamp = xml->readElementText(); + } else if (childName == "deckname") { name = xml->readElementText(); - else if (childName == "comments") + } else if (childName == "comments") { comments = xml->readElementText(); - else if (childName == "bannerCard") { - bannerCard = xml->readElementText(); - qDebug() << "Deckloader found the banner card " << bannerCard; + } else if (childName == "bannerCard") { + QString providerId = xml->attributes().value("providerId").toString(); + QString cardName = xml->readElementText(); + bannerCard = QPair(cardName, providerId); + } else if (childName == "tags") { + tags.clear(); // Clear existing tags + while (xml->readNextStartElement()) { + if (xml->name().toString() == "tag") { + tags.append(xml->readElementText()); + } + } } else if (childName == "zone") { InnerDecklistNode *newZone = getZoneObjFromName(xml->attributes().value("name").toString()); newZone->readElement(xml); } else if (childName == "sideboard_plan") { SideboardPlan *newSideboardPlan = new SideboardPlan; - if (newSideboardPlan->readElement(xml)) + if (newSideboardPlan->readElement(xml)) { sideboardPlans.insert(newSideboardPlan->getName(), newSideboardPlan); - else + } else { delete newSideboardPlan; + } } - } else if (xml->isEndElement() && (childName == "cockatrice_deck")) + } else if (xml->isEndElement() && (childName == "cockatrice_deck")) { return false; + } return true; } @@ -445,17 +458,33 @@ void DeckList::write(QXmlStreamWriter *xml) { xml->writeStartElement("cockatrice_deck"); xml->writeAttribute("version", "1"); + xml->writeTextElement("lastLoadedTimestamp", lastLoadedTimestamp); xml->writeTextElement("deckname", name); - xml->writeTextElement("comments", comments); - xml->writeTextElement("bannerCard", bannerCard); - - for (int i = 0; i < root->size(); i++) - root->at(i)->writeElement(xml); - - QMapIterator i(sideboardPlans); - while (i.hasNext()) - i.next().value()->write(xml); + xml->writeStartElement("bannerCard"); + xml->writeAttribute("providerId", bannerCard.second); + xml->writeCharacters(bannerCard.first); xml->writeEndElement(); + xml->writeTextElement("comments", comments); + + // Write tags + xml->writeStartElement("tags"); + for (const QString &tag : tags) { + xml->writeTextElement("tag", tag); + } + xml->writeEndElement(); + + // Write zones + for (int i = 0; i < root->size(); i++) { + root->at(i)->writeElement(xml); + } + + // Write sideboard plans + QMapIterator i(sideboardPlans); + while (i.hasNext()) { + i.next().value()->write(xml); + } + + xml->writeEndElement(); // Close "cockatrice_deck" } bool DeckList::loadFromXml(QXmlStreamReader *xml) diff --git a/common/decklist.h b/common/decklist.h index 3c879e376..79dca4e6a 100644 --- a/common/decklist.h +++ b/common/decklist.h @@ -250,8 +250,11 @@ class DeckList : public QObject { Q_OBJECT private: - QString name, comments, bannerCard; + QString name, comments; + QPair bannerCard; QString deckHash; + QString lastLoadedTimestamp; + QStringList tags; QMap sideboardPlans; InnerDecklistNode *root; void getCardListHelper(InnerDecklistNode *node, QSet &result) const; @@ -279,10 +282,26 @@ public slots: { comments = _comments; } - void setBannerCard(const QString &_bannerCard = QString()) + void setTags(const QStringList &_tags = QStringList()) + { + tags = _tags; + } + void addTag(const QString &_tag) + { + tags.append(_tag); + } + void clearTags() + { + tags.clear(); + } + void setBannerCard(const QPair &_bannerCard = QPair()) { bannerCard = _bannerCard; } + void setLastLoadedTimestamp(const QString &_lastLoadedTimestamp = QString()) + { + lastLoadedTimestamp = _lastLoadedTimestamp; + } public: explicit DeckList(); @@ -297,10 +316,18 @@ public: { return comments; } - QString getBannerCard() const + QStringList getTags() const + { + return tags; + } + QPair getBannerCard() const { return bannerCard; } + QString getLastLoadedTimestamp() const + { + return lastLoadedTimestamp; + } QList getCurrentSideboardPlan(); void setCurrentSideboardPlan(const QList &plan); const QMap &getSideboardPlans() const diff --git a/dbconverter/src/mocks.cpp b/dbconverter/src/mocks.cpp index 7f5c16e52..143b7ef7e 100644 --- a/dbconverter/src/mocks.cpp +++ b/dbconverter/src/mocks.cpp @@ -193,6 +193,14 @@ void SettingsCache::setVisualDeckStorageCardSize(int /* _visualDeckStorageCardSi void SettingsCache::setVisualDeckStorageShowOnLoad(QT_STATE_CHANGED_T /* _visualDeckStorageShowOnLoad */) { } +void SettingsCache::setVisualDeckStorageDrawUnusedColorIdentities( + QT_STATE_CHANGED_T /* _visualDeckStorageDrawUnusedColorIdentities */) +{ +} +void SettingsCache::setVisualDeckStorageUnusedColorIdentitiesOpacity( + int /* _visualDeckStorageUnusedColorIdentitiesOpacity */) +{ +} void SettingsCache::setHorizontalHand(QT_STATE_CHANGED_T /* _horizontalHand */) { } diff --git a/tests/carddatabase/mocks.cpp b/tests/carddatabase/mocks.cpp index 572c28f3a..bfb779b03 100644 --- a/tests/carddatabase/mocks.cpp +++ b/tests/carddatabase/mocks.cpp @@ -197,6 +197,14 @@ void SettingsCache::setVisualDeckStorageCardSize(int /* _visualDeckStorageCardSi void SettingsCache::setVisualDeckStorageShowOnLoad(QT_STATE_CHANGED_T /* _visualDeckStorageShowOnLoad */) { } +void SettingsCache::setVisualDeckStorageDrawUnusedColorIdentities( + QT_STATE_CHANGED_T /* _visualDeckStorageDrawUnusedColorIdentities */) +{ +} +void SettingsCache::setVisualDeckStorageUnusedColorIdentitiesOpacity( + int /* _visualDeckStorageUnusedColorIdentitiesOpacity */) +{ +} void SettingsCache::setHorizontalHand(QT_STATE_CHANGED_T /* _horizontalHand */) { }