diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index b99d84d15..e65b784f8 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -161,12 +161,19 @@ set(cockatrice_SOURCES src/game/zones/view_zone_widget.cpp 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/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/src/client/ui/widgets/general/layout_containers/flow_widget.cpp b/cockatrice/src/client/ui/widgets/general/layout_containers/flow_widget.cpp index 9475cd598..00dd76229 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 *widget_to_remove) const +{ + flowLayout->removeWidget(widget_to_remove); +} + /** * @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..c58b13527 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 *widget_to_remove) 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_widget.cpp b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_widget.cpp index ed98913f3..2121f2a88 100644 --- 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 @@ -17,8 +17,8 @@ void DeckPreviewColorCircleWidget::resizeEvent(QResizeEvent *event) // Get the parent of the DeckPreviewColorIdentityWidget QWidget *identityParent = parentWidget() ? parentWidget()->parentWidget() : nullptr; if (identityParent) { - // Calculate the circle diameter as 10% of the parent's height - int maxSize = identityParent->height() * 0.1; + // 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 @@ -124,6 +124,8 @@ DeckPreviewColorIdentityWidget::DeckPreviewColorIdentityWidget(const QString &co 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"; 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..756401e85 --- /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, "Add 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..52c039094 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_addition_widget.cpp @@ -0,0 +1,49 @@ +#include "deck_preview_tag_addition_widget.h" + +#include "deck_preview_tag_dialog.h" + +#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); + + // Create label for the tag name + tagLabel_ = new QLabel(tagName_, this); + layout->addWidget(tagLabel_); + + // Create close button + closeButton_ = new QPushButton("x", this); + closeButton_->setFixedSize(16, 16); // Small square button + layout->addWidget(closeButton_); + + // Adjust widget size + setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); + + // Connect close button to the remove signal + connect(closeButton_, &QPushButton::clicked, this, &DeckPreviewTagAdditionWidget::tagClosed); +} + +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(); + qDebug() << "Updated active tags:" << updatedTags; + parent->deckLoader->setTags(updatedTags); + parent->deckLoader->saveToFile(parent->parent->filePath, DeckLoader::CockatriceFormat); + } +} diff --git a/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_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..07d9e5e43 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_addition_widget.h @@ -0,0 +1,31 @@ +#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); + +signals: + void tagClicked(); // Emitted when the tag is clicked + void tagClosed(); // Emitted when the close button is clicked + +protected: + void mousePressEvent(QMouseEvent *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..620cc9061 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_dialog.cpp @@ -0,0 +1,120 @@ +#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) +{ + setWindowTitle("Deck Tags Manager"); + resize(400, 500); + + QStringList defaultTags = {"🏃️Aggro", "🧙‍ Control️", "⚔️ Midrange", "🌀 Combo", + "🪓 Mill", "🐉 Tribal", "🔒 Stax"}; + + // Merge knownTags with defaultTags, ensuring no duplicates + QStringList combinedTags = knownTags + defaultTags + activeTags; + combinedTags.removeDuplicates(); + + // Main layout + auto *mainLayout = new QVBoxLayout(this); + + // Instruction label + auto *instructionLabel = + new QLabel("Manage your deck tags. Check or uncheck tags as needed, or add new ones:", 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::checkStateChanged, this, + &DeckPreviewTagDialog::onCheckboxStateChanged); + } + + // Add tag input layout + auto *addTagLayout = new QHBoxLayout(); + newTagInput_ = new QLineEdit(this); + newTagInput_->setPlaceholderText("Add a new tag (e.g., Aggro️)"); + addTagButton_ = new QPushButton("Add Tag", 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(); + auto *okButton = new QPushButton("OK", this); + auto *cancelButton = new QPushButton("Cancel", 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); +} + +QStringList DeckPreviewTagDialog::getActiveTags() const +{ + return activeTags_; +} + +void DeckPreviewTagDialog::addTag() +{ + QString newTag = newTagInput_->text().trimmed(); + if (newTag.isEmpty()) { + QMessageBox::warning(this, "Invalid Input", "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, "Duplicate Tag", "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::checkStateChanged, this, &DeckPreviewTagDialog::onCheckboxStateChanged); + + // Clear the input field + newTagInput_->clear(); +} + +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..29e22e4f8 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_dialog.h @@ -0,0 +1,31 @@ +#ifndef DECK_PREVIEW_TAG_DIALOG_H +#define DECK_PREVIEW_TAG_DIALOG_H + +#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; + +private slots: + void addTag(); + void onCheckboxStateChanged(); + +private: + QListWidget *tagListView_; + QLineEdit *newTagInput_; + QPushButton *addTagButton_; + 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..d87bc43b3 --- /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 + closeButton->width() + 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() - closeButtonWidth - 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 index cad2613da..5c0a9df26 100644 --- a/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp +++ b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp @@ -2,13 +2,15 @@ #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(QWidget *parent, const QString &_filePath) : QWidget(parent), filePath(_filePath) +DeckPreviewWidget::DeckPreviewWidget(VisualDeckStorageWidget *_parent, const QString &_filePath) + : QWidget(_parent), parent(_parent), filePath(_filePath) { layout = new QVBoxLayout(this); setLayout(layout); @@ -35,9 +37,11 @@ DeckPreviewWidget::DeckPreviewWidget(QWidget *parent, const QString &_filePath) setFilePath(deckLoader->getLastFileName()); colorIdentityWidget = new DeckPreviewColorIdentityWidget(getColorIdentity()); + deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this, deckLoader); layout->addWidget(bannerCardDisplayWidget); layout->addWidget(colorIdentityWidget); + layout->addWidget(deckTagsDisplayWidget); } QString DeckPreviewWidget::getColorIdentity() diff --git a/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h index d1348a1df..8c9be2c8b 100644 --- a/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h +++ b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h @@ -3,23 +3,29 @@ #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(QWidget *parent, const QString &_filePath); + 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); 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 63083024b..28a505822 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 @@ -6,6 +6,7 @@ #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 @@ -24,11 +25,13 @@ VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(pare sortWidget = new VisualDeckStorageSortWidget(this); searchWidget = new VisualDeckStorageSearchWidget(this); + tagFilterWidget = new VisualDeckStorageTagFilterWidget(this); deckPreviewColorIdentityFilterWidget = new DeckPreviewColorIdentityFilterWidget(this); searchAndSortLayout->addWidget(sortWidget); searchAndSortLayout->addWidget(searchWidget); layout->addLayout(searchAndSortLayout); + layout->addWidget(tagFilterWidget); layout->addWidget(deckPreviewColorIdentityFilterWidget); flowWidget = new FlowWidget(this, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded); @@ -73,7 +76,7 @@ void VisualDeckStorageWidget::refreshBannerCards() } foreach (const QString &file, allFiles) { - auto *display = new DeckPreviewWidget(flowWidget, file); + auto *display = new DeckPreviewWidget(this, file); connect(display, &DeckPreviewWidget::deckPreviewClicked, this, &VisualDeckStorageWidget::deckPreviewClickedEvent); @@ -87,11 +90,56 @@ void VisualDeckStorageWidget::refreshBannerCards() auto filteredByColorIdentity = deckPreviewColorIdentityFilterWidget->filterWidgets(sortWidget->filterFiles(allDecks)); - auto filteredFiles = searchWidget->filterFiles(filteredByColorIdentity, searchWidget->getSearchText()); + auto filteredByTags = tagFilterWidget->filterDecksBySelectedTags(filteredByColorIdentity); + auto filteredFiles = searchWidget->filterFiles(filteredByTags, searchWidget->getSearchText()); + + tagFilterWidget->removeTagsNotInList(gatherAllTags(filteredFiles)); + tagFilterWidget->addTagsIfNotPresent(gatherAllTags(filteredFiles)); flowWidget->clearLayout(); // Clear existing widgets in the flow layout 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 39e8ec252..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 @@ -9,11 +9,13 @@ #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 class VisualDeckStorageSearchWidget; class VisualDeckStorageSortWidget; +class VisualDeckStorageTagFilterWidget; class DeckPreviewColorIdentityFilterWidget; class VisualDeckStorageWidget final : public QWidget { @@ -26,10 +28,13 @@ public slots: 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 bannerCardsRefreshed(); void deckPreviewClicked(QMouseEvent *event, DeckPreviewWidget *instance); void deckPreviewDoubleClicked(QMouseEvent *event, DeckPreviewWidget *instance); @@ -42,6 +47,7 @@ private: VisualDeckStorageSortWidget *sortWidget; VisualDeckStorageSearchWidget *searchWidget; + VisualDeckStorageTagFilterWidget *tagFilterWidget; DeckPreviewColorIdentityFilterWidget *deckPreviewColorIdentityFilterWidget; CardSizeWidget *cardSizeWidget; }; diff --git a/common/decklist.cpp b/common/decklist.cpp index 0b7a9b049..2acb6a58c 100644 --- a/common/decklist.cpp +++ b/common/decklist.cpp @@ -422,26 +422,35 @@ bool DeckList::readElement(QXmlStreamReader *xml) if (xml->isStartElement()) { if (childName == "lastLoadedTimestamp") { lastLoadedTimestamp = xml->readElementText(); - } else if (childName == "deckname") + } else if (childName == "deckname") { name = xml->readElementText(); - else if (childName == "comments") + } else if (childName == "comments") { comments = xml->readElementText(); - else if (childName == "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->isEndElement() && xml->name() == "tags")) { + if (xml->readNextStartElement() && xml->name() == "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; } @@ -457,14 +466,25 @@ void DeckList::write(QXmlStreamWriter *xml) xml->writeEndElement(); xml->writeTextElement("comments", comments); - for (int i = 0; i < root->size(); i++) - root->at(i)->writeElement(xml); - - QMapIterator i(sideboardPlans); - while (i.hasNext()) - i.next().value()->write(xml); - + // 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 4aedb8611..79dca4e6a 100644 --- a/common/decklist.h +++ b/common/decklist.h @@ -254,6 +254,7 @@ private: QPair bannerCard; QString deckHash; QString lastLoadedTimestamp; + QStringList tags; QMap sideboardPlans; InnerDecklistNode *root; void getCardListHelper(InnerDecklistNode *node, QSet &result) const; @@ -281,6 +282,18 @@ public slots: { comments = _comments; } + 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; @@ -303,6 +316,10 @@ public: { return comments; } + QStringList getTags() const + { + return tags; + } QPair getBannerCard() const { return bannerCard;