diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 03a64ba3a..522300346 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -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/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp index 11818a912..18b05f96b 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 @@ -28,6 +28,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 +42,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 +74,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 +112,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")); @@ -109,7 +135,7 @@ void DeckEditorDeckDockWidget::createDeckDock() &DeckEditorDeckDockWidget::setBannerCard); bannerCardComboBox->setHidden(!SettingsCache::instance().getDeckEditorBannerCardComboBoxVisible()); - deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this, deckLoader); + deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this, deckModel->getDeckList()); deckTagsDisplayWidget->setHidden(!SettingsCache::instance().getDeckEditorTagsWidgetVisible()); activeGroupCriteriaLabel = new QLabel(this); @@ -187,7 +213,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 +276,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 +286,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(); @@ -329,6 +363,7 @@ void DeckEditorDeckDockWidget::updateBannerCardComboBox() 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 +407,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(); + 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; @@ -412,7 +466,7 @@ void DeckEditorDeckDockWidget::cleanDeck() emit deckModified(); emit deckChanged(); updateBannerCardComboBox(); - deckTagsDisplayWidget->connectDeckList(); + deckTagsDisplayWidget->connectDeckList(deckModel->getDeckList()); } void DeckEditorDeckDockWidget::recursiveExpand(const QModelIndex &index) @@ -422,6 +476,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 +501,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 +627,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 +652,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/tabs/abstract_tab_deck_editor.cpp b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp index 8a26b3a6a..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); 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/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.cpp index e5cbc0adb..eb105697f 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.cpp @@ -14,8 +14,8 @@ #include #include -DeckPreviewDeckTagsDisplayWidget::DeckPreviewDeckTagsDisplayWidget(QWidget *_parent, DeckLoader *_deckLoader) - : QWidget(_parent), deckLoader(_deckLoader) +DeckPreviewDeckTagsDisplayWidget::DeckPreviewDeckTagsDisplayWidget(QWidget *_parent, DeckList *_deckList) + : QWidget(_parent), deckList(nullptr) { setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); @@ -28,20 +28,21 @@ DeckPreviewDeckTagsDisplayWidget::DeckPreviewDeckTagsDisplayWidget(QWidget *_par flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded); - connectDeckList(); + if (_deckList) { + connectDeckList(_deckList); + } layout->addWidget(flowWidget); } -void DeckPreviewDeckTagsDisplayWidget::connectDeckList() +void DeckPreviewDeckTagsDisplayWidget::connectDeckList(DeckList *_deckList) { - if (deckLoader) { - disconnect(deckLoader->getDeckList(), &DeckList::deckTagsChanged, this, - &DeckPreviewDeckTagsDisplayWidget::refreshTags); + if (deckList) { + disconnect(deckList, &DeckList::deckTagsChanged, this, &DeckPreviewDeckTagsDisplayWidget::refreshTags); } - connect(deckLoader->getDeckList(), &DeckList::deckTagsChanged, this, - &DeckPreviewDeckTagsDisplayWidget::refreshTags); + deckList = _deckList; + connect(deckList, &DeckList::deckTagsChanged, this, &DeckPreviewDeckTagsDisplayWidget::refreshTags); refreshTags(); } @@ -50,7 +51,7 @@ void DeckPreviewDeckTagsDisplayWidget::refreshTags() { flowWidget->clearLayout(); - for (const QString &tag : deckLoader->getDeckList()->getTags()) { + for (const QString &tag : deckList->getTags()) { flowWidget->addWidget(new DeckPreviewTagDisplayWidget(this, tag)); } @@ -97,7 +98,7 @@ void DeckPreviewDeckTagsDisplayWidget::openTagEditDlg() if (qobject_cast(parentWidget())) { auto *deckPreviewWidget = qobject_cast(parentWidget()); QStringList knownTags = deckPreviewWidget->visualDeckStorageWidget->tagFilterWidget->getAllKnownTags(); - QStringList activeTags = deckLoader->getDeckList()->getTags(); + QStringList activeTags = deckList->getTags(); bool canAddTags = true; @@ -148,7 +149,7 @@ void DeckPreviewDeckTagsDisplayWidget::openTagEditDlg() DeckPreviewTagDialog dialog(knownTags, activeTags); if (dialog.exec() == QDialog::Accepted) { QStringList updatedTags = dialog.getActiveTags(); - deckLoader->getDeckList()->setTags(updatedTags); + deckList->setTags(updatedTags); deckPreviewWidget->deckLoader->saveToFile(deckPreviewWidget->filePath, DeckLoader::CockatriceFormat); } } @@ -174,12 +175,12 @@ void DeckPreviewDeckTagsDisplayWidget::openTagEditDlg() knownTags.removeDuplicates(); } - QStringList activeTags = deckLoader->getDeckList()->getTags(); + QStringList activeTags = deckList->getTags(); DeckPreviewTagDialog dialog(knownTags, activeTags); if (dialog.exec() == QDialog::Accepted) { QStringList updatedTags = dialog.getActiveTags(); - deckLoader->getDeckList()->setTags(updatedTags); + deckList->setTags(updatedTags); deckEditor->setModified(true); } } diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.h b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.h index 645c769bd..4270e38e5 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.h +++ b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.h @@ -20,10 +20,10 @@ class DeckPreviewDeckTagsDisplayWidget : public QWidget Q_OBJECT public: - explicit DeckPreviewDeckTagsDisplayWidget(QWidget *_parent, DeckLoader *_deckLoader); - void connectDeckList(); + explicit DeckPreviewDeckTagsDisplayWidget(QWidget *_parent, DeckList *_deckList); + void connectDeckList(DeckList *_deckList); void refreshTags(); - DeckLoader *deckLoader; + DeckList *deckList; FlowWidget *flowWidget; public slots: diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp index ba111100d..8b2bec10b 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp @@ -82,7 +82,7 @@ void DeckPreviewWidget::initializeUi(const bool deckLoadSuccess) setFilePath(deckLoader->getLastFileName()); colorIdentityWidget = new ColorIdentityWidget(this, getColorIdentity()); - deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this, deckLoader); + deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this, deckLoader->getDeckList()); bannerCardLabel = new QLabel(this); bannerCardLabel->setObjectName("bannerCardLabel"); 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 be26c51a3..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 @@ -719,4 +720,15 @@ void DeckList::forEachCard(const std::function @@ -317,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