diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index d77d70d36..522300346 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -53,7 +53,6 @@ set(cockatrice_SOURCES src/game/board/abstract_card_drag_item.cpp src/game/board/abstract_card_item.cpp src/game/board/abstract_counter.cpp - src/game/board/abstract_graphics_item.cpp src/game/board/arrow_item.cpp src/game/board/arrow_target.cpp src/game/board/card_drag_item.cpp @@ -113,6 +112,7 @@ set(cockatrice_SOURCES src/game/zones/table_zone.cpp src/game/zones/view_zone.cpp src/game/zones/view_zone_widget.cpp + src/game_graphics/board/abstract_graphics_item.cpp src/interface/card_picture_loader/card_picture_loader.cpp src/interface/card_picture_loader/card_picture_loader_local.cpp src/interface/card_picture_loader/card_picture_loader_request_status_display_widget.cpp @@ -145,6 +145,7 @@ set(cockatrice_SOURCES src/interface/widgets/deck_analytics/mana_base_widget.cpp src/interface/widgets/deck_analytics/mana_curve_widget.cpp src/interface/widgets/deck_analytics/mana_devotion_widget.cpp + src/interface/widgets/deck_editor/deck_list_history_manager_widget.cpp src/interface/widgets/deck_editor/deck_editor_card_info_dock_widget.cpp src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp diff --git a/cockatrice/cockatrice.qrc b/cockatrice/cockatrice.qrc index 469cf3d77..99f5bff23 100644 --- a/cockatrice/cockatrice.qrc +++ b/cockatrice/cockatrice.qrc @@ -7,11 +7,14 @@ resources/icons/arrow_bottom_green.svg resources/icons/arrow_down_green.svg + resources/icons/arrow_history.svg resources/icons/arrow_left_green.svg + resources/icons/arrow_redo.svg resources/icons/arrow_right_blue.svg resources/icons/arrow_right_green.svg resources/icons/arrow_top_green.svg resources/icons/arrow_up_green.svg + resources/icons/arrow_undo.svg resources/icons/clearsearch.svg resources/icons/cogwheel.svg resources/icons/conceded.svg diff --git a/cockatrice/resources/icons/arrow_history.svg b/cockatrice/resources/icons/arrow_history.svg new file mode 100644 index 000000000..d130758e6 --- /dev/null +++ b/cockatrice/resources/icons/arrow_history.svg @@ -0,0 +1,8 @@ + + + + history + + + + \ No newline at end of file diff --git a/cockatrice/resources/icons/arrow_redo.svg b/cockatrice/resources/icons/arrow_redo.svg new file mode 100644 index 000000000..dd89e3314 --- /dev/null +++ b/cockatrice/resources/icons/arrow_redo.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/cockatrice/resources/icons/arrow_undo.svg b/cockatrice/resources/icons/arrow_undo.svg new file mode 100644 index 000000000..2e182ffb2 --- /dev/null +++ b/cockatrice/resources/icons/arrow_undo.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/cockatrice/src/game/board/abstract_card_item.h b/cockatrice/src/game/board/abstract_card_item.h index a935c97b6..dad73dc14 100644 --- a/cockatrice/src/game/board/abstract_card_item.h +++ b/cockatrice/src/game/board/abstract_card_item.h @@ -7,8 +7,8 @@ #ifndef ABSTRACTCARDITEM_H #define ABSTRACTCARDITEM_H +#include "../../game_graphics/board/graphics_item_type.h" #include "arrow_target.h" -#include "graphics_item_type.h" #include #include diff --git a/cockatrice/src/game/board/arrow_target.h b/cockatrice/src/game/board/arrow_target.h index 8cc5d55dc..55f4ef678 100644 --- a/cockatrice/src/game/board/arrow_target.h +++ b/cockatrice/src/game/board/arrow_target.h @@ -7,7 +7,7 @@ #ifndef ARROWTARGET_H #define ARROWTARGET_H -#include "abstract_graphics_item.h" +#include "../../game_graphics/board/abstract_graphics_item.h" #include diff --git a/cockatrice/src/game/board/counter_general.cpp b/cockatrice/src/game/board/counter_general.cpp index c71077324..d68486a1b 100644 --- a/cockatrice/src/game/board/counter_general.cpp +++ b/cockatrice/src/game/board/counter_general.cpp @@ -1,7 +1,7 @@ #include "counter_general.h" +#include "../../game_graphics/board/abstract_graphics_item.h" #include "../../interface/pixel_map_generator.h" -#include "abstract_graphics_item.h" #include diff --git a/cockatrice/src/game/deckview/deck_view.cpp b/cockatrice/src/game/deckview/deck_view.cpp index 48c906100..64e9abc46 100644 --- a/cockatrice/src/game/deckview/deck_view.cpp +++ b/cockatrice/src/game/deckview/deck_view.cpp @@ -330,7 +330,7 @@ void DeckViewScene::setDeck(const DeckList &_deck) if (deck) delete deck; - deck = new DeckList(_deck); + deck = new DeckList(_deck.writeToString_Native()); rebuildTree(); applySideboardPlan(deck->getCurrentSideboardPlan()); rearrangeItems(); diff --git a/cockatrice/src/game/hand_counter.h b/cockatrice/src/game/hand_counter.h index cd2b0fe81..2c0175ecc 100644 --- a/cockatrice/src/game/hand_counter.h +++ b/cockatrice/src/game/hand_counter.h @@ -7,8 +7,8 @@ #ifndef HANDCOUNTER_H #define HANDCOUNTER_H -#include "board/abstract_graphics_item.h" -#include "board/graphics_item_type.h" +#include "../game_graphics/board/abstract_graphics_item.h" +#include "../game_graphics/board/graphics_item_type.h" #include diff --git a/cockatrice/src/game/phases_toolbar.h b/cockatrice/src/game/phases_toolbar.h index 57ab6de66..215a97dd1 100644 --- a/cockatrice/src/game/phases_toolbar.h +++ b/cockatrice/src/game/phases_toolbar.h @@ -8,7 +8,7 @@ #ifndef PHASESTOOLBAR_H #define PHASESTOOLBAR_H -#include "board/abstract_graphics_item.h" +#include "../game_graphics/board/abstract_graphics_item.h" #include #include diff --git a/cockatrice/src/game/player/player.cpp b/cockatrice/src/game/player/player.cpp index 300300de3..ffb26c26e 100644 --- a/cockatrice/src/game/player/player.cpp +++ b/cockatrice/src/game/player/player.cpp @@ -263,9 +263,10 @@ void Player::deleteCard(CardItem *card) } } -void Player::setDeck(const DeckLoader &_deck) +// TODO: Does a player need a DeckLoader? +void Player::setDeck(DeckLoader &_deck) { - deck = new DeckLoader(_deck); + deck = new DeckLoader(this, _deck.getDeckList()); emit deckChanged(); } diff --git a/cockatrice/src/game/player/player.h b/cockatrice/src/game/player/player.h index 92125530a..0751abb14 100644 --- a/cockatrice/src/game/player/player.h +++ b/cockatrice/src/game/player/player.h @@ -7,8 +7,8 @@ #ifndef PLAYER_H #define PLAYER_H +#include "../../game_graphics/board/abstract_graphics_item.h" #include "../../interface/widgets/menus/tearoff_menu.h" -#include "../board/abstract_graphics_item.h" #include "../dialogs/dlg_create_token.h" #include "menu/player_menu.h" #include "player_actions.h" @@ -130,7 +130,7 @@ public: return playerMenu; } - void setDeck(const DeckLoader &_deck); + void setDeck(DeckLoader &_deck); [[nodiscard]] DeckLoader *getDeck() const { diff --git a/cockatrice/src/game/player/player_area.h b/cockatrice/src/game/player/player_area.h index ec065fb4b..ec0d23cb6 100644 --- a/cockatrice/src/game/player/player_area.h +++ b/cockatrice/src/game/player/player_area.h @@ -7,7 +7,7 @@ #ifndef COCKATRICE_PLAYER_AREA_H #define COCKATRICE_PLAYER_AREA_H -#include "../board/graphics_item_type.h" +#include "../../game_graphics/board/graphics_item_type.h" #include "QGraphicsItem" /** diff --git a/cockatrice/src/game/player/player_target.h b/cockatrice/src/game/player/player_target.h index bf26838c3..aaa0f0610 100644 --- a/cockatrice/src/game/player/player_target.h +++ b/cockatrice/src/game/player/player_target.h @@ -7,9 +7,9 @@ #ifndef PLAYERTARGET_H #define PLAYERTARGET_H +#include "../../game_graphics/board/graphics_item_type.h" #include "../board/abstract_counter.h" #include "../board/arrow_target.h" -#include "../board/graphics_item_type.h" #include #include diff --git a/cockatrice/src/game/zones/card_zone.h b/cockatrice/src/game/zones/card_zone.h index f489915e2..6fe8157e4 100644 --- a/cockatrice/src/game/zones/card_zone.h +++ b/cockatrice/src/game/zones/card_zone.h @@ -7,8 +7,8 @@ #ifndef CARDZONE_H #define CARDZONE_H -#include "../board/abstract_graphics_item.h" -#include "../board/graphics_item_type.h" +#include "../../game_graphics/board/abstract_graphics_item.h" +#include "../../game_graphics/board/graphics_item_type.h" #include "logic/card_zone_logic.h" #include diff --git a/cockatrice/src/game/board/abstract_graphics_item.cpp b/cockatrice/src/game_graphics/board/abstract_graphics_item.cpp similarity index 100% rename from cockatrice/src/game/board/abstract_graphics_item.cpp rename to cockatrice/src/game_graphics/board/abstract_graphics_item.cpp diff --git a/cockatrice/src/game/board/abstract_graphics_item.h b/cockatrice/src/game_graphics/board/abstract_graphics_item.h similarity index 100% rename from cockatrice/src/game/board/abstract_graphics_item.h rename to cockatrice/src/game_graphics/board/abstract_graphics_item.h diff --git a/cockatrice/src/game/board/graphics_item_type.h b/cockatrice/src/game_graphics/board/graphics_item_type.h similarity index 100% rename from cockatrice/src/game/board/graphics_item_type.h rename to cockatrice/src/game_graphics/board/graphics_item_type.h diff --git a/cockatrice/src/interface/deck_loader/deck_loader.cpp b/cockatrice/src/interface/deck_loader/deck_loader.cpp index ebc951c6e..320302142 100644 --- a/cockatrice/src/interface/deck_loader/deck_loader.cpp +++ b/cockatrice/src/interface/deck_loader/deck_loader.cpp @@ -33,19 +33,6 @@ DeckLoader::DeckLoader(QObject *parent) DeckLoader::DeckLoader(QObject *parent, DeckList *_deckList) : QObject(parent), deckList(_deckList), lastFileFormat(CockatriceFormat), lastRemoteDeckId(-1) { - deckList->setParent(this); -} - -DeckLoader::DeckLoader(const DeckLoader &other) - : QObject(), deckList(other.deckList), lastFileName(other.lastFileName), lastFileFormat(other.lastFileFormat), - lastRemoteDeckId(other.lastRemoteDeckId) -{ -} - -void DeckLoader::setDeckList(DeckList *_deckList) -{ - deckList = _deckList; - deckList->setParent(this); } bool DeckLoader::loadFromFile(const QString &fileName, FileFormat fmt, bool userRequest) @@ -165,12 +152,14 @@ bool DeckLoader::saveToFile(const QString &fileName, FileFormat fmt) break; case CockatriceFormat: result = deckList->saveToFile_Native(&file); + qCInfo(DeckLoaderLog) << "Saving to " << fileName << "-" << result; break; } if (result) { lastFileName = fileName; lastFileFormat = fmt; + qCInfo(DeckLoaderLog) << "Deck was saved -" << result; } file.flush(); diff --git a/cockatrice/src/interface/deck_loader/deck_loader.h b/cockatrice/src/interface/deck_loader/deck_loader.h index a83542ce9..57305b7de 100644 --- a/cockatrice/src/interface/deck_loader/deck_loader.h +++ b/cockatrice/src/interface/deck_loader/deck_loader.h @@ -53,8 +53,9 @@ private: public: DeckLoader(QObject *parent); DeckLoader(QObject *parent, DeckList *_deckList); - void setDeckList(DeckList *_deckList); - DeckLoader(const DeckLoader &other); + DeckLoader(const DeckLoader &) = delete; + DeckLoader &operator=(const DeckLoader &) = delete; + const QString &getLastFileName() const { return lastFileName; diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp index 7a4820dd5..56bd16f0a 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp @@ -13,6 +13,30 @@ #include #include +static int findRestoreIndex(const CardRef &wanted, const QComboBox *combo) +{ + // Try providerId + name (strongest match) + if (!wanted.providerId.isEmpty()) { + for (int i = 0; i < combo->count(); ++i) { + auto pair = combo->itemData(i).value>(); + if (pair.second == wanted.providerId && pair.first == wanted.name) { + return i; + } + } + } + + // Try name only + for (int i = 0; i < combo->count(); ++i) { + auto pair = combo->itemData(i).value>(); + if (pair.first == wanted.name) { + return i; + } + } + + // Not found + return -1; +} + DeckEditorDeckDockWidget::DeckEditorDeckDockWidget(AbstractTabDeckEditor *parent) : QDockWidget(parent), deckEditor(parent) { @@ -28,6 +52,11 @@ DeckEditorDeckDockWidget::DeckEditorDeckDockWidget(AbstractTabDeckEditor *parent void DeckEditorDeckDockWidget::createDeckDock() { + historyManager = new DeckListHistoryManager(); + + connect(deckEditor, &AbstractTabDeckEditor::cardAboutToBeAdded, this, + &DeckEditorDeckDockWidget::onCardAboutToBeAdded); + deckModel = new DeckListModel(this); deckModel->setObjectName("deckModel"); connect(deckModel, &DeckListModel::deckHashChanged, this, &DeckEditorDeckDockWidget::updateHash); @@ -37,6 +66,10 @@ void DeckEditorDeckDockWidget::createDeckDock() proxy = new DeckListStyleProxy(this); proxy->setSourceModel(deckModel); + historyManagerWidget = new DeckListHistoryManagerWidget(deckModel, proxy, historyManager, this); + connect(historyManagerWidget, &DeckListHistoryManagerWidget::requestDisplayWidgetSync, this, + &DeckEditorDeckDockWidget::syncDisplayWidgetsToModel); + deckView = new QTreeView(); deckView->setObjectName("deckView"); deckView->setModel(proxy); @@ -65,7 +98,15 @@ void DeckEditorDeckDockWidget::createDeckDock() nameEdit->setMaxLength(MAX_NAME_LENGTH); nameEdit->setObjectName("nameEdit"); nameLabel->setBuddy(nameEdit); - connect(nameEdit, &LineEditUnfocusable::textChanged, this, &DeckEditorDeckDockWidget::updateName); + + nameDebounceTimer = new QTimer(this); + nameDebounceTimer->setSingleShot(true); + nameDebounceTimer->setInterval(300); // debounce duration in ms + connect(nameDebounceTimer, &QTimer::timeout, this, [this]() { updateName(nameEdit->text()); }); + + connect(nameEdit, &LineEditUnfocusable::textChanged, this, [this]() { + nameDebounceTimer->start(); // restart debounce timer + }); quickSettingsWidget = new SettingsButtonWidget(this); @@ -95,7 +136,16 @@ void DeckEditorDeckDockWidget::createDeckDock() commentsEdit->setMinimumHeight(nameEdit->minimumSizeHint().height()); commentsEdit->setObjectName("commentsEdit"); commentsLabel->setBuddy(commentsEdit); - connect(commentsEdit, &QTextEdit::textChanged, this, &DeckEditorDeckDockWidget::updateComments); + + commentsDebounceTimer = new QTimer(this); + commentsDebounceTimer->setSingleShot(true); + commentsDebounceTimer->setInterval(400); // longer debounce for multi-line + connect(commentsDebounceTimer, &QTimer::timeout, this, [this]() { updateComments(); }); + + connect(commentsEdit, &QTextEdit::textChanged, this, [this]() { + commentsDebounceTimer->start(); // restart debounce timer + }); + bannerCardLabel = new QLabel(); bannerCardLabel->setObjectName("bannerCardLabel"); bannerCardLabel->setText(tr("Banner Card")); @@ -187,7 +237,8 @@ void DeckEditorDeckDockWidget::createDeckDock() lowerLayout->addWidget(tbDecrement, 0, 3); lowerLayout->addWidget(tbRemoveCard, 0, 4); lowerLayout->addWidget(tbSwapCard, 0, 5); - lowerLayout->addWidget(deckView, 1, 0, 1, 6); + lowerLayout->addWidget(historyManagerWidget, 0, 6); + lowerLayout->addWidget(deckView, 1, 0, 1, 7); // Create widgets for both layouts to make splitter work correctly auto *topWidget = new QWidget; @@ -249,6 +300,8 @@ void DeckEditorDeckDockWidget::updateCard(const QModelIndex /*¤t*/, const void DeckEditorDeckDockWidget::updateName(const QString &name) { + historyManager->save(deckLoader->getDeckList()->createMemento( + QString(tr("Rename deck to \"%1\" from \"%2\"")).arg(name).arg(deckLoader->getDeckList()->getName()))); deckModel->getDeckList()->setName(name); deckEditor->setModified(name.isEmpty()); emit nameChanged(); @@ -257,6 +310,11 @@ void DeckEditorDeckDockWidget::updateName(const QString &name) void DeckEditorDeckDockWidget::updateComments() { + historyManager->save( + deckLoader->getDeckList()->createMemento(QString(tr("Updated comments (was %1 chars, now %2 chars)")) + .arg(deckLoader->getDeckList()->getComments().size()) + .arg(commentsEdit->toPlainText().size()))); + deckModel->getDeckList()->setComments(commentsEdit->toPlainText()); deckEditor->setModified(commentsEdit->toPlainText().isEmpty()); emit commentsChanged(); @@ -272,8 +330,8 @@ void DeckEditorDeckDockWidget::updateHash() void DeckEditorDeckDockWidget::updateBannerCardComboBox() { - // Store the current text of the combo box - QString currentText = bannerCardComboBox->currentText(); + // Store current banner card identity + CardRef wanted = deckModel->getDeckList()->getBannerCard(); // Block signals temporarily bool wasBlocked = bannerCardComboBox->blockSignals(true); @@ -281,54 +339,51 @@ void DeckEditorDeckDockWidget::updateBannerCardComboBox() // Clear the existing items in the combo box bannerCardComboBox->clear(); - // Prepare the new items with deduplication + // Collect unique (name, providerId) pairs QSet> bannerCardSet; QList cardsInDeck = deckModel->getDeckList()->getCardNodes(); for (auto currentCard : cardsInDeck) { - for (int k = 0; k < currentCard->getNumber(); ++k) { - if (CardDatabaseManager::query()->getCard(currentCard->toCardRef())) { - bannerCardSet.insert({currentCard->getName(), currentCard->getCardProviderId()}); - } + if (!CardDatabaseManager::query()->getCard(currentCard->toCardRef())) { + continue; } + + // Insert one entry per distinct card, ignore copies + bannerCardSet.insert({currentCard->getName(), currentCard->getCardProviderId()}); } + // Convert to sorted list QList> pairList = bannerCardSet.values(); // 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(); - }); + std::sort(pairList.begin(), pairList.end(), + [](const auto &a, const auto &b) { return a.first.toLower() < b.first.toLower(); }); + // Add to combo box for (const auto &pair : pairList) { bannerCardComboBox->addItem(pair.first, QVariant::fromValue(pair)); } - // Try to restore the previous selection by finding the currentText - int restoredIndex = bannerCardComboBox->findText(currentText); - if (restoredIndex != -1) { - bannerCardComboBox->setCurrentIndex(restoredIndex); - if (deckModel->getDeckList()->getBannerCard().providerId != - bannerCardComboBox->currentData().value>().second) { - setBannerCard(restoredIndex); - } + // Try to find an index with a matching card + int restoreIndex = findRestoreIndex(wanted, bannerCardComboBox); + + // Handle results + if (restoreIndex != -1) { + bannerCardComboBox->setCurrentIndex(restoreIndex); + setBannerCard(restoreIndex); } else { // Add a placeholder "-" and set it as the current selection - int bannerIndex = bannerCardComboBox->findText(deckModel->getDeckList()->getBannerCard().name); - if (bannerIndex != -1) { - bannerCardComboBox->setCurrentIndex(bannerIndex); - } else { - bannerCardComboBox->insertItem(0, "-"); - bannerCardComboBox->setCurrentIndex(0); - } + bannerCardComboBox->insertItem(0, "-"); + bannerCardComboBox->setCurrentIndex(0); } - // Restore the previous signal blocking state + // Restore signal state bannerCardComboBox->blockSignals(wasBlocked); } void DeckEditorDeckDockWidget::setBannerCard(int /* changedIndex */) { + historyManager->save(deckLoader->getDeckList()->createMemento(tr("Banner card changed"))); auto [name, id] = bannerCardComboBox->currentData().value>(); deckModel->getDeckList()->setBannerCard({name, id}); deckEditor->setModified(true); @@ -372,21 +427,40 @@ void DeckEditorDeckDockWidget::setDeck(DeckLoader *_deck) connect(deckLoader, &DeckLoader::deckLoaded, deckModel, &DeckListModel::rebuildTree); connect(deckLoader->getDeckList(), &DeckList::deckHashChanged, deckModel, &DeckListModel::deckHashChanged); - nameEdit->setText(deckModel->getDeckList()->getName()); - commentsEdit->setText(deckModel->getDeckList()->getComments()); + historyManager->clear(); + historyManagerWidget->setDeckListModel(deckModel); - syncBannerCardComboBoxSelectionWithDeck(); - updateBannerCardComboBox(); - updateHash(); - deckModel->sort(deckView->header()->sortIndicatorSection(), deckView->header()->sortIndicatorOrder()); - deckView->expandAll(); - deckView->expandAll(); - - deckTagsDisplayWidget->connectDeckList(deckModel->getDeckList()); + syncDisplayWidgetsToModel(); emit deckChanged(); } +void DeckEditorDeckDockWidget::syncDisplayWidgetsToModel() +{ + nameEdit->blockSignals(true); + nameEdit->setText(deckModel->getDeckList()->getName()); + nameEdit->blockSignals(false); + + commentsEdit->blockSignals(true); + commentsEdit->setText(deckModel->getDeckList()->getComments()); + commentsEdit->blockSignals(false); + + bannerCardComboBox->blockSignals(true); + syncBannerCardComboBoxSelectionWithDeck(); + updateBannerCardComboBox(); + bannerCardComboBox->blockSignals(false); + updateHash(); + sortDeckModelToDeckView(); + expandAll(); + + deckTagsDisplayWidget->connectDeckList(deckModel->getDeckList()); +} + +void DeckEditorDeckDockWidget::sortDeckModelToDeckView() +{ + deckModel->sort(deckView->header()->sortIndicatorSection(), deckView->header()->sortIndicatorOrder()); +} + DeckLoader *DeckEditorDeckDockWidget::getDeckLoader() { return deckLoader; @@ -422,6 +496,12 @@ void DeckEditorDeckDockWidget::recursiveExpand(const QModelIndex &index) deckView->expand(index); } +void DeckEditorDeckDockWidget::expandAll() +{ + deckView->expandAll(); + deckView->expandAll(); +} + /** * Gets the index of all the currently selected card nodes in the decklist table. * The list is in reverse order of the visual selection, so that rows can be deleted while iterating over them. @@ -441,6 +521,14 @@ QModelIndexList DeckEditorDeckDockWidget::getSelectedCardNodes() const return selectedRows; } +void DeckEditorDeckDockWidget::onCardAboutToBeAdded(const ExactCard &addedCard, const QString &zoneName) +{ + historyManager->save(deckLoader->getDeckList()->createMemento( + QString(tr("Added (%1): %2 (%3) %4")) + .arg(zoneName, addedCard.getName(), addedCard.getPrinting().getSet()->getCorrectedShortName(), + addedCard.getPrinting().getProperty("num")))); +} + void DeckEditorDeckDockWidget::actIncrement() { auto selectedRows = getSelectedCardNodes(); @@ -559,6 +647,11 @@ void DeckEditorDeckDockWidget::actRemoveCard() continue; } QModelIndex sourceIndex = proxy->mapToSource(index); + QString cardName = sourceIndex.sibling(sourceIndex.row(), 1).data().toString(); + + historyManager->save( + deckLoader->getDeckList()->createMemento(QString(tr("Removed \"%1\" (all copies)")).arg(cardName))); + deckModel->removeRow(sourceIndex.row(), sourceIndex.parent()); isModified = true; } @@ -579,9 +672,21 @@ void DeckEditorDeckDockWidget::offsetCountAtIndex(const QModelIndex &idx, int of QModelIndex sourceIndex = proxy->mapToSource(idx); const QModelIndex numberIndex = sourceIndex.sibling(sourceIndex.row(), 0); + const QModelIndex nameIndex = sourceIndex.sibling(sourceIndex.row(), 1); + + const QString cardName = deckModel->data(nameIndex, Qt::EditRole).toString(); const int count = deckModel->data(numberIndex, Qt::EditRole).toInt(); const int new_count = count + offset; + const auto reason = + QString(tr("%1 %2 × \"%3\" (%4)")) + .arg(offset > 0 ? tr("Added") : tr("Removed")) + .arg(qAbs(offset)) + .arg(cardName) + .arg(deckModel->data(sourceIndex.sibling(sourceIndex.row(), 4), Qt::DisplayRole).toString()); + + historyManager->save(deckLoader->getDeckList()->createMemento(reason)); + if (new_count <= 0) { deckModel->removeRow(sourceIndex.row(), sourceIndex.parent()); } else { diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h index d4703a5dc..b633bdaed 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h @@ -12,7 +12,9 @@ #include "../../key_signals.h" #include "../utility/custom_line_edit.h" #include "../visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.h" +#include "deck_list_history_manager_widget.h" #include "deck_list_style_proxy.h" +#include "libcockatrice/deck_list/deck_list_history_manager.h" #include #include @@ -53,6 +55,8 @@ public slots: void cleanDeck(); void updateBannerCardComboBox(); void setDeck(DeckLoader *_deck); + void syncDisplayWidgetsToModel(); + void sortDeckModelToDeckView(); DeckLoader *getDeckLoader(); DeckList *getDeckList(); void actIncrement(); @@ -61,6 +65,7 @@ public slots: void actDecrementSelection(); void actSwapCard(); void actRemoveCard(); + void onCardAboutToBeAdded(const ExactCard &card, const QString &zoneName); void offsetCountAtIndex(const QModelIndex &idx, int offset); signals: @@ -73,14 +78,18 @@ signals: private: AbstractTabDeckEditor *deckEditor; + DeckListHistoryManager *historyManager; + DeckListHistoryManagerWidget *historyManagerWidget; KeySignals deckViewKeySignals; QLabel *nameLabel; LineEditUnfocusable *nameEdit; + QTimer *nameDebounceTimer; SettingsButtonWidget *quickSettingsWidget; QCheckBox *showBannerCardCheckBox; QCheckBox *showTagsWidgetCheckBox; QLabel *commentsLabel; QTextEdit *commentsEdit; + QTimer *commentsDebounceTimer; QLabel *bannerCardLabel; DeckPreviewDeckTagsDisplayWidget *deckTagsDisplayWidget; QLabel *hashLabel1; @@ -104,6 +113,7 @@ private slots: void updateShowBannerCardComboBox(bool visible); void updateShowTagsWidget(bool visible); void syncBannerCardComboBoxSelectionWithDeck(); + void expandAll(); }; #endif // DECK_EDITOR_DECK_DOCK_WIDGET_H diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_list_history_manager_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_list_history_manager_widget.cpp new file mode 100644 index 000000000..c68934ea6 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_editor/deck_list_history_manager_widget.cpp @@ -0,0 +1,160 @@ +#include "deck_list_history_manager_widget.h" + +DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckListModel *_deckListModel, + DeckListStyleProxy *_styleProxy, + DeckListHistoryManager *manager, + QWidget *parent) + : QWidget(parent), deckListModel(_deckListModel), styleProxy(_styleProxy), historyManager(manager) +{ + layout = new QHBoxLayout(this); + + aUndo = new QAction(QString(), this); + aUndo->setIcon(QPixmap("theme:icons/arrow_undo")); + aUndo->setShortcut(QKeySequence::Undo); + aUndo->setShortcutContext(Qt::ApplicationShortcut); + connect(aUndo, &QAction::triggered, this, &DeckListHistoryManagerWidget::doUndo); + + undoButton = new QToolButton(this); + undoButton->setDefaultAction(aUndo); + + aRedo = new QAction(QString(), this); + aRedo->setIcon(QPixmap("theme:icons/arrow_redo")); + aRedo->setShortcut(QKeySequence::Redo); + aRedo->setShortcutContext(Qt::ApplicationShortcut); + connect(aRedo, &QAction::triggered, this, &DeckListHistoryManagerWidget::doRedo); + + redoButton = new QToolButton(this); + redoButton->setDefaultAction(aRedo); + + layout->addWidget(undoButton); + layout->addWidget(redoButton); + + historyButton = new SettingsButtonWidget(this); + historyButton->setButtonIcon(QPixmap("theme:icons/arrow_history")); + + historyLabel = new QLabel(this); + + historyList = new QListWidget(this); + + historyButton->addSettingsWidget(historyLabel); + historyButton->addSettingsWidget(historyList); + + layout->addWidget(historyButton); + + connect(historyList, &QListWidget::itemClicked, this, &DeckListHistoryManagerWidget::onListClicked); + + connect(historyManager, &DeckListHistoryManager::undoRedoStateChanged, this, + &DeckListHistoryManagerWidget::refreshList); + + refreshList(); + retranslateUi(); +} + +void DeckListHistoryManagerWidget::retranslateUi() +{ + undoButton->setToolTip(tr("Undo")); + redoButton->setToolTip(tr("Redo")); + historyButton->setToolTip(tr("Undo/Redo history")); + historyLabel->setText(tr("Click on an entry to revert to that point in the history.")); +} + +void DeckListHistoryManagerWidget::setDeckListModel(DeckListModel *_deckListModel) +{ + deckListModel = _deckListModel; +} + +void DeckListHistoryManagerWidget::refreshList() +{ + historyList->clear(); + + // Fill redo section first (oldest redo at top, newest redo closest to divider) + const auto redoStack = historyManager->getRedoStack(); + for (int i = 0; i < redoStack.size(); ++i) { // iterate forward + auto item = new QListWidgetItem(tr("[redo] ") + redoStack[i].getReason(), historyList); + item->setData(Qt::UserRole, QVariant("redo")); + item->setData(Qt::UserRole + 1, i); // index in redo stack + item->setForeground(Qt::gray); + historyList->addItem(item); + } + + // Divider + if (!historyManager->getUndoStack().isEmpty() && !historyManager->getRedoStack().isEmpty()) { + auto divider = new QListWidgetItem("──────────", historyList); + divider->setFlags(Qt::NoItemFlags); // not selectable + historyList->addItem(divider); + } + + // Fill undo section + const auto undoStack = historyManager->getUndoStack(); + for (int i = undoStack.size() - 1; i >= 0; --i) { + auto item = new QListWidgetItem(tr("[undo] ") + undoStack[i].getReason(), historyList); + item->setData(Qt::UserRole, QVariant("undo")); + item->setData(Qt::UserRole + 1, i); // index in undo stack + historyList->addItem(item); + } + + // Button enabled states + undoButton->setEnabled(historyManager->canUndo()); + redoButton->setEnabled(historyManager->canRedo()); +} + +void DeckListHistoryManagerWidget::doUndo() +{ + if (!historyManager->canUndo()) { + return; + } + + historyManager->undo(deckListModel->getDeckList()); + deckListModel->rebuildTree(); + emit deckListModel->layoutChanged(); + emit requestDisplayWidgetSync(); + + refreshList(); +} + +void DeckListHistoryManagerWidget::doRedo() +{ + if (!historyManager->canRedo()) { + return; + } + + historyManager->redo(deckListModel->getDeckList()); + deckListModel->rebuildTree(); + + emit deckListModel->layoutChanged(); + emit requestDisplayWidgetSync(); + + refreshList(); +} + +void DeckListHistoryManagerWidget::onListClicked(QListWidgetItem *item) +{ + // Ignore non-selectable items (like divider) + if (!(item->flags() & Qt::ItemIsSelectable)) { + return; + } + + const QString mode = item->data(Qt::UserRole).toString(); + int index = item->data(Qt::UserRole + 1).toInt(); + + if (mode == "redo") { + const auto redoStack = historyManager->getRedoStack(); + int steps = redoStack.size() - index; + for (int i = 0; i < steps; ++i) { + historyManager->redo(deckListModel->getDeckList()); + } + } else if (mode == "undo") { + const auto undoStack = historyManager->getUndoStack(); + int steps = undoStack.size() - 1 - index; + for (int i = 0; i < steps + 1; ++i) { + historyManager->undo(deckListModel->getDeckList()); + } + } + + deckListModel->rebuildTree(); + + emit deckListModel->layoutChanged(); + emit requestDisplayWidgetSync(); + + refreshList(); +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_list_history_manager_widget.h b/cockatrice/src/interface/widgets/deck_editor/deck_list_history_manager_widget.h new file mode 100644 index 000000000..23619b91b --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_editor/deck_list_history_manager_widget.h @@ -0,0 +1,58 @@ +#ifndef COCKATRICE_DECK_EDITOR_DECK_LIST_HISTORY_MANAGER_WIDGET_H +#define COCKATRICE_DECK_EDITOR_DECK_LIST_HISTORY_MANAGER_WIDGET_H + +#ifndef COCKATRICE_DECK_UNDO_WIDGET_H +#define COCKATRICE_DECK_UNDO_WIDGET_H + +#include "../quick_settings/settings_button_widget.h" +#include "deck_list_style_proxy.h" + +#include +#include +#include +#include +#include +#include +#include + +class DeckListHistoryManagerWidget : public QWidget +{ + Q_OBJECT + +signals: + void requestDisplayWidgetSync(); + +public slots: + void retranslateUi(); + +public: + explicit DeckListHistoryManagerWidget(DeckListModel *deckListModel, + DeckListStyleProxy *styleProxy, + DeckListHistoryManager *manager, + QWidget *parent = nullptr); + void setDeckListModel(DeckListModel *_deckListModel); + +private slots: + void refreshList(); + void onListClicked(QListWidgetItem *item); + void doUndo(); + void doRedo(); + +private: + DeckListModel *deckListModel; + DeckListStyleProxy *styleProxy; + DeckListHistoryManager *historyManager; + + QHBoxLayout *layout; + QAction *aUndo; + QToolButton *undoButton; + QAction *aRedo; + QToolButton *redoButton; + SettingsButtonWidget *historyButton; + QLabel *historyLabel; + QListWidget *historyList; +}; + +#endif // COCKATRICE_DECK_UNDO_WIDGET_H + +#endif // COCKATRICE_DECK_EDITOR_DECK_LIST_HISTORY_MANAGER_WIDGET_H diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_list_style_proxy.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_list_style_proxy.cpp index b697f130f..9c2265dfd 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_list_style_proxy.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_list_style_proxy.cpp @@ -7,9 +7,13 @@ QVariant DeckListStyleProxy::data(const QModelIndex &index, int role) const { + QModelIndex src = mapToSource(index); + if (!src.isValid()) + return {}; + QVariant value = QIdentityProxyModel::data(index, role); - const bool isCard = QIdentityProxyModel::data(index, DeckRoles::IsCardRole).toBool(); + bool isCard = src.data(DeckRoles::IsCardRole).toBool(); if (role == Qt::FontRole && !isCard) { QFont f; @@ -24,7 +28,7 @@ QVariant DeckListStyleProxy::data(const QModelIndex &index, int role) const int base = 255 - (index.row() % 2) * 30; return legal ? QBrush(QColor(base, base, base)) : QBrush(QColor(255, base / 3, base / 3)); } else { - int depth = QIdentityProxyModel::data(index, DeckRoles::DepthRole).toInt(); + int depth = src.data(DeckRoles::DepthRole).toInt(); int color = 90 + 60 * depth; return QBrush(QColor(color, 255, color)); } diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.cpp index 3385dd41e..2a276479f 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.cpp @@ -133,16 +133,16 @@ void DlgLoadDeckFromClipboard::actOK() /** * Creates the dialog window for the "Edit deck in clipboard" action * - * @param deckList The existing deck in the deck editor. Copies the instance + * @param _deckLoader The existing deck in the deck editor. Copies the instance * @param _annotated Whether to add annotations to the text that is loaded from the deck * @param parent The parent widget */ -DlgEditDeckInClipboard::DlgEditDeckInClipboard(const DeckLoader &deckList, bool _annotated, QWidget *parent) +DlgEditDeckInClipboard::DlgEditDeckInClipboard(DeckLoader *_deckLoader, bool _annotated, QWidget *parent) : AbstractDlgDeckTextEdit(parent), annotated(_annotated) { setWindowTitle(tr("Edit deck in clipboard")); - deckLoader = new DeckLoader(deckList); + deckLoader = new DeckLoader(this, _deckLoader->getDeckList()); deckLoader->setParent(this); DlgEditDeckInClipboard::actRefresh(); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.h b/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.h index d93a020cc..982840228 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.h +++ b/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.h @@ -88,7 +88,7 @@ private: bool annotated; public: - explicit DlgEditDeckInClipboard(const DeckLoader &deckList, bool _annotated, QWidget *parent = nullptr); + explicit DlgEditDeckInClipboard(DeckLoader *_deckLoader, bool _annotated, QWidget *parent = nullptr); [[nodiscard]] DeckLoader *getDeckList() const override { diff --git a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp index 65e1e4e71..ad68ecdbc 100644 --- a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp +++ b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp @@ -131,6 +131,8 @@ void AbstractTabDeckEditor::addCardHelper(const ExactCard &card, QString zoneNam if (card.getInfo().getIsToken()) zoneName = DECK_ZONE_TOKENS; + emit cardAboutToBeAdded(card, zoneName); + QModelIndex newCardIndex = deckDockWidget->deckModel->addCard(card, zoneName); deckDockWidget->deckView->clearSelection(); deckDockWidget->deckView->setCurrentIndex(newCardIndex); @@ -486,7 +488,7 @@ void AbstractTabDeckEditor::actLoadDeckFromClipboard() */ void AbstractTabDeckEditor::editDeckInClipboard(bool annotated) { - DlgEditDeckInClipboard dlg(*getDeckLoader(), annotated, this); + DlgEditDeckInClipboard dlg(getDeckLoader(), annotated, this); if (!dlg.exec()) return; diff --git a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h index c0a5518c6..86517de63 100644 --- a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h +++ b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h @@ -180,6 +180,7 @@ public slots: virtual void dockTopLevelChanged(bool topLevel) = 0; signals: + void cardAboutToBeAdded(const ExactCard &addedCard, const QString &zoneName); /** @brief Emitted when a deck should be opened in a new editor tab. */ void openDeckEditor(DeckLoader *deckLoader); diff --git a/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp b/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp index bf74b73fb..b483f0616 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp @@ -242,11 +242,11 @@ void TabDeckStorage::actOpenLocalDeck() continue; QString filePath = localDirModel->filePath(curLeft); - DeckLoader deckLoader(this); - if (!deckLoader.loadFromFile(filePath, DeckLoader::CockatriceFormat, true)) + auto deckLoader = new DeckLoader(this); + if (!deckLoader->loadFromFile(filePath, DeckLoader::CockatriceFormat, true)) continue; - emit openDeckEditor(&deckLoader); + emit openDeckEditor(deckLoader); } } diff --git a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp index 6d550be01..902b1f589 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp @@ -869,7 +869,7 @@ TabDeckEditor *TabSupervisor::addDeckEditorTab(DeckLoader *deckToOpen) { auto *tab = new TabDeckEditor(this); if (deckToOpen) - tab->openDeck(new DeckLoader(this, deckToOpen->getDeckList())); + tab->openDeck(deckToOpen); connect(tab, &AbstractTabDeckEditor::deckEditorClosing, this, &TabSupervisor::deckEditorClosed); connect(tab, &AbstractTabDeckEditor::openDeckEditor, this, &TabSupervisor::addDeckEditorTab); myAddTab(tab); @@ -882,7 +882,7 @@ TabDeckEditorVisual *TabSupervisor::addVisualDeckEditorTab(DeckLoader *deckToOpe { auto *tab = new TabDeckEditorVisual(this); if (deckToOpen) - tab->openDeck(new DeckLoader(this, deckToOpen->getDeckList())); + tab->openDeck(deckToOpen); connect(tab, &AbstractTabDeckEditor::deckEditorClosing, this, &TabSupervisor::deckEditorClosed); connect(tab, &AbstractTabDeckEditor::openDeckEditor, this, &TabSupervisor::addVisualDeckEditorTab); myAddTab(tab); diff --git a/cockatrice/src/interface/widgets/tabs/visual_deck_storage/tab_deck_storage_visual.cpp b/cockatrice/src/interface/widgets/tabs/visual_deck_storage/tab_deck_storage_visual.cpp index b4749f408..bfb498909 100644 --- a/cockatrice/src/interface/widgets/tabs/visual_deck_storage/tab_deck_storage_visual.cpp +++ b/cockatrice/src/interface/widgets/tabs/visual_deck_storage/tab_deck_storage_visual.cpp @@ -27,11 +27,11 @@ TabDeckStorageVisual::TabDeckStorageVisual(TabSupervisor *_tabSupervisor) void TabDeckStorageVisual::actOpenLocalDeck(const QString &filePath) { - DeckLoader deckLoader(this); - if (!deckLoader.loadFromFile(filePath, DeckLoader::getFormatFromName(filePath), true)) { + auto deckLoader = new DeckLoader(this); + if (!deckLoader->loadFromFile(filePath, DeckLoader::getFormatFromName(filePath), true)) { QMessageBox::critical(this, tr("Error"), tr("Could not open deck at %1").arg(filePath)); return; } - emit openDeckEditor(&deckLoader); + emit openDeckEditor(deckLoader); } diff --git a/doc/carddatabase_v4/cards.xsd b/doc/carddatabase_v4/cards.xsd index 9a879302b..92d30b94f 100644 --- a/doc/carddatabase_v4/cards.xsd +++ b/doc/carddatabase_v4/cards.xsd @@ -30,6 +30,7 @@ + diff --git a/libcockatrice_card/libcockatrice/card/card_info.cpp b/libcockatrice_card/libcockatrice/card/card_info.cpp index 58aa83848..3054a10cf 100644 --- a/libcockatrice_card/libcockatrice/card/card_info.cpp +++ b/libcockatrice_card/libcockatrice/card/card_info.cpp @@ -32,7 +32,7 @@ CardInfo::CardInfo(const QString &_name, { simpleName = CardInfo::simplifyName(name); - refreshCachedSetNames(); + refreshCachedSets(); } CardInfoPtr CardInfo::newInstance(const QString &_name) @@ -84,7 +84,7 @@ void CardInfo::addToSet(const CardSetPtr &_set, const PrintingInfo _info) setsToPrintings[_set->getShortName()].append(_info); } - refreshCachedSetNames(); + refreshCachedSets(); } void CardInfo::combineLegalities(const QVariantHash &props) @@ -98,6 +98,12 @@ void CardInfo::combineLegalities(const QVariantHash &props) } } +void CardInfo::refreshCachedSets() +{ + refreshCachedSetNames(); + refreshCachedAltNames(); +} + void CardInfo::refreshCachedSetNames() { QStringList setList; @@ -113,6 +119,21 @@ void CardInfo::refreshCachedSetNames() setsNames = setList.join(", "); } +void CardInfo::refreshCachedAltNames() +{ + altNames.clear(); + + // update the altNames with the flavorNames + for (const auto &printings : setsToPrintings) { + for (const auto &printing : printings) { + QString flavorName = printing.getFlavorName(); + if (!flavorName.isEmpty()) { + altNames.insert(flavorName); + } + } + } +} + QString CardInfo::simplifyName(const QString &name) { static const QRegularExpression spaceOrSplit("(\\s+|\\/\\/.*)"); diff --git a/libcockatrice_card/libcockatrice/card/card_info.h b/libcockatrice_card/libcockatrice/card/card_info.h index a52c0553a..ab1d51a49 100644 --- a/libcockatrice_card/libcockatrice/card/card_info.h +++ b/libcockatrice_card/libcockatrice/card/card_info.h @@ -77,8 +77,9 @@ private: QList reverseRelatedCards; ///< Cards that refer back to this card. QList reverseRelatedCardsToMe; ///< Cards that consider this card as related. SetToPrintingsMap setsToPrintings; ///< Mapping from set names to printing variations. - QString setsNames; ///< Cached, human-readable list of set names. UiAttributes uiAttributes; ///< Attributes that affect display and game logic + QString setsNames; ///< Cached, human-readable list of set names. + QSet altNames; ///< Cached set of alternate names, used when searching ///@} public: @@ -114,7 +115,8 @@ public: : QObject(other.parent()), name(other.name), simpleName(other.simpleName), text(other.text), isToken(other.isToken), properties(other.properties), relatedCards(other.relatedCards), reverseRelatedCards(other.reverseRelatedCards), reverseRelatedCardsToMe(other.reverseRelatedCardsToMe), - setsToPrintings(other.setsToPrintings), setsNames(other.setsNames), uiAttributes(other.uiAttributes) + setsToPrintings(other.setsToPrintings), uiAttributes(other.uiAttributes), setsNames(other.setsNames), + altNames(other.altNames) { } @@ -185,6 +187,10 @@ public: { return simpleName; } + const QSet &getAltNames() + { + return altNames; + }; const QString &getText() const { return text; @@ -303,11 +309,11 @@ public: void combineLegalities(const QVariantHash &props); /** - * @brief Refreshes the cached, human-readable list of set names. + * @brief Refreshes all cached fields that are calculated from the contained sets and printings. * - * Typically called after adding or modifying set memberships. + * Typically called after adding or modifying set memberships or printings. */ - void refreshCachedSetNames(); + void refreshCachedSets(); /** * @brief Simplifies a name for fuzzy matching. @@ -319,6 +325,21 @@ public: */ static QString simplifyName(const QString &name); +private: + /** + * @brief Refreshes the cached, human-readable list of set names. + * + * Typically called after adding or modifying set memberships. + */ + void refreshCachedSetNames(); + + /** + * @brief Refreshes the cached list of alt names for the card. + * + * Typically called after adding or modifying the contained printings. + */ + void refreshCachedAltNames(); + signals: /** * @brief Emitted when a pixmap for this card has been updated or finished loading. diff --git a/libcockatrice_card/libcockatrice/card/database/card_database.cpp b/libcockatrice_card/libcockatrice/card/database/card_database.cpp index 7e23b4663..57b3ce06e 100644 --- a/libcockatrice_card/libcockatrice/card/database/card_database.cpp +++ b/libcockatrice_card/libcockatrice/card/database/card_database.cpp @@ -194,8 +194,9 @@ void CardDatabase::markAllSetsAsKnown() void CardDatabase::notifyEnabledSetsChanged() { // refresh the list of cached set names - for (const CardInfoPtr &card : cards) - card->refreshCachedSetNames(); + for (const CardInfoPtr &card : cards) { + card->refreshCachedSets(); + } // inform the carddatabasemodels that they need to re-check their list of cards emit cardDatabaseEnabledSetsChanged(); diff --git a/libcockatrice_card/libcockatrice/card/printing/printing_info.cpp b/libcockatrice_card/libcockatrice/card/printing/printing_info.cpp index 9de563d78..340998565 100644 --- a/libcockatrice_card/libcockatrice/card/printing/printing_info.cpp +++ b/libcockatrice_card/libcockatrice/card/printing/printing_info.cpp @@ -12,4 +12,9 @@ PrintingInfo::PrintingInfo(const CardSetPtr &_set) : set(_set) QString PrintingInfo::getUuid() const { return properties.value("uuid").toString(); +} + +QString PrintingInfo::getFlavorName() const +{ + return properties.value("flavorName").toString(); } \ No newline at end of file diff --git a/libcockatrice_card/libcockatrice/card/printing/printing_info.h b/libcockatrice_card/libcockatrice/card/printing/printing_info.h index 37d7a15e2..4329ca2f4 100644 --- a/libcockatrice_card/libcockatrice/card/printing/printing_info.h +++ b/libcockatrice_card/libcockatrice/card/printing/printing_info.h @@ -110,6 +110,13 @@ public: * @return A string representing the providerID. */ QString getUuid() const; + + /** + * @brief Returns the flavorName for this printing. + * + * @return The flavorName, or empty if it isn't present. + */ + QString getFlavorName() const; }; #endif // COCKATRICE_PRINTING_INFO_H diff --git a/libcockatrice_deck_list/CMakeLists.txt b/libcockatrice_deck_list/CMakeLists.txt index 1e0511c01..d9b27354b 100644 --- a/libcockatrice_deck_list/CMakeLists.txt +++ b/libcockatrice_deck_list/CMakeLists.txt @@ -3,8 +3,12 @@ set(CMAKE_AUTOUIC ON) set(CMAKE_AUTORCC ON) set(HEADERS - libcockatrice/deck_list/abstract_deck_list_card_node.h libcockatrice/deck_list/abstract_deck_list_node.h - libcockatrice/deck_list/deck_list.h libcockatrice/deck_list/deck_list_card_node.h + libcockatrice/deck_list/abstract_deck_list_card_node.h + libcockatrice/deck_list/abstract_deck_list_node.h + libcockatrice/deck_list/deck_list.h + libcockatrice/deck_list/deck_list_card_node.h + libcockatrice/deck_list/deck_list_history_manager.h + libcockatrice/deck_list/deck_list_memento.h libcockatrice/deck_list/inner_deck_list_node.h ) @@ -16,9 +20,13 @@ endif() add_library( libcockatrice_deck_list STATIC - ${MOC_SOURCES} libcockatrice/deck_list/abstract_deck_list_card_node.cpp - libcockatrice/deck_list/abstract_deck_list_node.cpp libcockatrice/deck_list/deck_list.cpp - libcockatrice/deck_list/deck_list_card_node.cpp libcockatrice/deck_list/inner_deck_list_node.cpp + ${MOC_SOURCES} + libcockatrice/deck_list/abstract_deck_list_card_node.cpp + libcockatrice/deck_list/abstract_deck_list_node.cpp + libcockatrice/deck_list/deck_list.cpp + libcockatrice/deck_list/deck_list_card_node.cpp + libcockatrice/deck_list/deck_list_history_manager.cpp + libcockatrice/deck_list/inner_deck_list_node.cpp ) add_dependencies(libcockatrice_deck_list libcockatrice_protocol) diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp index a12c6fe0e..e26e55e08 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp @@ -2,6 +2,7 @@ #include "abstract_deck_list_node.h" #include "deck_list_card_node.h" +#include "deck_list_memento.h" #include "inner_deck_list_node.h" #include @@ -80,20 +81,6 @@ DeckList::DeckList() root = new InnerDecklistNode; } -// 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), - lastLoadedTimestamp(other.lastLoadedTimestamp), tags(other.tags), cachedDeckHash(other.cachedDeckHash) -{ - root = new InnerDecklistNode(other.getRoot()); - - QMapIterator spIterator(other.getSideboardPlans()); - while (spIterator.hasNext()) { - spIterator.next(); - sideboardPlans.insert(spIterator.key(), new SideboardPlan(spIterator.key(), spIterator.value()->getMoveList())); - } -} - DeckList::DeckList(const QString &nativeString) { root = new InnerDecklistNode; @@ -733,4 +720,15 @@ void DeckList::forEachCard(const std::function @@ -215,8 +216,9 @@ public slots: public: /// @brief Construct an empty deck. explicit DeckList(); - /// @brief Deep-copy constructor. - DeckList(const DeckList &other); + /// @brief Delete copy constructor. + DeckList(const DeckList &) = delete; + DeckList &operator=(const DeckList &) = delete; /// @brief Construct from a serialized native-format string. explicit DeckList(const QString &nativeString); ~DeckList() override; @@ -316,6 +318,8 @@ public: * @param func Function taking (zone node, card node). */ void forEachCard(const std::function &func) const; + DeckListMemento createMemento(const QString &reason) const; + void restoreMemento(const DeckListMemento &m); }; #endif diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_history_manager.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_history_manager.cpp new file mode 100644 index 000000000..83c9cc0bb --- /dev/null +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_history_manager.cpp @@ -0,0 +1,53 @@ +#include "deck_list_history_manager.h" + +void DeckListHistoryManager::save(const DeckListMemento &memento) +{ + undoStack.push(memento); + redoStack.clear(); + emit undoRedoStateChanged(); +} + +void DeckListHistoryManager::clear() +{ + undoStack.clear(); + redoStack.clear(); + emit undoRedoStateChanged(); +} + +void DeckListHistoryManager::undo(DeckList *deck) +{ + if (undoStack.isEmpty()) + return; + + // Peek at the memento we are going to restore + const DeckListMemento &mementoToRestore = undoStack.top(); + + // Save current state for redo + DeckListMemento currentState = deck->createMemento(mementoToRestore.getReason()); + redoStack.push(currentState); + + // Pop the last state from undo stack and restore it + DeckListMemento memento = undoStack.pop(); + deck->restoreMemento(memento); + + emit undoRedoStateChanged(); +} + +void DeckListHistoryManager::redo(DeckList *deck) +{ + if (redoStack.isEmpty()) + return; + + // Peek at the memento we are going to restore + const DeckListMemento &mementoToRestore = redoStack.top(); + + // Save current state for undo + DeckListMemento currentState = deck->createMemento(mementoToRestore.getReason()); + undoStack.push(currentState); + + // Pop the next state from redo stack and restore it + DeckListMemento memento = redoStack.pop(); + deck->restoreMemento(memento); + + emit undoRedoStateChanged(); +} diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_history_manager.h b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_history_manager.h new file mode 100644 index 000000000..f30c0affe --- /dev/null +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_history_manager.h @@ -0,0 +1,54 @@ +#ifndef COCKATRICE_DECK_LIST_HISTORY_MANAGER_H +#define COCKATRICE_DECK_LIST_HISTORY_MANAGER_H + +#include "deck_list.h" +#include "deck_list_memento.h" + +#include +#include + +class DeckListHistoryManager : public QObject +{ + Q_OBJECT + +signals: + void undoRedoStateChanged(); + +public: + explicit DeckListHistoryManager(QObject *parent = nullptr) : QObject(parent) + { + } + + void save(const DeckListMemento &memento); + + void clear(); + + bool canUndo() const + { + return !undoStack.isEmpty(); + } + + bool canRedo() const + { + return !redoStack.isEmpty(); + } + + void undo(DeckList *deck); + + void redo(DeckList *deck); + + QStack getRedoStack() const + { + return redoStack; + } + QStack getUndoStack() const + { + return undoStack; + } + +private: + QStack undoStack; + QStack redoStack; +}; + +#endif // COCKATRICE_DECK_LIST_HISTORY_MANAGER_H diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_memento.h b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_memento.h new file mode 100644 index 000000000..d2e36b804 --- /dev/null +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_memento.h @@ -0,0 +1,28 @@ +#ifndef COCKATRICE_DECK_LIST_MEMENTO_H +#define COCKATRICE_DECK_LIST_MEMENTO_H +#include + +class DeckListMemento +{ +public: + DeckListMemento() = default; + explicit DeckListMemento(const QString &memento, const QString &reason = QString()) + : memento(memento), reason(reason) + { + } + + QString getMemento() const + { + return memento; + } + QString getReason() const + { + return reason; + } + +private: + QString memento; + QString reason; +}; + +#endif // COCKATRICE_DECK_LIST_MEMENTO_H diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp index 52e3b678c..ed77b55ca 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp @@ -533,14 +533,14 @@ void DeckListModel::cleanList() } /** - * @param _deck The deck. Takes ownership of the object + * @param _deck The deck. */ void DeckListModel::setDeckList(DeckList *_deck) { - deckList->deleteLater(); - deckList = _deck; - deckList->setParent(this); - rebuildTree(); + if (deckList != _deck) { + deckList = _deck; + rebuildTree(); + } } QList DeckListModel::getCards() const diff --git a/oracle/src/oracleimporter.cpp b/oracle/src/oracleimporter.cpp index 41335d72d..f316e439d 100644 --- a/oracle/src/oracleimporter.cpp +++ b/oracle/src/oracleimporter.cpp @@ -293,6 +293,13 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList printingInfo.setProperty(xmlPropertyName, propertyValue); } + // handle flavorNames specially due to double-faced cards + QString faceFlavorName = getStringPropertyFromMap(card, "faceFlavorName"); + QString flavorName = !faceFlavorName.isEmpty() ? faceFlavorName : getStringPropertyFromMap(card, "flavorName"); + if (!flavorName.isEmpty()) { + printingInfo.setProperty("flavorName", flavorName); + } + // Identifiers for (auto i = identifierProperties.cbegin(), end = identifierProperties.cend(); i != end; ++i) { QString mtgjsonProperty = i.key();