From c84bd84f14a428d7fc87cc7338085f13e1d25f4d Mon Sep 17 00:00:00 2001 From: tetektoza Date: Thu, 23 Nov 2023 23:00:09 +0100 Subject: [PATCH] UndoStack: Introduce undostack Right now we're using QUndoStack which is from Qt library, although it has limited usage which we want to expand since we want to handle errors in a proper way without any hacks regarding QUndoStack, so these series of patches will introduce customly made Undostack, as well as Command class which will replace QUndoCommand/QUndoStack classes. --- CMakeLists.txt | 6 ++- source/celview.cpp | 22 +++++------ source/celview.h | 6 +-- source/command.cpp | 11 ++++++ source/command.h | 15 ++++++++ source/framecmds.cpp | 6 +-- source/framecmds.h | 15 ++++---- source/levelcelview.cpp | 25 +++++++------ source/levelcelview.h | 6 +-- source/mainwindow.cpp | 28 +++++++++++--- source/mainwindow.h | 7 +++- source/palettewidget.cpp | 43 ++++++++++------------ source/palettewidget.h | 19 +++++----- source/undostack.cpp | 79 ++++++++++++++++++++++++++++++++++++++++ source/undostack.h | 30 +++++++++++++++ 15 files changed, 238 insertions(+), 80 deletions(-) create mode 100644 source/command.cpp create mode 100644 source/command.h create mode 100644 source/undostack.cpp create mode 100644 source/undostack.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 7bb9d321c..eaaeb4277 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ set(CMAKE_AUTOUIC ON) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) -set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets) @@ -43,6 +43,10 @@ set(PROJECT_SOURCES source/openasdialog.cpp source/palettewidget.cpp source/settingsdialog.cpp + source/undostack.cpp + source/undostack.h + source/command.cpp + source/command.h ) if(${QT_VERSION_MAJOR} GREATER_EQUAL 6) diff --git a/source/celview.cpp b/source/celview.cpp index 7d8fab30a..a9eb9f794 100644 --- a/source/celview.cpp +++ b/source/celview.cpp @@ -70,7 +70,7 @@ void CelScene::contextMenuEvent(QContextMenuEvent *event) emit this->showContextMenu(event->globalPos()); } -CelView::CelView(std::shared_ptr us, QWidget *parent) +CelView::CelView(std::shared_ptr us, QWidget *parent) : QWidget(parent) , undoStack(us) , ui(new Ui::CelView()) @@ -214,17 +214,17 @@ void CelView::removeFrames(int startingIndex, int endingIndex) void CelView::sendAddFrameCmd(IMAGE_FILE_MODE mode, int index, const QString &imagefilePath) { - AddFrameCommand *command; + std::unique_ptr command; try { - command = new AddFrameCommand(mode, index, imagefilePath); + command = std::make_unique(mode, index, imagefilePath); } catch (...) { QMessageBox::critical(this, "Error", "Failed to read image file: " + imagefilePath); return; } // send a command to undostack, making adding frame undo/redoable - QObject::connect(command, &AddFrameCommand::added, this, &CelView::insertFrames); - QObject::connect(command, &AddFrameCommand::undoAdded, this, &CelView::removeFrames); + QObject::connect(dynamic_cast(command.get()), &AddFrameCommand::added, this, &CelView::insertFrames); + QObject::connect(dynamic_cast(command.get()), &AddFrameCommand::undoAdded, this, &CelView::removeFrames); undoStack->push(command); } @@ -247,9 +247,9 @@ void CelView::sendReplaceCurrentFrameCmd(const QString &imagefilePath) } // send a command to undostack, making replacing frame undo/redoable - ReplaceFrameCommand *command = new ReplaceFrameCommand(this->currentFrameIndex, image, this->gfx->getFrameImage(this->currentFrameIndex)); - QObject::connect(command, &ReplaceFrameCommand::replaced, this, &CelView::replaceCurrentFrame); - QObject::connect(command, &ReplaceFrameCommand::undoReplaced, this, &CelView::replaceCurrentFrame); + std::unique_ptr command = std::make_unique(this->currentFrameIndex, image, this->gfx->getFrameImage(this->currentFrameIndex)); + QObject::connect(dynamic_cast(command.get()), &ReplaceFrameCommand::replaced, this, &CelView::replaceCurrentFrame); + QObject::connect(dynamic_cast(command.get()), &ReplaceFrameCommand::undoReplaced, this, &CelView::replaceCurrentFrame); undoStack->push(command); } @@ -257,9 +257,9 @@ void CelView::sendReplaceCurrentFrameCmd(const QString &imagefilePath) void CelView::sendRemoveFrameCmd() { // send a command to undostack, making deleting frame undo/redoable - RemoveFrameCommand *command = new RemoveFrameCommand(this->currentFrameIndex, this->gfx->getFrameImage(this->currentFrameIndex)); - QObject::connect(command, &RemoveFrameCommand::removed, this, &CelView::removeCurrentFrame); - QObject::connect(command, &RemoveFrameCommand::inserted, this, &CelView::insertImageFile); + std::unique_ptr command = std::make_unique(this->currentFrameIndex, this->gfx->getFrameImage(this->currentFrameIndex)); + QObject::connect(dynamic_cast(command.get()), &RemoveFrameCommand::removed, this, &CelView::removeCurrentFrame); + QObject::connect(dynamic_cast(command.get()), &RemoveFrameCommand::inserted, this, &CelView::insertImageFile); this->undoStack->push(command); } diff --git a/source/celview.h b/source/celview.h index 9aa790694..5e3ac313e 100644 --- a/source/celview.h +++ b/source/celview.h @@ -9,13 +9,13 @@ #include #include #include -#include #include #include #include #include "d1gfx.h" +#include "undostack.h" #define CEL_SCENE_SPACING 8 @@ -50,7 +50,7 @@ class CelView : public QWidget { Q_OBJECT public: - explicit CelView(std::shared_ptr us, QWidget *parent = nullptr); + explicit CelView(std::shared_ptr us, QWidget *parent = nullptr); ~CelView(); void initialize(D1Gfx *gfx); @@ -111,7 +111,7 @@ private slots: std::stack removedGroupIdxs; // holds indexes of groups that have been removed, used for undo ops std::stack removedFrameGroupIdxs; // holds group indexes of frames that got removed, used for undo ops - std::shared_ptr undoStack; + std::shared_ptr undoStack; Ui::CelView *ui; CelScene *celScene; diff --git a/source/command.cpp b/source/command.cpp new file mode 100644 index 000000000..ced7a2a1a --- /dev/null +++ b/source/command.cpp @@ -0,0 +1,11 @@ +#include "command.h" + +void Command::setObsolete(bool isObsolete) +{ + m_isObsolete = isObsolete; +} + +bool Command::isObsolete() const +{ + return m_isObsolete; +} diff --git a/source/command.h b/source/command.h new file mode 100644 index 000000000..67d6ccba4 --- /dev/null +++ b/source/command.h @@ -0,0 +1,15 @@ +#pragma once + +class Command { +public: + virtual void undo() = 0; + virtual void redo() = 0; + + void setObsolete(bool isObsolete); + bool isObsolete() const; + + virtual ~Command() = default; + +private: + bool m_isObsolete = false; +}; diff --git a/source/framecmds.cpp b/source/framecmds.cpp index 91eace4c4..96a915040 100644 --- a/source/framecmds.cpp +++ b/source/framecmds.cpp @@ -5,7 +5,7 @@ #include "framecmds.h" #include "mainwindow.h" -RemoveFrameCommand::RemoveFrameCommand(int currentFrameIndex, const QImage img, QUndoCommand *parent) +RemoveFrameCommand::RemoveFrameCommand(int currentFrameIndex, const QImage img) : frameIndexToRevert(currentFrameIndex) , imgToRevert(img) { @@ -22,7 +22,7 @@ void RemoveFrameCommand::redo() emit this->removed(frameIndexToRevert); } -ReplaceFrameCommand::ReplaceFrameCommand(int currentFrameIndex, const QImage imgToReplace, const QImage imgToRestore, QUndoCommand *parent) +ReplaceFrameCommand::ReplaceFrameCommand(int currentFrameIndex, const QImage imgToReplace, const QImage imgToRestore) : frameIndexToReplace(currentFrameIndex) , imgToReplace(imgToReplace) , imgToRestore(imgToRestore) @@ -40,7 +40,7 @@ void ReplaceFrameCommand::redo() emit this->replaced(frameIndexToReplace, imgToReplace); } -AddFrameCommand::AddFrameCommand(IMAGE_FILE_MODE mode, int index, const QString imagefilePath, QUndoCommand *parent) +AddFrameCommand::AddFrameCommand(IMAGE_FILE_MODE mode, int index, const QString imagefilePath) : startingIndex(index) , mode(mode) { diff --git a/source/framecmds.h b/source/framecmds.h index 447ecd20a..c669bbcd6 100644 --- a/source/framecmds.h +++ b/source/framecmds.h @@ -1,14 +1,15 @@ #pragma once #include "celview.h" +#include "command.h" + #include -#include -class RemoveFrameCommand : public QObject, public QUndoCommand { +class RemoveFrameCommand : public QObject, public Command { Q_OBJECT public: - explicit RemoveFrameCommand(int currentFrameIndex, const QImage img, QUndoCommand *parent = nullptr); + explicit RemoveFrameCommand(int currentFrameIndex, const QImage img); ~RemoveFrameCommand() = default; void undo() override; @@ -23,11 +24,11 @@ class RemoveFrameCommand : public QObject, public QUndoCommand { int frameIndexToRevert = 0; }; -class ReplaceFrameCommand : public QObject, public QUndoCommand { +class ReplaceFrameCommand : public QObject, public Command { Q_OBJECT public: - explicit ReplaceFrameCommand(int currentFrameIndex, const QImage imgToReplace, const QImage imgToRestore, QUndoCommand *parent = nullptr); + explicit ReplaceFrameCommand(int currentFrameIndex, const QImage imgToReplace, const QImage imgToRestore); ~ReplaceFrameCommand() = default; void undo() override; @@ -43,11 +44,11 @@ class ReplaceFrameCommand : public QObject, public QUndoCommand { int frameIndexToReplace = 0; }; -class AddFrameCommand : public QObject, public QUndoCommand { +class AddFrameCommand : public QObject, public Command { Q_OBJECT public: - explicit AddFrameCommand(IMAGE_FILE_MODE mode, int index, const QString imagefilePath, QUndoCommand *parent = nullptr); + explicit AddFrameCommand(IMAGE_FILE_MODE mode, int index, const QString imagefilePath); ~AddFrameCommand() = default; void undo() override; diff --git a/source/levelcelview.cpp b/source/levelcelview.cpp index c5efa6304..b890437eb 100644 --- a/source/levelcelview.cpp +++ b/source/levelcelview.cpp @@ -16,10 +16,11 @@ #include #include #include +#include -LevelCelView::LevelCelView(std::shared_ptr us, QWidget *parent) +LevelCelView::LevelCelView(std::shared_ptr us, QWidget *parent) : QWidget(parent) - , undoStack(us) + , undoStack(std::move(us)) , ui(new Ui::LevelCelView()) , celScene(new CelScene(this)) { @@ -375,17 +376,17 @@ void LevelCelView::insertFrames(IMAGE_FILE_MODE mode, const QStringList &imagefi void LevelCelView::sendAddFrameCmd(IMAGE_FILE_MODE mode, int index, const QString &imagefilePath) { - AddFrameCommand *command; + std::unique_ptr command; try { - command = new AddFrameCommand(mode, index, imagefilePath); + command = std::make_unique(mode, index, imagefilePath); } catch (...) { QMessageBox::critical(this, "Error", "Failed to read image file: " + imagefilePath); return; } // send a command to undostack, making adding frame undo/redoable - QObject::connect(command, &AddFrameCommand::added, this, static_cast &images, IMAGE_FILE_MODE mode)>(&LevelCelView::insertFrames)); - QObject::connect(command, &AddFrameCommand::undoAdded, this, &LevelCelView::removeFrames); + QObject::connect(dynamic_cast(command.get()), &AddFrameCommand::added, this, static_cast &images, IMAGE_FILE_MODE mode)>(&LevelCelView::insertFrames)); + QObject::connect(dynamic_cast(command.get()), &AddFrameCommand::undoAdded, this, &LevelCelView::removeFrames); undoStack->push(command); } @@ -729,9 +730,9 @@ void LevelCelView::sendReplaceCurrentFrameCmd(const QString &imagefilePath) } // send a command to undostack, making replacing frame undo/redoable - ReplaceFrameCommand *command = new ReplaceFrameCommand(this->currentFrameIndex, image, this->gfx->getFrameImage(this->currentFrameIndex)); - QObject::connect(command, &ReplaceFrameCommand::replaced, this, &LevelCelView::replaceCurrentFrame); - QObject::connect(command, &ReplaceFrameCommand::undoReplaced, this, &LevelCelView::replaceCurrentFrame); + std::unique_ptr command = std::make_unique(this->currentFrameIndex, image, this->gfx->getFrameImage(this->currentFrameIndex)); + QObject::connect(dynamic_cast(command.get()), &ReplaceFrameCommand::replaced, this, &LevelCelView::replaceCurrentFrame); + QObject::connect(dynamic_cast(command.get()), &ReplaceFrameCommand::undoReplaced, this, &LevelCelView::replaceCurrentFrame); undoStack->push(command); } @@ -800,9 +801,9 @@ void LevelCelView::sendRemoveFrameCmd() } // send a command to undostack, making deleting frame undo/redoable - RemoveFrameCommand *command = new RemoveFrameCommand(this->currentFrameIndex, this->gfx->getFrameImage(this->currentFrameIndex)); - QObject::connect(command, &RemoveFrameCommand::removed, this, &LevelCelView::removeCurrentFrame); - QObject::connect(command, &RemoveFrameCommand::inserted, this, static_cast(&LevelCelView::insertFrame)); + std::unique_ptr command = std::make_unique(this->currentFrameIndex, this->gfx->getFrameImage(this->currentFrameIndex)); + QObject::connect(dynamic_cast(command.get()), &RemoveFrameCommand::removed, this, &LevelCelView::removeCurrentFrame); + QObject::connect(dynamic_cast(command.get()), &RemoveFrameCommand::inserted, this, static_cast(&LevelCelView::insertFrame)); this->undoStack->push(command); } diff --git a/source/levelcelview.h b/source/levelcelview.h index 0c95198c0..e8de771d2 100644 --- a/source/levelcelview.h +++ b/source/levelcelview.h @@ -9,7 +9,6 @@ #include #include #include -#include #include #include @@ -26,6 +25,7 @@ #include "leveltabframewidget.h" #include "leveltabsubtilewidget.h" #include "leveltabtilewidget.h" +#include "undostack.h" namespace Ui { class LevelCelView; @@ -43,7 +43,7 @@ class LevelCelView : public QWidget { Q_OBJECT public: - explicit LevelCelView(std::shared_ptr us, QWidget *parent = nullptr); + explicit LevelCelView(std::shared_ptr us, QWidget *parent = nullptr); ~LevelCelView(); void initialize(D1Gfx *gfx, D1Min *min, D1Til *til, D1Sol *sol, D1Amp *amp); @@ -160,7 +160,7 @@ private slots: void insertFrame(int index, QImage image); private: - std::shared_ptr undoStack; + std::shared_ptr undoStack; Ui::LevelCelView *ui; CelScene *celScene; LevelTabTileWidget *tabTileWidget = new LevelTabTileWidget(); diff --git a/source/mainwindow.cpp b/source/mainwindow.cpp index 0dfacf01a..6c292b18a 100644 --- a/source/mainwindow.cpp +++ b/source/mainwindow.cpp @@ -1,5 +1,6 @@ #include "mainwindow.h" +#include #include #include #include @@ -18,8 +19,6 @@ #include #include #include -#include -#include #include #include "config.h" @@ -47,11 +46,11 @@ MainWindow::MainWindow() this->ui->menuFile->insertMenu(firstFileAction, &this->newMenu); // Initialize 'Undo/Redo' of 'Edit - this->undoStack = std::make_shared(this); - this->undoAction = undoStack->createUndoAction(this, "Undo"); + this->undoStack = std::make_shared(); + this->undoAction = new QAction("Undo", this); this->undoAction->setShortcuts(QKeySequence::Undo); this->ui->menuEdit->addAction(this->undoAction); - this->redoAction = undoStack->createRedoAction(this, "Redo"); + this->redoAction = new QAction("Redo", this); this->redoAction->setShortcuts(QKeySequence::Redo); this->ui->menuEdit->addAction(this->redoAction); this->ui->menuEdit->addSeparator(); @@ -60,6 +59,10 @@ MainWindow::MainWindow() this->ui->menuEdit->addAction(this->redoAction); this->ui->menuEdit->addSeparator(); + // Bind Undo/Redo actions to appropriate slots + QObject::connect(this->undoAction, SIGNAL(triggered()), this, SLOT(actionUndo_triggered())); + QObject::connect(this->redoAction, SIGNAL(triggered()), this, SLOT(actionRedo_triggered())); + // Initialize 'Frame' submenu of 'Edit' this->frameMenu.setToolTipsVisible(true); this->frameMenu.addAction("Insert", this, SLOT(actionInsertFrame_triggered()))->setToolTip("Add new frames before the current one"); @@ -149,6 +152,7 @@ void MainWindow::updateWindow() this->palHits->update(); this->palWidget->refresh(); this->undoAction->setEnabled(this->undoStack->canUndo()); + this->redoAction->setEnabled(this->undoStack->canRedo()); // update menu options bool hasFrame = this->gfx->getFrameCount() != 0; @@ -1061,6 +1065,20 @@ void MainWindow::closeEvent(QCloseEvent *event) event->accept(); } +void MainWindow::actionUndo_triggered() +{ + this->undoStack->undo(); + this->undoAction->setEnabled(this->undoStack->canUndo()); + this->redoAction->setEnabled(this->undoStack->canRedo()); +} + +void MainWindow::actionRedo_triggered() +{ + this->undoStack->redo(); + this->undoAction->setEnabled(this->undoStack->canUndo()); + this->redoAction->setEnabled(this->undoStack->canRedo()); +} + void MainWindow::actionInsertFrame_triggered() { this->addFrames(false); diff --git a/source/mainwindow.h b/source/mainwindow.h index f68b3e35c..7b8399640 100644 --- a/source/mainwindow.h +++ b/source/mainwindow.h @@ -5,7 +5,6 @@ #include #include #include -#include #include @@ -22,6 +21,7 @@ #include "openasdialog.h" #include "palettewidget.h" #include "settingsdialog.h" +#include #define D1_GRAPHICS_TOOL_TITLE "Diablo 1 Graphics Tool" #define D1_GRAPHICS_TOOL_VERSION "1.0.1" @@ -84,6 +84,9 @@ class MainWindow : public QMainWindow { void addTiles(bool append); public slots: + void actionUndo_triggered(); + void actionRedo_triggered(); + void actionInsertFrame_triggered(); void actionAddFrame_triggered(); void actionReplaceFrame_triggered(); @@ -166,7 +169,7 @@ private slots: QMenu subtileMenu = QMenu("Tile"); QMenu tileMenu = QMenu("MegaTile"); - std::shared_ptr undoStack; + std::shared_ptr undoStack; QAction *undoAction; QAction *redoAction; diff --git a/source/palettewidget.cpp b/source/palettewidget.cpp index 0bb702d4b..17f9bdd07 100644 --- a/source/palettewidget.cpp +++ b/source/palettewidget.cpp @@ -22,9 +22,8 @@ enum class COLORFILTER_TYPE { Q_DECLARE_METATYPE(COLORFILTER_TYPE) -EditColorsCommand::EditColorsCommand(D1Pal *p, quint8 sci, quint8 eci, QColor nc, QColor ec, QUndoCommand *parent) - : QUndoCommand(parent) - , pal(p) +EditColorsCommand::EditColorsCommand(D1Pal *p, quint8 sci, quint8 eci, QColor nc, QColor ec) + : pal(p) , startColorIndex(sci) , endColorIndex(eci) , newColor(nc) @@ -72,9 +71,8 @@ void EditColorsCommand::redo() emit this->modified(); } -EditTranslationsCommand::EditTranslationsCommand(D1Trn *t, quint8 sci, quint8 eci, QList nt, QUndoCommand *parent) - : QUndoCommand(parent) - , trn(t) +EditTranslationsCommand::EditTranslationsCommand(D1Trn *t, quint8 sci, quint8 eci, QList nt) + : trn(t) , startColorIndex(sci) , endColorIndex(eci) , newTranslations(nt) @@ -110,9 +108,8 @@ void EditTranslationsCommand::redo() emit this->modified(); } -ClearTranslationsCommand::ClearTranslationsCommand(D1Trn *t, quint8 sci, quint8 eci, QUndoCommand *parent) - : QUndoCommand(parent) - , trn(t) +ClearTranslationsCommand::ClearTranslationsCommand(D1Trn *t, quint8 sci, quint8 eci) + : trn(t) , startColorIndex(sci) , endColorIndex(eci) { @@ -265,9 +262,9 @@ QPushButton *PaletteWidget::addButton(QStyle::StandardPixmap type, QString toolt return button; } -PaletteWidget::PaletteWidget(std::shared_ptr us, QString title) +PaletteWidget::PaletteWidget(std::shared_ptr us, QString title) : QWidget(nullptr) - , undoStack(us) + , undoStack(std::move(us)) , ui(new Ui::PaletteWidget()) , scene(new PaletteScene(this)) { @@ -449,9 +446,9 @@ void PaletteWidget::checkTranslationsSelection(QList indexes) // Build color editing command and connect it to the current palette widget // to update the PAL/TRN and CEL views when undo/redo is performed - EditTranslationsCommand *command = new EditTranslationsCommand( + std::unique_ptr command = std::make_unique( this->trn, this->selectedFirstColorIndex, this->selectedLastColorIndex, indexes); - QObject::connect(command, &EditTranslationsCommand::modified, this, &PaletteWidget::modify); + QObject::connect(dynamic_cast(command.get()), &EditTranslationsCommand::modified, this, &PaletteWidget::modify); this->undoStack->push(command); @@ -852,9 +849,9 @@ void PaletteWidget::on_colorLineEdit_returnPressed() // Build color editing command and connect it to the current palette widget // to update the PAL/TRN and CEL views when undo/redo is performed - EditColorsCommand *command = new EditColorsCommand( + std::unique_ptr command = std::make_unique( this->pal, this->selectedFirstColorIndex, this->selectedLastColorIndex, color, color); - QObject::connect(command, &EditColorsCommand::modified, this, &PaletteWidget::modify); + QObject::connect(dynamic_cast(command.get()), &EditColorsCommand::modified, this, &PaletteWidget::modify); this->undoStack->push(command); @@ -874,9 +871,9 @@ void PaletteWidget::on_colorPickPushButton_clicked() // Build color editing command and connect it to the current palette widget // to update the PAL/TRN and CEL views when undo/redo is performed - EditColorsCommand *command = new EditColorsCommand( + std::unique_ptr command = std::make_unique( this->pal, this->selectedFirstColorIndex, this->selectedLastColorIndex, color, colorEnd); - QObject::connect(command, &EditColorsCommand::modified, this, &PaletteWidget::modify); + QObject::connect(dynamic_cast(command.get()), &EditColorsCommand::modified, this, &PaletteWidget::modify); this->undoStack->push(command); } @@ -885,9 +882,9 @@ void PaletteWidget::on_colorClearPushButton_clicked() { // Build color editing command and connect it to the current palette widget // to update the PAL/TRN and CEL views when undo/redo is performed - auto *command = new EditColorsCommand( + std::unique_ptr command = std::make_unique( this->pal, this->selectedFirstColorIndex, this->selectedLastColorIndex, this->paletteDefaultColor, this->paletteDefaultColor); - QObject::connect(command, &EditColorsCommand::modified, this, &PaletteWidget::modify); + QObject::connect(dynamic_cast(command.get()), &EditColorsCommand::modified, this, &PaletteWidget::modify); this->undoStack->push(command); } @@ -903,9 +900,9 @@ void PaletteWidget::on_translationIndexLineEdit_returnPressed() // Build translation editing command and connect it to the current palette widget // to update the PAL/TRN and CEL views when undo/redo is performed - EditTranslationsCommand *command = new EditTranslationsCommand( + std::unique_ptr command = std::make_unique( this->trn, this->selectedFirstColorIndex, this->selectedLastColorIndex, newTranslations); - QObject::connect(command, &EditTranslationsCommand::modified, this, &PaletteWidget::modify); + QObject::connect(dynamic_cast(command.get()), &EditTranslationsCommand::modified, this, &PaletteWidget::modify); this->undoStack->push(command); @@ -926,9 +923,9 @@ void PaletteWidget::on_translationClearPushButton_clicked() { // Build translation clearing command and connect it to the current palette widget // to update the PAL/TRN and CEL views when undo/redo is performed - auto *command = new ClearTranslationsCommand( + std::unique_ptr command = std::make_unique( this->trn, this->selectedFirstColorIndex, this->selectedLastColorIndex); - QObject::connect(command, &ClearTranslationsCommand::modified, this, &PaletteWidget::modify); + QObject::connect(dynamic_cast(command.get()), &ClearTranslationsCommand::modified, this, &PaletteWidget::modify); this->undoStack->push(command); } diff --git a/source/palettewidget.h b/source/palettewidget.h index a30fd3868..b2d1beb14 100644 --- a/source/palettewidget.h +++ b/source/palettewidget.h @@ -6,8 +6,6 @@ #include #include #include -#include -#include #include #include "celview.h" @@ -15,6 +13,7 @@ #include "d1palhits.h" #include "d1trn.h" #include "levelcelview.h" +#include "undostack.h" #define PALETTE_WIDTH 192 #define PALETTE_COLORS_PER_LINE 16 @@ -36,11 +35,11 @@ class EditColorsCommand; class EditTranslationsCommand; } // namespace Ui -class EditColorsCommand : public QObject, public QUndoCommand { +class EditColorsCommand : public QObject, public Command { Q_OBJECT public: - explicit EditColorsCommand(D1Pal *, quint8, quint8, QColor, QColor, QUndoCommand *parent = nullptr); + explicit EditColorsCommand(D1Pal *, quint8, quint8, QColor, QColor); ~EditColorsCommand() = default; void undo() override; @@ -58,11 +57,11 @@ class EditColorsCommand : public QObject, public QUndoCommand { QColor endColor; }; -class EditTranslationsCommand : public QObject, public QUndoCommand { +class EditTranslationsCommand : public QObject, public Command { Q_OBJECT public: - explicit EditTranslationsCommand(D1Trn *, quint8, quint8, QList, QUndoCommand *parent = nullptr); + explicit EditTranslationsCommand(D1Trn *, quint8, quint8, QList); ~EditTranslationsCommand() = default; void undo() override; @@ -79,11 +78,11 @@ class EditTranslationsCommand : public QObject, public QUndoCommand { QList newTranslations; }; -class ClearTranslationsCommand : public QObject, public QUndoCommand { +class ClearTranslationsCommand : public QObject, public Command { Q_OBJECT public: - explicit ClearTranslationsCommand(D1Trn *, quint8, quint8, QUndoCommand *parent = nullptr); + explicit ClearTranslationsCommand(D1Trn *, quint8, quint8); ~ClearTranslationsCommand() = default; void undo() override; @@ -125,7 +124,7 @@ class PaletteWidget : public QWidget { Q_OBJECT public: - explicit PaletteWidget(std::shared_ptr undoStack, QString title); + explicit PaletteWidget(std::shared_ptr undoStack, QString title); ~PaletteWidget(); void setPal(D1Pal *p); @@ -216,7 +215,7 @@ private slots: void on_monsterTrnPushButton_clicked(); private: - std::shared_ptr undoStack; + std::shared_ptr undoStack; Ui::PaletteWidget *ui; bool isTrn; diff --git a/source/undostack.cpp b/source/undostack.cpp new file mode 100644 index 000000000..4078e5221 --- /dev/null +++ b/source/undostack.cpp @@ -0,0 +1,79 @@ +#include "undostack.h" + +void UndoStack::push(std::unique_ptr &cmd) +{ + try { + cmd->redo(); + } catch (...) { + } + + // Erase any command that was set to obsolete + std::erase_if(m_cmds, [](const auto &cmd) { return cmd->isObsolete(); }); + + // We also need to erase any command that was currently undo'd but not redo'd + if (!m_cmdsIndexes.empty()) { + for (const auto &idx : m_cmdsIndexes) { + m_cmds.erase(m_cmds.begin() + idx); + } + m_cmdsIndexes.clear(); + } + + m_cmds.push_back(std::move(cmd)); + m_canUndo = true; + m_canRedo = false; + m_undoPos = m_cmds.size() - 1; +} + +void UndoStack::undo() +{ + // Erase any command that was previously set as obsolete + std::erase_if(m_cmds, [](const auto &cmd) { return cmd->isObsolete(); }); + + if (m_undoPos == 0) + m_canUndo = false; + + m_cmds[m_undoPos]->undo(); + + // Save any index that is currently being undo'd, so we can keep + // track of the commands if any push happens - then the commands + // from corresponding indexes will get cleared + m_cmdsIndexes.push_back(m_undoPos); + + m_canRedo = true; + m_undoPos--; +} + +void UndoStack::redo() +{ + // erase any command that was previously set as obsolete + std::erase_if(m_cmds, [](const auto &cmd) { return cmd->isObsolete(); }); + + m_cmds[m_undoPos + 1]->redo(); + + // Erase any saved indexes from undo(), since we won't need to clear those + // commands anymore if any new command will get pushed onto the stack + m_cmdsIndexes.erase(m_cmdsIndexes.begin() + (m_undoPos + 1)); + + m_undoPos++; + m_canUndo = true; + + if (m_undoPos + 1 > m_cmds.size() - 1) + m_canRedo = false; +} + +bool UndoStack::canRedo() const +{ + return m_canRedo; +} + +bool UndoStack::canUndo() const +{ + return m_canUndo; +} + +void UndoStack::clear() +{ + m_undoPos = m_redoPos = 0; + m_canUndo = m_canRedo = false; + m_cmds.clear(); +} diff --git a/source/undostack.h b/source/undostack.h new file mode 100644 index 000000000..5a87970f9 --- /dev/null +++ b/source/undostack.h @@ -0,0 +1,30 @@ +#pragma once + +#include "command.h" + +#include +#include +#include + +class UndoStack { +public: + UndoStack() = default; + ~UndoStack() = default; + + void push(std::unique_ptr &cmd); + + void undo(); + void redo(); + [[nodiscard]] bool canUndo() const; + [[nodiscard]] bool canRedo() const; + + void clear(); + +private: + bool m_canUndo = false; + bool m_canRedo = false; + int8_t m_undoPos = 0; + int8_t m_redoPos = 0; + std::vector> m_cmds; // holds all the commands on the stack + std::vector m_cmdsIndexes; // holds indexes of the commands that are currently undo'd +};