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 @@
+
+
\ 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