[DeckEditor] Deck List History Manager. (#6340)

* [DeckEditor] Deck List History Manager.

Took 23 minutes

Took 17 minutes

* Add icons.

Took 2 minutes


Took 3 seconds

* Small fixes.

Took 12 minutes

* Style lint.

Took 48 seconds

* tr() things.

Took 5 minutes

* Add tooltips for buttons.

Took 3 minutes

* Add explanation label to history.

Took 3 minutes

* Refactor to .cpp, delegate undo/redo to manager, don't return memento

Took 8 minutes

* Clear history when setting deck.

Took 6 minutes

* Move to value based stacks.

Took 52 seconds

* Default constructor.

Took 31 seconds

Took 3 minutes

Took 4 minutes

Took 2 minutes

* Have it listen to deck editor additions.

Took 18 minutes

* Don't connect buttons *and* actions.

Took 2 minutes

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
BruebachL
2025-11-20 14:54:32 +01:00
committed by GitHub
parent c46f6d1178
commit 846f16ddaa
21 changed files with 612 additions and 41 deletions

View File

@@ -145,6 +145,7 @@ set(cockatrice_SOURCES
src/interface/widgets/deck_analytics/mana_base_widget.cpp src/interface/widgets/deck_analytics/mana_base_widget.cpp
src/interface/widgets/deck_analytics/mana_curve_widget.cpp src/interface/widgets/deck_analytics/mana_curve_widget.cpp
src/interface/widgets/deck_analytics/mana_devotion_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_card_info_dock_widget.cpp
src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp
src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp

View File

@@ -7,11 +7,14 @@
<file>resources/icons/arrow_bottom_green.svg</file> <file>resources/icons/arrow_bottom_green.svg</file>
<file>resources/icons/arrow_down_green.svg</file> <file>resources/icons/arrow_down_green.svg</file>
<file>resources/icons/arrow_history.svg</file>
<file>resources/icons/arrow_left_green.svg</file> <file>resources/icons/arrow_left_green.svg</file>
<file>resources/icons/arrow_redo.svg</file>
<file>resources/icons/arrow_right_blue.svg</file> <file>resources/icons/arrow_right_blue.svg</file>
<file>resources/icons/arrow_right_green.svg</file> <file>resources/icons/arrow_right_green.svg</file>
<file>resources/icons/arrow_top_green.svg</file> <file>resources/icons/arrow_top_green.svg</file>
<file>resources/icons/arrow_up_green.svg</file> <file>resources/icons/arrow_up_green.svg</file>
<file>resources/icons/arrow_undo.svg</file>
<file>resources/icons/clearsearch.svg</file> <file>resources/icons/clearsearch.svg</file>
<file>resources/icons/cogwheel.svg</file> <file>resources/icons/cogwheel.svg</file>
<file>resources/icons/conceded.svg</file> <file>resources/icons/conceded.svg</file>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
<title>
history
</title>
<path d="M9 6v5h.06l2.48 2.47 1.41-1.41L11 10.11V6H9z"/>
<path d="M10 1a9 9 0 0 0-7.85 13.35L.5 16H6v-5.5l-2.38 2.38A7 7 0 1 1 10 17v2a9 9 0 0 0 0-18z"/>
</svg>

After

Width:  |  Height:  |  Size: 332 B

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="windows-1252"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" x="0px"
y="0px" width="485.212px" height="485.212px" viewBox="0 0 485.212 485.212"
style="enable-background:new 0 0 485.212 485.212;" xml:space="preserve">
<g>
<path d="M242.607,424.559c-75.252,0-136.468-61.209-136.468-136.465c0-75.252,61.216-136.466,136.468-136.466v90.978 l151.629-121.302L242.607,0v90.978c-108.687,0-197.117,88.432-197.117,197.117c0,108.691,88.43,197.118,197.117,197.118 c108.687,0,197.114-88.427,197.114-197.118h-60.645C379.077,363.35,317.859,424.559,242.607,424.559z"/>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1018 B

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="windows-1252"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" x="0px"
y="0px" width="512px" height="512px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;"
xml:space="preserve">
<g>
<path d="M256,448c79.406,0,144-64.594,144-144s-64.594-144-144-144v96L96,128L256,0v96c114.688,0,208,93.313,208,208 c0,114.688-93.312,208-208,208c-114.687,0-208-93.312-208-208h64C112,383.406,176.594,448,256,448z"/>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 874 B

View File

@@ -28,6 +28,11 @@ DeckEditorDeckDockWidget::DeckEditorDeckDockWidget(AbstractTabDeckEditor *parent
void DeckEditorDeckDockWidget::createDeckDock() void DeckEditorDeckDockWidget::createDeckDock()
{ {
historyManager = new DeckListHistoryManager();
connect(deckEditor, &AbstractTabDeckEditor::cardAboutToBeAdded, this,
&DeckEditorDeckDockWidget::onCardAboutToBeAdded);
deckModel = new DeckListModel(this); deckModel = new DeckListModel(this);
deckModel->setObjectName("deckModel"); deckModel->setObjectName("deckModel");
connect(deckModel, &DeckListModel::deckHashChanged, this, &DeckEditorDeckDockWidget::updateHash); connect(deckModel, &DeckListModel::deckHashChanged, this, &DeckEditorDeckDockWidget::updateHash);
@@ -37,6 +42,10 @@ void DeckEditorDeckDockWidget::createDeckDock()
proxy = new DeckListStyleProxy(this); proxy = new DeckListStyleProxy(this);
proxy->setSourceModel(deckModel); proxy->setSourceModel(deckModel);
historyManagerWidget = new DeckListHistoryManagerWidget(deckModel, proxy, historyManager, this);
connect(historyManagerWidget, &DeckListHistoryManagerWidget::requestDisplayWidgetSync, this,
&DeckEditorDeckDockWidget::syncDisplayWidgetsToModel);
deckView = new QTreeView(); deckView = new QTreeView();
deckView->setObjectName("deckView"); deckView->setObjectName("deckView");
deckView->setModel(proxy); deckView->setModel(proxy);
@@ -65,7 +74,15 @@ void DeckEditorDeckDockWidget::createDeckDock()
nameEdit->setMaxLength(MAX_NAME_LENGTH); nameEdit->setMaxLength(MAX_NAME_LENGTH);
nameEdit->setObjectName("nameEdit"); nameEdit->setObjectName("nameEdit");
nameLabel->setBuddy(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); quickSettingsWidget = new SettingsButtonWidget(this);
@@ -95,7 +112,16 @@ void DeckEditorDeckDockWidget::createDeckDock()
commentsEdit->setMinimumHeight(nameEdit->minimumSizeHint().height()); commentsEdit->setMinimumHeight(nameEdit->minimumSizeHint().height());
commentsEdit->setObjectName("commentsEdit"); commentsEdit->setObjectName("commentsEdit");
commentsLabel->setBuddy(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 = new QLabel();
bannerCardLabel->setObjectName("bannerCardLabel"); bannerCardLabel->setObjectName("bannerCardLabel");
bannerCardLabel->setText(tr("Banner Card")); bannerCardLabel->setText(tr("Banner Card"));
@@ -109,7 +135,7 @@ void DeckEditorDeckDockWidget::createDeckDock()
&DeckEditorDeckDockWidget::setBannerCard); &DeckEditorDeckDockWidget::setBannerCard);
bannerCardComboBox->setHidden(!SettingsCache::instance().getDeckEditorBannerCardComboBoxVisible()); bannerCardComboBox->setHidden(!SettingsCache::instance().getDeckEditorBannerCardComboBoxVisible());
deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this, deckLoader); deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this, deckModel->getDeckList());
deckTagsDisplayWidget->setHidden(!SettingsCache::instance().getDeckEditorTagsWidgetVisible()); deckTagsDisplayWidget->setHidden(!SettingsCache::instance().getDeckEditorTagsWidgetVisible());
activeGroupCriteriaLabel = new QLabel(this); activeGroupCriteriaLabel = new QLabel(this);
@@ -187,7 +213,8 @@ void DeckEditorDeckDockWidget::createDeckDock()
lowerLayout->addWidget(tbDecrement, 0, 3); lowerLayout->addWidget(tbDecrement, 0, 3);
lowerLayout->addWidget(tbRemoveCard, 0, 4); lowerLayout->addWidget(tbRemoveCard, 0, 4);
lowerLayout->addWidget(tbSwapCard, 0, 5); 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 // Create widgets for both layouts to make splitter work correctly
auto *topWidget = new QWidget; auto *topWidget = new QWidget;
@@ -249,6 +276,8 @@ void DeckEditorDeckDockWidget::updateCard(const QModelIndex /*&current*/, const
void DeckEditorDeckDockWidget::updateName(const QString &name) 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); deckModel->getDeckList()->setName(name);
deckEditor->setModified(name.isEmpty()); deckEditor->setModified(name.isEmpty());
emit nameChanged(); emit nameChanged();
@@ -257,6 +286,11 @@ void DeckEditorDeckDockWidget::updateName(const QString &name)
void DeckEditorDeckDockWidget::updateComments() 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()); deckModel->getDeckList()->setComments(commentsEdit->toPlainText());
deckEditor->setModified(commentsEdit->toPlainText().isEmpty()); deckEditor->setModified(commentsEdit->toPlainText().isEmpty());
emit commentsChanged(); emit commentsChanged();
@@ -329,6 +363,7 @@ void DeckEditorDeckDockWidget::updateBannerCardComboBox()
void DeckEditorDeckDockWidget::setBannerCard(int /* changedIndex */) void DeckEditorDeckDockWidget::setBannerCard(int /* changedIndex */)
{ {
historyManager->save(deckLoader->getDeckList()->createMemento(tr("Banner card changed")));
auto [name, id] = bannerCardComboBox->currentData().value<QPair<QString, QString>>(); auto [name, id] = bannerCardComboBox->currentData().value<QPair<QString, QString>>();
deckModel->getDeckList()->setBannerCard({name, id}); deckModel->getDeckList()->setBannerCard({name, id});
deckEditor->setModified(true); deckEditor->setModified(true);
@@ -372,21 +407,40 @@ void DeckEditorDeckDockWidget::setDeck(DeckLoader *_deck)
connect(deckLoader, &DeckLoader::deckLoaded, deckModel, &DeckListModel::rebuildTree); connect(deckLoader, &DeckLoader::deckLoaded, deckModel, &DeckListModel::rebuildTree);
connect(deckLoader->getDeckList(), &DeckList::deckHashChanged, deckModel, &DeckListModel::deckHashChanged); connect(deckLoader->getDeckList(), &DeckList::deckHashChanged, deckModel, &DeckListModel::deckHashChanged);
nameEdit->setText(deckModel->getDeckList()->getName()); historyManager->clear();
commentsEdit->setText(deckModel->getDeckList()->getComments()); historyManagerWidget->setDeckListModel(deckModel);
syncBannerCardComboBoxSelectionWithDeck(); syncDisplayWidgetsToModel();
updateBannerCardComboBox();
updateHash();
deckModel->sort(deckView->header()->sortIndicatorSection(), deckView->header()->sortIndicatorOrder());
deckView->expandAll();
deckView->expandAll();
deckTagsDisplayWidget->connectDeckList();
emit deckChanged(); 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() DeckLoader *DeckEditorDeckDockWidget::getDeckLoader()
{ {
return deckLoader; return deckLoader;
@@ -412,7 +466,7 @@ void DeckEditorDeckDockWidget::cleanDeck()
emit deckModified(); emit deckModified();
emit deckChanged(); emit deckChanged();
updateBannerCardComboBox(); updateBannerCardComboBox();
deckTagsDisplayWidget->connectDeckList(); deckTagsDisplayWidget->connectDeckList(deckModel->getDeckList());
} }
void DeckEditorDeckDockWidget::recursiveExpand(const QModelIndex &index) void DeckEditorDeckDockWidget::recursiveExpand(const QModelIndex &index)
@@ -422,6 +476,12 @@ void DeckEditorDeckDockWidget::recursiveExpand(const QModelIndex &index)
deckView->expand(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. * 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. * 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; 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() void DeckEditorDeckDockWidget::actIncrement()
{ {
auto selectedRows = getSelectedCardNodes(); auto selectedRows = getSelectedCardNodes();
@@ -559,6 +627,11 @@ void DeckEditorDeckDockWidget::actRemoveCard()
continue; continue;
} }
QModelIndex sourceIndex = proxy->mapToSource(index); 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()); deckModel->removeRow(sourceIndex.row(), sourceIndex.parent());
isModified = true; isModified = true;
} }
@@ -579,9 +652,21 @@ void DeckEditorDeckDockWidget::offsetCountAtIndex(const QModelIndex &idx, int of
QModelIndex sourceIndex = proxy->mapToSource(idx); QModelIndex sourceIndex = proxy->mapToSource(idx);
const QModelIndex numberIndex = sourceIndex.sibling(sourceIndex.row(), 0); 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 count = deckModel->data(numberIndex, Qt::EditRole).toInt();
const int new_count = count + offset; 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) { if (new_count <= 0) {
deckModel->removeRow(sourceIndex.row(), sourceIndex.parent()); deckModel->removeRow(sourceIndex.row(), sourceIndex.parent());
} else { } else {

View File

@@ -12,7 +12,9 @@
#include "../../key_signals.h" #include "../../key_signals.h"
#include "../utility/custom_line_edit.h" #include "../utility/custom_line_edit.h"
#include "../visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.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 "deck_list_style_proxy.h"
#include "libcockatrice/deck_list/deck_list_history_manager.h"
#include <QComboBox> #include <QComboBox>
#include <QDockWidget> #include <QDockWidget>
@@ -53,6 +55,8 @@ public slots:
void cleanDeck(); void cleanDeck();
void updateBannerCardComboBox(); void updateBannerCardComboBox();
void setDeck(DeckLoader *_deck); void setDeck(DeckLoader *_deck);
void syncDisplayWidgetsToModel();
void sortDeckModelToDeckView();
DeckLoader *getDeckLoader(); DeckLoader *getDeckLoader();
DeckList *getDeckList(); DeckList *getDeckList();
void actIncrement(); void actIncrement();
@@ -61,6 +65,7 @@ public slots:
void actDecrementSelection(); void actDecrementSelection();
void actSwapCard(); void actSwapCard();
void actRemoveCard(); void actRemoveCard();
void onCardAboutToBeAdded(const ExactCard &card, const QString &zoneName);
void offsetCountAtIndex(const QModelIndex &idx, int offset); void offsetCountAtIndex(const QModelIndex &idx, int offset);
signals: signals:
@@ -73,14 +78,18 @@ signals:
private: private:
AbstractTabDeckEditor *deckEditor; AbstractTabDeckEditor *deckEditor;
DeckListHistoryManager *historyManager;
DeckListHistoryManagerWidget *historyManagerWidget;
KeySignals deckViewKeySignals; KeySignals deckViewKeySignals;
QLabel *nameLabel; QLabel *nameLabel;
LineEditUnfocusable *nameEdit; LineEditUnfocusable *nameEdit;
QTimer *nameDebounceTimer;
SettingsButtonWidget *quickSettingsWidget; SettingsButtonWidget *quickSettingsWidget;
QCheckBox *showBannerCardCheckBox; QCheckBox *showBannerCardCheckBox;
QCheckBox *showTagsWidgetCheckBox; QCheckBox *showTagsWidgetCheckBox;
QLabel *commentsLabel; QLabel *commentsLabel;
QTextEdit *commentsEdit; QTextEdit *commentsEdit;
QTimer *commentsDebounceTimer;
QLabel *bannerCardLabel; QLabel *bannerCardLabel;
DeckPreviewDeckTagsDisplayWidget *deckTagsDisplayWidget; DeckPreviewDeckTagsDisplayWidget *deckTagsDisplayWidget;
QLabel *hashLabel1; QLabel *hashLabel1;
@@ -104,6 +113,7 @@ private slots:
void updateShowBannerCardComboBox(bool visible); void updateShowBannerCardComboBox(bool visible);
void updateShowTagsWidget(bool visible); void updateShowTagsWidget(bool visible);
void syncBannerCardComboBoxSelectionWithDeck(); void syncBannerCardComboBoxSelectionWithDeck();
void expandAll();
}; };
#endif // DECK_EDITOR_DECK_DOCK_WIDGET_H #endif // DECK_EDITOR_DECK_DOCK_WIDGET_H

View File

@@ -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();
}

View File

@@ -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 <QAction>
#include <QHBoxLayout>
#include <QListWidget>
#include <QToolButton>
#include <QWidget>
#include <libcockatrice/deck_list/deck_list_history_manager.h>
#include <libcockatrice/models/deck_list/deck_list_model.h>
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

View File

@@ -7,9 +7,13 @@
QVariant DeckListStyleProxy::data(const QModelIndex &index, int role) const QVariant DeckListStyleProxy::data(const QModelIndex &index, int role) const
{ {
QModelIndex src = mapToSource(index);
if (!src.isValid())
return {};
QVariant value = QIdentityProxyModel::data(index, role); 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) { if (role == Qt::FontRole && !isCard) {
QFont f; QFont f;
@@ -24,7 +28,7 @@ QVariant DeckListStyleProxy::data(const QModelIndex &index, int role) const
int base = 255 - (index.row() % 2) * 30; int base = 255 - (index.row() % 2) * 30;
return legal ? QBrush(QColor(base, base, base)) : QBrush(QColor(255, base / 3, base / 3)); return legal ? QBrush(QColor(base, base, base)) : QBrush(QColor(255, base / 3, base / 3));
} else { } else {
int depth = QIdentityProxyModel::data(index, DeckRoles::DepthRole).toInt(); int depth = src.data(DeckRoles::DepthRole).toInt();
int color = 90 + 60 * depth; int color = 90 + 60 * depth;
return QBrush(QColor(color, 255, color)); return QBrush(QColor(color, 255, color));
} }

View File

@@ -131,6 +131,8 @@ void AbstractTabDeckEditor::addCardHelper(const ExactCard &card, QString zoneNam
if (card.getInfo().getIsToken()) if (card.getInfo().getIsToken())
zoneName = DECK_ZONE_TOKENS; zoneName = DECK_ZONE_TOKENS;
emit cardAboutToBeAdded(card, zoneName);
QModelIndex newCardIndex = deckDockWidget->deckModel->addCard(card, zoneName); QModelIndex newCardIndex = deckDockWidget->deckModel->addCard(card, zoneName);
deckDockWidget->deckView->clearSelection(); deckDockWidget->deckView->clearSelection();
deckDockWidget->deckView->setCurrentIndex(newCardIndex); deckDockWidget->deckView->setCurrentIndex(newCardIndex);

View File

@@ -180,6 +180,7 @@ public slots:
virtual void dockTopLevelChanged(bool topLevel) = 0; virtual void dockTopLevelChanged(bool topLevel) = 0;
signals: signals:
void cardAboutToBeAdded(const ExactCard &addedCard, const QString &zoneName);
/** @brief Emitted when a deck should be opened in a new editor tab. */ /** @brief Emitted when a deck should be opened in a new editor tab. */
void openDeckEditor(DeckLoader *deckLoader); void openDeckEditor(DeckLoader *deckLoader);

View File

@@ -14,8 +14,8 @@
#include <QLabel> #include <QLabel>
#include <QMessageBox> #include <QMessageBox>
DeckPreviewDeckTagsDisplayWidget::DeckPreviewDeckTagsDisplayWidget(QWidget *_parent, DeckLoader *_deckLoader) DeckPreviewDeckTagsDisplayWidget::DeckPreviewDeckTagsDisplayWidget(QWidget *_parent, DeckList *_deckList)
: QWidget(_parent), deckLoader(_deckLoader) : QWidget(_parent), deckList(nullptr)
{ {
setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
@@ -28,20 +28,21 @@ DeckPreviewDeckTagsDisplayWidget::DeckPreviewDeckTagsDisplayWidget(QWidget *_par
flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded); flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded);
connectDeckList(); if (_deckList) {
connectDeckList(_deckList);
}
layout->addWidget(flowWidget); layout->addWidget(flowWidget);
} }
void DeckPreviewDeckTagsDisplayWidget::connectDeckList() void DeckPreviewDeckTagsDisplayWidget::connectDeckList(DeckList *_deckList)
{ {
if (deckLoader) { if (deckList) {
disconnect(deckLoader->getDeckList(), &DeckList::deckTagsChanged, this, disconnect(deckList, &DeckList::deckTagsChanged, this, &DeckPreviewDeckTagsDisplayWidget::refreshTags);
&DeckPreviewDeckTagsDisplayWidget::refreshTags);
} }
connect(deckLoader->getDeckList(), &DeckList::deckTagsChanged, this, deckList = _deckList;
&DeckPreviewDeckTagsDisplayWidget::refreshTags); connect(deckList, &DeckList::deckTagsChanged, this, &DeckPreviewDeckTagsDisplayWidget::refreshTags);
refreshTags(); refreshTags();
} }
@@ -50,7 +51,7 @@ void DeckPreviewDeckTagsDisplayWidget::refreshTags()
{ {
flowWidget->clearLayout(); flowWidget->clearLayout();
for (const QString &tag : deckLoader->getDeckList()->getTags()) { for (const QString &tag : deckList->getTags()) {
flowWidget->addWidget(new DeckPreviewTagDisplayWidget(this, tag)); flowWidget->addWidget(new DeckPreviewTagDisplayWidget(this, tag));
} }
@@ -97,7 +98,7 @@ void DeckPreviewDeckTagsDisplayWidget::openTagEditDlg()
if (qobject_cast<DeckPreviewWidget *>(parentWidget())) { if (qobject_cast<DeckPreviewWidget *>(parentWidget())) {
auto *deckPreviewWidget = qobject_cast<DeckPreviewWidget *>(parentWidget()); auto *deckPreviewWidget = qobject_cast<DeckPreviewWidget *>(parentWidget());
QStringList knownTags = deckPreviewWidget->visualDeckStorageWidget->tagFilterWidget->getAllKnownTags(); QStringList knownTags = deckPreviewWidget->visualDeckStorageWidget->tagFilterWidget->getAllKnownTags();
QStringList activeTags = deckLoader->getDeckList()->getTags(); QStringList activeTags = deckList->getTags();
bool canAddTags = true; bool canAddTags = true;
@@ -148,7 +149,7 @@ void DeckPreviewDeckTagsDisplayWidget::openTagEditDlg()
DeckPreviewTagDialog dialog(knownTags, activeTags); DeckPreviewTagDialog dialog(knownTags, activeTags);
if (dialog.exec() == QDialog::Accepted) { if (dialog.exec() == QDialog::Accepted) {
QStringList updatedTags = dialog.getActiveTags(); QStringList updatedTags = dialog.getActiveTags();
deckLoader->getDeckList()->setTags(updatedTags); deckList->setTags(updatedTags);
deckPreviewWidget->deckLoader->saveToFile(deckPreviewWidget->filePath, DeckLoader::CockatriceFormat); deckPreviewWidget->deckLoader->saveToFile(deckPreviewWidget->filePath, DeckLoader::CockatriceFormat);
} }
} }
@@ -174,12 +175,12 @@ void DeckPreviewDeckTagsDisplayWidget::openTagEditDlg()
knownTags.removeDuplicates(); knownTags.removeDuplicates();
} }
QStringList activeTags = deckLoader->getDeckList()->getTags(); QStringList activeTags = deckList->getTags();
DeckPreviewTagDialog dialog(knownTags, activeTags); DeckPreviewTagDialog dialog(knownTags, activeTags);
if (dialog.exec() == QDialog::Accepted) { if (dialog.exec() == QDialog::Accepted) {
QStringList updatedTags = dialog.getActiveTags(); QStringList updatedTags = dialog.getActiveTags();
deckLoader->getDeckList()->setTags(updatedTags); deckList->setTags(updatedTags);
deckEditor->setModified(true); deckEditor->setModified(true);
} }
} }

View File

@@ -20,10 +20,10 @@ class DeckPreviewDeckTagsDisplayWidget : public QWidget
Q_OBJECT Q_OBJECT
public: public:
explicit DeckPreviewDeckTagsDisplayWidget(QWidget *_parent, DeckLoader *_deckLoader); explicit DeckPreviewDeckTagsDisplayWidget(QWidget *_parent, DeckList *_deckList);
void connectDeckList(); void connectDeckList(DeckList *_deckList);
void refreshTags(); void refreshTags();
DeckLoader *deckLoader; DeckList *deckList;
FlowWidget *flowWidget; FlowWidget *flowWidget;
public slots: public slots:

View File

@@ -82,7 +82,7 @@ void DeckPreviewWidget::initializeUi(const bool deckLoadSuccess)
setFilePath(deckLoader->getLastFileName()); setFilePath(deckLoader->getLastFileName());
colorIdentityWidget = new ColorIdentityWidget(this, getColorIdentity()); colorIdentityWidget = new ColorIdentityWidget(this, getColorIdentity());
deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this, deckLoader); deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this, deckLoader->getDeckList());
bannerCardLabel = new QLabel(this); bannerCardLabel = new QLabel(this);
bannerCardLabel->setObjectName("bannerCardLabel"); bannerCardLabel->setObjectName("bannerCardLabel");

View File

@@ -3,8 +3,12 @@ set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON) set(CMAKE_AUTORCC ON)
set(HEADERS set(HEADERS
libcockatrice/deck_list/abstract_deck_list_card_node.h libcockatrice/deck_list/abstract_deck_list_node.h libcockatrice/deck_list/abstract_deck_list_card_node.h
libcockatrice/deck_list/deck_list.h libcockatrice/deck_list/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 libcockatrice/deck_list/inner_deck_list_node.h
) )
@@ -16,9 +20,13 @@ endif()
add_library( add_library(
libcockatrice_deck_list STATIC libcockatrice_deck_list STATIC
${MOC_SOURCES} libcockatrice/deck_list/abstract_deck_list_card_node.cpp ${MOC_SOURCES}
libcockatrice/deck_list/abstract_deck_list_node.cpp libcockatrice/deck_list/deck_list.cpp libcockatrice/deck_list/abstract_deck_list_card_node.cpp
libcockatrice/deck_list/deck_list_card_node.cpp libcockatrice/deck_list/inner_deck_list_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) add_dependencies(libcockatrice_deck_list libcockatrice_protocol)

View File

@@ -2,6 +2,7 @@
#include "abstract_deck_list_node.h" #include "abstract_deck_list_node.h"
#include "deck_list_card_node.h" #include "deck_list_card_node.h"
#include "deck_list_memento.h"
#include "inner_deck_list_node.h" #include "inner_deck_list_node.h"
#include <QCryptographicHash> #include <QCryptographicHash>
@@ -720,3 +721,14 @@ void DeckList::forEachCard(const std::function<void(InnerDecklistNode *, Decklis
} }
} }
} }
DeckListMemento DeckList::createMemento(const QString &reason) const
{
return DeckListMemento(writeToString_Native(), reason);
}
void DeckList::restoreMemento(const DeckListMemento &m)
{
cleanList();
loadFromString_Native(m.getMemento());
}

View File

@@ -10,6 +10,7 @@
#ifndef DECKLIST_H #ifndef DECKLIST_H
#define DECKLIST_H #define DECKLIST_H
#include "deck_list_memento.h"
#include "inner_deck_list_node.h" #include "inner_deck_list_node.h"
#include <QMap> #include <QMap>
@@ -317,6 +318,8 @@ public:
* @param func Function taking (zone node, card node). * @param func Function taking (zone node, card node).
*/ */
void forEachCard(const std::function<void(InnerDecklistNode *, DecklistCardNode *)> &func) const; void forEachCard(const std::function<void(InnerDecklistNode *, DecklistCardNode *)> &func) const;
DeckListMemento createMemento(const QString &reason) const;
void restoreMemento(const DeckListMemento &m);
}; };
#endif #endif

View File

@@ -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();
}

View File

@@ -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 <QObject>
#include <QStack>
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<DeckListMemento> getRedoStack() const
{
return redoStack;
}
QStack<DeckListMemento> getUndoStack() const
{
return undoStack;
}
private:
QStack<DeckListMemento> undoStack;
QStack<DeckListMemento> redoStack;
};
#endif // COCKATRICE_DECK_LIST_HISTORY_MANAGER_H

View File

@@ -0,0 +1,28 @@
#ifndef COCKATRICE_DECK_LIST_MEMENTO_H
#define COCKATRICE_DECK_LIST_MEMENTO_H
#include <QString>
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