From a253a9895b09dea635e83ebd710b1fffe285b3b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= <d_muel20@uni-muenster.de> Date: Sat, 9 Nov 2019 14:58:28 +0100 Subject: [PATCH] * Added undo functionality to the image editor (not for all operations, though) --- Grinder/Grinder.pro | 24 ++- Grinder/Version.h | 4 +- Grinder/controller/ImageEditorController.cpp | 77 ++++++- Grinder/controller/ImageEditorController.h | 8 +- Grinder/image/LayerPixels.cpp | 4 + Grinder/image/LayerPixelsData.h | 1 + Grinder/res/Grinder.qrc | 2 + Grinder/res/Resources.h | 2 + Grinder/res/icons/redo-arrow.png | Bin 0 -> 11014 bytes Grinder/ui/image/ImageEditorUndoCommand.h | 32 +++ .../ui/image/ImageEditorUndoCommand.impl.h | 26 +++ .../ui/image/ImageEditorUndoCommandBase.cpp | 40 ++++ Grinder/ui/image/ImageEditorUndoCommandBase.h | 51 +++++ Grinder/ui/image/ImageEditorUndoCommandIDs.h | 20 ++ Grinder/ui/image/ImageEditorUndoStack.cpp | 50 +++++ Grinder/ui/image/ImageEditorUndoStack.h | 52 +++++ Grinder/ui/image/ImageEditorUndoStack.impl.h | 21 ++ Grinder/ui/image/ImageEditorWidget.cpp | 31 ++- Grinder/ui/image/ImageEditorWidget.h | 5 + .../commands/ConvertDraftItemsUndoCommand.cpp | 92 +++++++++ .../commands/ConvertDraftItemsUndoCommand.h | 40 ++++ .../commands/ConvertPixelsUndoCommand.cpp | 77 +++++++ .../image/commands/ConvertPixelsUndoCommand.h | 41 ++++ .../commands/CreateDraftItemUndoCommand.cpp | 68 ++++++ .../commands/CreateDraftItemUndoCommand.h | 39 ++++ Grinder/ui/image/commands/LayerUndoCommand.h | 38 ++++ .../ui/image/commands/LayerUndoCommand.impl.h | 39 ++++ .../image/commands/PaintPixelsUndoCommand.cpp | 41 ++++ .../image/commands/PaintPixelsUndoCommand.h | 34 +++ .../commands/RemoveDraftItemUndoCommand.cpp | 62 ++++++ .../commands/RemoveDraftItemUndoCommand.h | 34 +++ Grinder/ui/image/tools/PaintbrushTool.cpp | 195 ++++++++++-------- Grinder/ui/image/tools/PaintbrushTool.h | 106 +++++----- 33 files changed, 1202 insertions(+), 154 deletions(-) create mode 100644 Grinder/res/icons/redo-arrow.png create mode 100644 Grinder/ui/image/ImageEditorUndoCommand.h create mode 100644 Grinder/ui/image/ImageEditorUndoCommand.impl.h create mode 100644 Grinder/ui/image/ImageEditorUndoCommandBase.cpp create mode 100644 Grinder/ui/image/ImageEditorUndoCommandBase.h create mode 100644 Grinder/ui/image/ImageEditorUndoCommandIDs.h create mode 100644 Grinder/ui/image/ImageEditorUndoStack.cpp create mode 100644 Grinder/ui/image/ImageEditorUndoStack.h create mode 100644 Grinder/ui/image/ImageEditorUndoStack.impl.h create mode 100644 Grinder/ui/image/commands/ConvertDraftItemsUndoCommand.cpp create mode 100644 Grinder/ui/image/commands/ConvertDraftItemsUndoCommand.h create mode 100644 Grinder/ui/image/commands/ConvertPixelsUndoCommand.cpp create mode 100644 Grinder/ui/image/commands/ConvertPixelsUndoCommand.h create mode 100644 Grinder/ui/image/commands/CreateDraftItemUndoCommand.cpp create mode 100644 Grinder/ui/image/commands/CreateDraftItemUndoCommand.h create mode 100644 Grinder/ui/image/commands/LayerUndoCommand.h create mode 100644 Grinder/ui/image/commands/LayerUndoCommand.impl.h create mode 100644 Grinder/ui/image/commands/PaintPixelsUndoCommand.cpp create mode 100644 Grinder/ui/image/commands/PaintPixelsUndoCommand.h create mode 100644 Grinder/ui/image/commands/RemoveDraftItemUndoCommand.cpp create mode 100644 Grinder/ui/image/commands/RemoveDraftItemUndoCommand.h diff --git a/Grinder/Grinder.pro b/Grinder/Grinder.pro index aff0084..514f634 100644 --- a/Grinder/Grinder.pro +++ b/Grinder/Grinder.pro @@ -477,7 +477,14 @@ SOURCES += \ ml/tasks/InferenceTileQueue.cpp \ ml/tasks/MachineLearningTrainingTask.cpp \ ml/tasks/MachineLearningInferenceTask.cpp \ - ui/image/LayerSettingsDialog.cpp + ui/image/LayerSettingsDialog.cpp \ + ui/image/ImageEditorUndoStack.cpp \ + ui/image/commands/CreateDraftItemUndoCommand.cpp \ + ui/image/commands/RemoveDraftItemUndoCommand.cpp \ + ui/image/commands/PaintPixelsUndoCommand.cpp \ + ui/image/ImageEditorUndoCommandBase.cpp \ + ui/image/commands/ConvertDraftItemsUndoCommand.cpp \ + ui/image/commands/ConvertPixelsUndoCommand.cpp HEADERS += \ ui/mainwnd/GrinderWindow.h \ @@ -1039,7 +1046,20 @@ HEADERS += \ ml/tasks/InferenceTileQueue.h \ ml/tasks/MachineLearningTrainingTask.h \ ml/tasks/MachineLearningInferenceTask.h \ - ui/image/LayerSettingsDialog.h + ui/image/LayerSettingsDialog.h \ + ui/image/ImageEditorUndoCommand.h \ + ui/image/ImageEditorUndoStack.h \ + ui/image/ImageEditorUndoStack.impl.h \ + ui/image/ImageEditorUndoCommandIDs.h \ + ui/image/ImageEditorUndoCommand.impl.h \ + ui/image/commands/CreateDraftItemUndoCommand.h \ + ui/image/commands/RemoveDraftItemUndoCommand.h \ + ui/image/commands/PaintPixelsUndoCommand.h \ + ui/image/commands/LayerUndoCommand.h \ + ui/image/commands/LayerUndoCommand.impl.h \ + ui/image/ImageEditorUndoCommandBase.h \ + ui/image/commands/ConvertDraftItemsUndoCommand.h \ + ui/image/commands/ConvertPixelsUndoCommand.h FORMS += \ ui/mainwnd/GrinderWindow.ui \ diff --git a/Grinder/Version.h b/Grinder/Version.h index b68f9ad..c353176 100644 --- a/Grinder/Version.h +++ b/Grinder/Version.h @@ -10,14 +10,14 @@ #define GRNDR_INFO_TITLE "Grinder" #define GRNDR_INFO_COPYRIGHT "Copyright (c) WWU Muenster" -#define GRNDR_INFO_DATE "28.10.2019" +#define GRNDR_INFO_DATE "9.11.2019" #define GRNDR_INFO_COMPANY "WWU Muenster" #define GRNDR_INFO_WEBSITE "http://www.uni-muenster.de" #define GRNDR_VERSION_MAJOR 0 #define GRNDR_VERSION_MINOR 16 #define GRNDR_VERSION_REVISION 0 -#define GRNDR_VERSION_BUILD 407 +#define GRNDR_VERSION_BUILD 412 namespace grndr { diff --git a/Grinder/controller/ImageEditorController.cpp b/Grinder/controller/ImageEditorController.cpp index d837f73..b6901e1 100644 --- a/Grinder/controller/ImageEditorController.cpp +++ b/Grinder/controller/ImageEditorController.cpp @@ -17,11 +17,18 @@ #include "util/MathUtils.h" #include "res/Resources.h" +#include "ui/image/commands/CreateDraftItemUndoCommand.h" +#include "ui/image/commands/RemoveDraftItemUndoCommand.h" +#include "ui/image/commands/PaintPixelsUndoCommand.h" +#include "ui/image/commands/ConvertDraftItemsUndoCommand.h" +#include "ui/image/commands/ConvertPixelsUndoCommand.h" + #include <QtConcurrent/QtConcurrent> Q_DECLARE_METATYPE(std::shared_ptr<Layer>) -ImageEditorController::ImageEditorController(ImageEditor* imageEditor) : GenericController("Image editor"), ImageEditorComponent(imageEditor) +ImageEditorController::ImageEditorController(ImageEditor* imageEditor) : GenericController("Image editor"), ImageEditorComponent(imageEditor), + _undoStack{_imageEditor} { if (!imageEditor) throw std::invalid_argument{_EXCPT("imageEditor may not be null")}; @@ -346,7 +353,12 @@ std::shared_ptr<DraftItem> ImageEditorController::createDraftItem(DraftItemType if (_restrictions.maxDraftItemsPerLayer > 0 && layer->draftItems().size() >= _restrictions.maxDraftItemsPerLayer) throw LayerException{layer, _EXCPT(QString{"Layer '%1' has already reached its item limit of %2"}.arg(layer->getName()).arg(_restrictions.maxDraftItemsPerLayer))}; - return layer->createDraftItem(type); + auto draftItem = layer->createDraftItem(type); + + if (draftItem) + _undoStack.push<CreateDraftItemUndoCommand>(draftItem, layer); + + return draftItem; }, type, layer); } else @@ -359,7 +371,9 @@ void ImageEditorController::removeDraftItem(const DraftItem* item) const if (item) { - callControllerFunction("Removing a draft item", [](const DraftItem* item, Layer* layer) { + callControllerFunction("Removing a draft item", [this](const DraftItem* item, Layer* layer) { + _undoStack.push<RemoveDraftItemUndoCommand>(item, layer); + layer->removeDraftItem(item); return true; }, item, layer); @@ -430,6 +444,9 @@ void ImageEditorController::convertSelectedNodesToPixels() const } } + _undoStack.push<ConvertDraftItemsUndoCommand>(itemNodes); + _undoStack.rejectNewCommands(); // Do not push the upcoming undos for item removals onto the stack + LongOperation opConvertItems{"Converting items to pixels", static_cast<unsigned int>(itemNodes.size())}; unsigned int count = 1; @@ -443,6 +460,7 @@ void ImageEditorController::convertSelectedNodesToPixels() const // Remove all selected draft items after converting them to pixels removeSelectedNodes(); + _undoStack.acceptNewCommands(); return true; }); @@ -457,7 +475,7 @@ void ImageEditorController::convertLayerPixelsToItems(Layer* layer) if (layer && checkLayerEditability(layer)) { - callControllerFunction("Converting pixels to items", [this](Layer* layer) { + callControllerFunction("Converting pixels to items", [this](Layer* layer) { LongOperation opConvertPixels{"Converting pixels to items"}; _activeScene->clearSelection(); @@ -487,12 +505,15 @@ void ImageEditorController::convertLayerPixelsToItems(Layer* layer) if (_restrictions.maxDraftItemsPerLayer == 0 || layer->draftItems().size() + regionCount < _restrictions.maxDraftItemsPerLayer) { + _undoStack.rejectNewCommands(); // Do not push the upcoming undos onto the stack + opConvertPixels.setStatusMessage("Processing regions"); activateDefaultEditorTool(); // Create draft items for each region of each color LongOperation opCreateItems{"Creating draft items", regionCount}; + std::vector<std::shared_ptr<DraftItem>> draftItems; unsigned int count = 1; for (auto& it : regionMap) @@ -521,12 +542,17 @@ void ImageEditorController::convertLayerPixelsToItems(Layer* layer) // Select the created draft items if (auto draftItemNode = _activeScene->findDraftItemNode(draftItem.get())) draftItemNode->setSelected(true); + + draftItems.push_back(draftItem); } else break; } } + _undoStack.acceptNewCommands(); + _undoStack.push<ConvertPixelsUndoCommand>(draftItems, std::vector<Layer*>{layer}); + // Clear all pixels after converting them to items layer->layerPixels().data().clear(); } @@ -580,13 +606,14 @@ void ImageEditorController::convertSelectedPixelsToItem(Layer* layer, const Laye } break; - } + } - for (auto layer : layers) - clearSelectedPixels(layer, selection); + _undoStack.rejectNewCommands(); // Do not push the upcoming undos onto the stack activateDefaultEditorTool(); + std::vector<std::shared_ptr<DraftItem>> draftItems; + if (auto draftItem = createDraftItem(DraftItemType::Pixels, layer)) { if (auto pixelsItem = dynamic_cast<PixelsDraftItem*>(draftItem.get())) @@ -606,7 +633,20 @@ void ImageEditorController::convertSelectedPixelsToItem(Layer* layer, const Laye // Select the created draft items if (auto draftItemNode = _activeScene->findDraftItemNode(draftItem.get())) draftItemNode->setSelected(true); + + draftItems.push_back(draftItem); } + + _undoStack.acceptNewCommands(); + _undoStack.push<ConvertPixelsUndoCommand>(draftItems, layers); + + // Clear all pixels after converting them to items + _undoStack.rejectNewCommands(); // Prevent an undo for this operation + + for (auto layer : layers) + clearSelectedPixels(layer, selection); + + _undoStack.acceptNewCommands(); } return true; @@ -625,11 +665,12 @@ void ImageEditorController::paintPixels(Layer* layer, const std::list<QPoint>& p if (layer && checkLayerEditability(layer)) { - callControllerFunction("Painting pixels", [](Layer* layer, const std::list<QPoint>& pixels, QColor color) { - auto layerSize = layer->getLayerSize(); + callControllerFunction("Painting pixels", [this](Layer* layer, const std::list<QPoint>& pixels, QColor color) { + _undoStack.push<PaintPixelsUndoCommand>(layer); layer->layerPixels().data().beginPainting(); + auto layerSize = layer->getLayerSize(); QPoint min{std::numeric_limits<int>::max(), std::numeric_limits<int>::max()}; QPoint max{std::numeric_limits<int>::min(), std::numeric_limits<int>::min()}; @@ -677,6 +718,8 @@ void ImageEditorController::floodFillPixels(Layer* layer, QPoint seedPos, QColor if (layer && checkLayerEditability(layer)) { callControllerFunction("Filling pixels", [this](Layer* layer, QPoint seedPos, QColor color, float tolerance, bool perceivedDifference) { + _undoStack.push<PaintPixelsUndoCommand>(layer, "fill pixels"); + layer->layerPixels().data().beginPainting(); switch (_imageEditor->environment().getSamplingMode()) @@ -705,7 +748,9 @@ void ImageEditorController::fillSelectedPixels(Layer* layer, QColor color, const if (layer && checkLayerEditability(layer)) { - callControllerFunction("Filling selection", [](Layer* layer, QColor color, const LayerPixelsData::Selection& selection) { + callControllerFunction("Filling selection", [this](Layer* layer, QColor color, const LayerPixelsData::Selection& selection) { + _undoStack.push<PaintPixelsUndoCommand>(layer, "fill pixels"); + layer->layerPixels().data().beginPainting(); layer->layerPixels().data().fill(selection, color); layer->layerPixels().data().endPainting(); @@ -721,7 +766,9 @@ void ImageEditorController::clearSelectedPixels(Layer* layer, const LayerPixelsD if (layer && checkLayerEditability(layer)) { - callControllerFunction("Clearing pixels", [](Layer* layer, const LayerPixelsData::Selection& selection) { + callControllerFunction("Clearing pixels", [this](Layer* layer, const LayerPixelsData::Selection& selection) { + _undoStack.push<PaintPixelsUndoCommand>(layer, "clear pixels"); + layer->layerPixels().data().beginPainting(); layer->layerPixels().data().clear(selection); layer->layerPixels().data().endPainting(); @@ -751,6 +798,8 @@ void ImageEditorController::copySelectedNodes() const void ImageEditorController::pasteSelectedNodes() { callControllerFunction("Pasting draft items", [this]() { + _undoStack.beginMerging(); + // Create draft items for each object in the clipboard auto draftItems = grinder()->clipboardManager().deserialize<DraftItem>(DraftItemVector::Serialization_Element, [this](const SettingsContainer& settings) { DraftItemType type = settings(DraftItem::Serialization_Value_Type, DraftItemType::Undefined).toString(); @@ -758,6 +807,8 @@ void ImageEditorController::pasteSelectedNodes() return createDraftItem(type).get(); }); + _undoStack.endMerging(); + // Select the pasted items if (!draftItems.empty()) { @@ -785,6 +836,8 @@ void ImageEditorController::removeSelectedNodes() const if (_activeScene) { // Remove all selected draft items + _undoStack.beginMerging(); + for (auto node : _activeScene->getNodes<DraftItemNode>(true)) { if (auto item = node->draftItem().lock()) @@ -793,6 +846,8 @@ void ImageEditorController::removeSelectedNodes() const removeDraftItem(item.get()); } } + + _undoStack.endMerging(); } } diff --git a/Grinder/controller/ImageEditorController.h b/Grinder/controller/ImageEditorController.h index e21183b..8a8a862 100644 --- a/Grinder/controller/ImageEditorController.h +++ b/Grinder/controller/ImageEditorController.h @@ -9,6 +9,7 @@ #include "GenericController.h" #include "ui/image/ImageEditorComponent.h" #include "ui/image/ImageEditorScene.h" +#include "ui/image/ImageEditorUndoStack.h" namespace grndr { @@ -51,7 +52,10 @@ namespace grndr void switchLayer(Layer* layer); Layer* activeLayer() { return _activeLayer; } - const Layer* activeLayer() const { return _activeLayer; } + const Layer* activeLayer() const { return _activeLayer; } + + ImageEditorUndoStack& undoStack() { return _undoStack; } + const ImageEditorUndoStack& undoStack() const { return _undoStack; } public: void copyImageBuild(const ImageBuild* imageBuild = nullptr) const; @@ -145,6 +149,8 @@ namespace grndr std::unique_ptr<ImageEditorScene> _activeScene; Layer* _activeLayer{nullptr}; + mutable ImageEditorUndoStack _undoStack; + private: Restrictions _restrictions; }; diff --git a/Grinder/image/LayerPixels.cpp b/Grinder/image/LayerPixels.cpp index a088c29..51bc46c 100644 --- a/Grinder/image/LayerPixels.cpp +++ b/Grinder/image/LayerPixels.cpp @@ -20,6 +20,8 @@ void LayerPixels::renderDraftItems(const std::vector<DraftItem*>& draftItems, co { if (!draftItems.empty()) { + _pixelsData.beginPainting(); + // Render directly on the pixels using a painter QPainter painter{&_pixelsData.pixels()}; painter.setRenderHints(QPainter::Antialiasing|QPainter::TextAntialiasing|QPainter::HighQualityAntialiasing|QPainter::SmoothPixmapTransform, false); @@ -30,6 +32,8 @@ void LayerPixels::renderDraftItems(const std::vector<DraftItem*>& draftItems, co if (auto renderer = draftItem->createRenderer(rendererStyle)) renderer->render(&painter, ItemRenderer::RenderModeFlag::RenderToImage, ItemRenderer::RenderFlag::NoFlag, renderedColors); } + + _pixelsData.endPainting(); } } diff --git a/Grinder/image/LayerPixelsData.h b/Grinder/image/LayerPixelsData.h index 625ca62..d2b0ea6 100644 --- a/Grinder/image/LayerPixelsData.h +++ b/Grinder/image/LayerPixelsData.h @@ -51,6 +51,7 @@ namespace grndr void clear(QPoint pos) { clear(pos.x(), pos.y()); } void clear(const Selection& selection) { fill(selection, QColor{}); } void clear() { _pixels->fill(0); emitDataModified(); } + void reset() { _pixels = QImage{}; emitDataModified(); } public: void resize(QSize newSize); diff --git a/Grinder/res/Grinder.qrc b/Grinder/res/Grinder.qrc index 99cc282..a5bc8b4 100644 --- a/Grinder/res/Grinder.qrc +++ b/Grinder/res/Grinder.qrc @@ -82,6 +82,8 @@ <file>icons/pipes.png</file> <file>icons/frame-landscape-sample.png</file> <file>icons/overlay-sample.png</file> + <file>icons/undo-arrow.png</file> + <file>icons/redo-arrow.png</file> </qresource> <qresource prefix="/"> <file>css/global.css</file> diff --git a/Grinder/res/Resources.h b/Grinder/res/Resources.h index 32db1f1..96536ad 100644 --- a/Grinder/res/Resources.h +++ b/Grinder/res/Resources.h @@ -50,6 +50,8 @@ #define FILE_ICON_FOLDER ":/icons/icons/folder-with-information.png" #define FILE_ICON_OPEN ":/icons/icons/folder-out-interface-symbol.png" #define FILE_ICON_SAVE ":/icons/icons/floppy-disk-digital-data-storage-or-save-interface-symbol.png" +#define FILE_ICON_UNDO ":/icons/icons/undo-arrow.png" +#define FILE_ICON_REDO ":/icons/icons/redo-arrow.png" #define FILE_ICON_MOVEUP ":/icons/icons/arrow-up.png" #define FILE_ICON_MOVEDOWN ":/icons/icons/arrow-down.png" diff --git a/Grinder/res/icons/redo-arrow.png b/Grinder/res/icons/redo-arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..61677725e5ee53a2e7c5d31876345c4f1a013165 GIT binary patch literal 11014 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 literal 0 HcmV?d00001 diff --git a/Grinder/ui/image/ImageEditorUndoCommand.h b/Grinder/ui/image/ImageEditorUndoCommand.h new file mode 100644 index 0000000..0aaec75 --- /dev/null +++ b/Grinder/ui/image/ImageEditorUndoCommand.h @@ -0,0 +1,32 @@ +/****************************************************************************** + * File: ImageEditorUndoCommand.h + * Date: 30.10.2019 + *****************************************************************************/ + +#ifndef IMAGEEDITORUNDOCOMMAND_H +#define IMAGEEDITORUNDOCOMMAND_H + +#include "ImageEditorUndoCommandBase.h" + +namespace grndr +{ + template<typename ClassType> + class ImageEditorUndoCommand : public ImageEditorUndoCommandBase + { + public: + using class_type = ClassType; + + public: + using ImageEditorUndoCommandBase::ImageEditorUndoCommandBase; + + public: + virtual bool mergeWith(const QUndoCommand* other) override; + + protected: + virtual bool mergeCommand(const class_type* otherCommand) { Q_UNUSED(otherCommand); return false; } + }; +} + +#include "ImageEditorUndoCommand.impl.h" + +#endif diff --git a/Grinder/ui/image/ImageEditorUndoCommand.impl.h b/Grinder/ui/image/ImageEditorUndoCommand.impl.h new file mode 100644 index 0000000..bd4ba6e --- /dev/null +++ b/Grinder/ui/image/ImageEditorUndoCommand.impl.h @@ -0,0 +1,26 @@ +/****************************************************************************** + * File: ImageEditorUndoCommand.impl.h + * Date: 01.11.2019 + *****************************************************************************/ + +#include "Grinder.h" +#include "ImageEditorUndoCommand.h" + +template<typename ClassType> +bool ImageEditorUndoCommand<ClassType>::mergeWith(const QUndoCommand* other) +{ + if (!_enableMerging || other->id() != id()) + return false; + + if (auto otherCommand = dynamic_cast<const class_type*>(other)) + { + if (otherCommand->getMergingIndex() == _mergingIndex) + { + bool result = mergeCommand(otherCommand); + updateCommand(); + return result; + } + } + + return false; +} diff --git a/Grinder/ui/image/ImageEditorUndoCommandBase.cpp b/Grinder/ui/image/ImageEditorUndoCommandBase.cpp new file mode 100644 index 0000000..deeea57 --- /dev/null +++ b/Grinder/ui/image/ImageEditorUndoCommandBase.cpp @@ -0,0 +1,40 @@ +/****************************************************************************** + * File: ImageEditorUndoCommandBase.cpp + * Date: 06.11.2019 + *****************************************************************************/ + +#include "Grinder.h" +#include "ImageEditorUndoCommandBase.h" +#include "ImageEditor.h" + +ImageEditorUndoCommandBase::ImageEditorUndoCommandBase(ImageEditorUndoStack* undoStack, ImageEditor* imageEditor, int id, QString text) : QUndoCommand(text), ImageEditorComponent(imageEditor), + _undoStack{undoStack}, _id{id} +{ + if (!undoStack) + throw std::runtime_error{_EXCPT("undoStack may not be null")}; +} + +void ImageEditorUndoCommandBase::undo() +{ + _undoStack->rejectNewCommands(); // Do not push anything onto the stack during an undo + undoCommand(); + _undoStack->acceptNewCommands(); + + updateCommand(); +} + +int ImageEditorUndoCommandBase::id() const +{ + if (_enableMerging) + return _id; + else + return QUndoCommand::id(); +} + +void ImageEditorUndoCommandBase::updateCommand() +{ + // Cleanup the undo stack; use a timer so that the undo stack has been updated beforehand + QTimer::singleShot(0, [undoStack = _undoStack]() { + undoStack->cleanupStack(); + }); +} diff --git a/Grinder/ui/image/ImageEditorUndoCommandBase.h b/Grinder/ui/image/ImageEditorUndoCommandBase.h new file mode 100644 index 0000000..467a2db --- /dev/null +++ b/Grinder/ui/image/ImageEditorUndoCommandBase.h @@ -0,0 +1,51 @@ +/****************************************************************************** + * File: ImageEditorUndoCommandBase.h + * Date: 06.11.2019 + *****************************************************************************/ + +#ifndef IMAGEEDITORUNDOCOMMANDBASE_H +#define IMAGEEDITORUNDOCOMMANDBASE_H + +#include <QUndoCommand> + +#include "ImageEditorComponent.h" +#include "ImageEditorUndoCommandIDs.h" + +namespace grndr +{ + class ImageEditorUndoStack; + + class ImageEditorUndoCommandBase : public QObject, public QUndoCommand, public ImageEditorComponent + { + Q_OBJECT + + public: + ImageEditorUndoCommandBase(ImageEditorUndoStack* undoStack, ImageEditor* imageEditor, int id, QString text); + + public: + virtual void undo() override; + + public: + virtual int id() const override; + + public: + bool isMergingEnabled() const { return _enableMerging; } + void enableMerging(bool enable = true, unsigned long long mergingIndex = 0) { _enableMerging = enable; _mergingIndex = mergingIndex; } + unsigned long long getMergingIndex() const { return _mergingIndex; } + + protected: + virtual void undoCommand() = 0; + + virtual void updateCommand(); + + protected: + ImageEditorUndoStack* _undoStack{nullptr}; + + int _id{-1}; + + bool _enableMerging{false}; + unsigned long long _mergingIndex{0}; + }; +} + +#endif diff --git a/Grinder/ui/image/ImageEditorUndoCommandIDs.h b/Grinder/ui/image/ImageEditorUndoCommandIDs.h new file mode 100644 index 0000000..50aeb08 --- /dev/null +++ b/Grinder/ui/image/ImageEditorUndoCommandIDs.h @@ -0,0 +1,20 @@ +/****************************************************************************** + * File: ImageEditorUndoCommandIDs.h + * Date: 01.11.2019 + *****************************************************************************/ + +namespace grndr +{ + class ImageEditorUndoCommandID final + { + public: + enum + { + CreateDraftItem, + RemoveDraftItem, + PaintPixels, + ConvertDraftItems, + ConvertPixels, + }; + }; +} diff --git a/Grinder/ui/image/ImageEditorUndoStack.cpp b/Grinder/ui/image/ImageEditorUndoStack.cpp new file mode 100644 index 0000000..76f196e --- /dev/null +++ b/Grinder/ui/image/ImageEditorUndoStack.cpp @@ -0,0 +1,50 @@ +/****************************************************************************** + * File: ImageEditorUndoStack.cpp + * Date: 30.10.2019 + *****************************************************************************/ + +#include "Grinder.h" +#include "ImageEditorUndoStack.h" +#include "ImageEditorUndoCommandBase.h" + +ImageEditorUndoStack::ImageEditorUndoStack(ImageEditor* imageEditor) : ImageEditorComponent(imageEditor) +{ + connect(this, &QUndoStack::canUndoChanged, this, &ImageEditorUndoStack::emitUndoStackChanged); + connect(this, &QUndoStack::undoTextChanged, this, &ImageEditorUndoStack::emitUndoStackChanged); + connect(this, &QUndoStack::canRedoChanged, this, &ImageEditorUndoStack::emitUndoStackChanged); + connect(this, &QUndoStack::redoTextChanged, this, &ImageEditorUndoStack::emitUndoStackChanged); +} + +void ImageEditorUndoStack::cleanupStack() +{ + // Remove obsolete commands from the top of the stack + for (int i = index() - 1; i >= 0; --i) + { + if (auto cmd = command(i)) + { + if (cmd->isObsolete()) + undo(); + else + break; + } + } + + emitUndoStackChanged(); +} + +void ImageEditorUndoStack::endMerging() +{ + if (_mergingCount == 1) + { + if (auto cmd = const_cast<ImageEditorUndoCommandBase*>(dynamic_cast<const ImageEditorUndoCommandBase*>(command(index() - 1)))) + cmd->enableMerging(false); + } + + if (--_mergingCount < 0) + _mergingCount = 0; +} + +void ImageEditorUndoStack::emitUndoStackChanged() +{ + emit undoStackChanged(); +} diff --git a/Grinder/ui/image/ImageEditorUndoStack.h b/Grinder/ui/image/ImageEditorUndoStack.h new file mode 100644 index 0000000..f8b8e0e --- /dev/null +++ b/Grinder/ui/image/ImageEditorUndoStack.h @@ -0,0 +1,52 @@ +/****************************************************************************** + * File: ImageEditorUndoStack.h + * Date: 30.10.2019 + *****************************************************************************/ + +#ifndef IMAGEEDITORUNDOSTACK_H +#define IMAGEEDITORUNDOSTACK_H + +#include <QUndoStack> + +#include "ImageEditorComponent.h" + +namespace grndr +{ + class ImageEditorUndoStack : public QUndoStack, public ImageEditorComponent + { + Q_OBJECT + + public: + ImageEditorUndoStack(ImageEditor* imageEditor); + + public: + template<typename CommandType, typename... Args> + CommandType* push(Args... args); + + public: + void cleanupStack(); + + public: + void acceptNewCommands() { _acceptNewCommands = true; } + void rejectNewCommands() { _acceptNewCommands = false; } + + void beginMerging() { ++_mergingCount; ++_mergingIndex; } + void endMerging(); + + public slots: + void emitUndoStackChanged(); + + signals: + void undoStackChanged(); + + private: + bool _acceptNewCommands{true}; + + int _mergingCount{0}; + unsigned long long _mergingIndex{0}; + }; +} + +#include "ImageEditorUndoStack.impl.h" + +#endif diff --git a/Grinder/ui/image/ImageEditorUndoStack.impl.h b/Grinder/ui/image/ImageEditorUndoStack.impl.h new file mode 100644 index 0000000..59c510f --- /dev/null +++ b/Grinder/ui/image/ImageEditorUndoStack.impl.h @@ -0,0 +1,21 @@ +/****************************************************************************** + * File: ImageEditorUndoStack.impl.h + * Date: 30.10.2019 + *****************************************************************************/ + +#include "Grinder.h" +#include "ImageEditorUndoStack.h" + +template<typename CommandType, typename... Args> +CommandType* ImageEditorUndoStack::push(Args... args) +{ + if (_acceptNewCommands) + { + auto command = new CommandType{this, _imageEditor, std::forward<Args>(args)...}; + command->enableMerging(_mergingCount > 0, _mergingIndex); + QUndoStack::push(command); + return command; + } + else + return nullptr; +} diff --git a/Grinder/ui/image/ImageEditorWidget.cpp b/Grinder/ui/image/ImageEditorWidget.cpp index 4d823f4..48182d8 100644 --- a/Grinder/ui/image/ImageEditorWidget.cpp +++ b/Grinder/ui/image/ImageEditorWidget.cpp @@ -33,6 +33,8 @@ ImageEditorWidget::ImageEditorWidget(ImageEditor* imageEditor, QWidget* parent) qRegisterMetaType<std::shared_ptr<ImageReference>>(); // Create editor actions + _undoAction = UIUtils::createAction(this, "&Undo", FILE_ICON_UNDO, SLOT(undoCommand()), "Undo the last command (Ctrl+Z)", "Ctrl+Z", Qt::WidgetWithChildrenShortcut); + _copyImageBuildAction = UIUtils::createAction(this, "&Copy image build", FILE_ICON_COPY, SLOT(copyImageBuild()), "Copy the current image build to the clipboard (Ctrl+Shift+C)", "Ctrl+Shift+C", Qt::WidgetWithChildrenShortcut); _pasteImageBuildAction = UIUtils::createAction(this, "&Paste image build", FILE_ICON_PASTE, SLOT(pasteImageBuild()), "Paste an image build from the clipboard (Ctrl+Shift+V)", "Ctrl+Shift+V", Qt::WidgetWithChildrenShortcut); _duplicateImageBuildAction = UIUtils::createAction(this, "&Copy image build to next image", FILE_ICON_EDITOR_COPYFROMPREVIOUS, SLOT(duplicateImageBuild()), "Copy the current image build to the next image (Ctrl+D)", "Ctrl+D", Qt::WidgetWithChildrenShortcut); @@ -51,6 +53,9 @@ ImageEditorWidget::ImageEditorWidget(ImageEditor* imageEditor, QWidget* parent) // Reflect primary color changes connect(&_imageEditor->environment(), &ImageEditorEnvironment::primaryColorChanged, this, &ImageEditorWidget::primaryColorChanged); + // Reflect undo state + connect(&_imageEditor->controller().undoStack(), &ImageEditorUndoStack::undoStackChanged, this, &ImageEditorWidget::updateUndo); + // Listen for various events to update our actions connect(&grinder()->project(), SIGNAL(imageReferenceCreated(const std::shared_ptr<ImageReference>&)), this, SLOT(updateActions())); connect(&grinder()->project(), SIGNAL(imageReferenceRemoved(const std::shared_ptr<ImageReference>&)), this, SLOT(updateActions()), Qt::QueuedConnection); // Delay this signal so that the image reference has been removed from the image references list @@ -170,7 +175,9 @@ void ImageEditorWidget::setupImageControlBar() ui->imageSceneControlBar->addSeparator(Qt::AlignLeft); } - // Add the copy&paste actions + // Add various editor-global actions + ui->imageSceneControlBar->addSeparator(Qt::AlignLeft); + ui->imageSceneControlBar->addAction(_undoAction, Qt::ToolButtonFollowStyle, Qt::AlignLeft); ui->imageSceneControlBar->addSeparator(Qt::AlignLeft); ui->imageSceneControlBar->addAction(_copyImageBuildAction, Qt::ToolButtonFollowStyle, Qt::AlignLeft); ui->imageSceneControlBar->addAction(_pasteImageBuildAction, Qt::ToolButtonFollowStyle, Qt::AlignLeft); @@ -246,6 +253,8 @@ void ImageEditorWidget::updateActions() _pasteImageBuildAction->setEnabled(imageBuild && grinder()->clipboardManager().hasData(ImageBuildVector::Serialization_Element)); _editImageBuildTagsAction->setEnabled(_imageEditor->controller().activeImageBuild() && _imageEditor->controller().activeImageBuild()->inputImageTags()); + + updateUndo(); } void ImageEditorWidget::updateSceneZoomLevel(qreal zoomLevel) @@ -253,6 +262,21 @@ void ImageEditorWidget::updateSceneZoomLevel(qreal zoomLevel) _imageSceneZoomLabel->setText(QString{"%1%"}.arg(static_cast<int>(zoomLevel * 100.0))); } +void ImageEditorWidget::updateUndo() +{ + auto modifyAction = [](QAction* action, QString actionName, bool actionEnabled, QString actionText) { + QString text = !actionText.isEmpty() ? QString{"%1 %2"}.arg(actionName).arg(actionText) : actionName; + + action->setEnabled(actionEnabled); + action->setText(QString{"&%1"}.arg(text)); + action->setToolTip(QString{"%1 (%2)"}.arg(text).arg(action->shortcut().toString())); + action->setStatusTip(action->toolTip()); + }; + + auto& undoStack = _imageEditor->controller().undoStack(); + modifyAction(_undoAction, "Undo", undoStack.canUndo(), undoStack.undoText()); +} + void ImageEditorWidget::updatePrimaryColor() { if (auto scene = _imageEditor->controller().activeScene()) @@ -297,6 +321,11 @@ void ImageEditorWidget::assignPrimaryColorToPreset() } } +void ImageEditorWidget::undoCommand() const +{ + _imageEditor->controller().undoStack().undo(); +} + void ImageEditorWidget::copyImageBuild() const { _imageEditor->controller().copyImageBuild(); diff --git a/Grinder/ui/image/ImageEditorWidget.h b/Grinder/ui/image/ImageEditorWidget.h index e400344..8f77542 100644 --- a/Grinder/ui/image/ImageEditorWidget.h +++ b/Grinder/ui/image/ImageEditorWidget.h @@ -66,6 +66,7 @@ namespace grndr private slots: void updateActions(); void updateSceneZoomLevel(qreal zoomLevel); + void updateUndo(); void updatePrimaryColor(); void primaryColorChanged(QColor color); @@ -73,6 +74,8 @@ namespace grndr void colorPresetSelected(QColor color); void assignPrimaryColorToPreset(); + void undoCommand() const; + void copyImageBuild() const; void pasteImageBuild() const; void duplicateImageBuild() const; @@ -83,6 +86,8 @@ namespace grndr private: QAction* _newLayerAction{nullptr}; + QAction* _undoAction{nullptr}; + QAction* _copyImageBuildAction{nullptr}; QAction* _pasteImageBuildAction{nullptr}; QAction* _duplicateImageBuildAction{nullptr}; diff --git a/Grinder/ui/image/commands/ConvertDraftItemsUndoCommand.cpp b/Grinder/ui/image/commands/ConvertDraftItemsUndoCommand.cpp new file mode 100644 index 0000000..5fdd0bd --- /dev/null +++ b/Grinder/ui/image/commands/ConvertDraftItemsUndoCommand.cpp @@ -0,0 +1,92 @@ +/****************************************************************************** + * File: ConvertDraftItemsUndoCommand.cpp + * Date: 09.11.2019 + *****************************************************************************/ + +#include "Grinder.h" +#include "ConvertDraftItemsUndoCommand.h" +#include "ui/image/ImageEditor.h" + +ConvertDraftItemsUndoCommand::ConvertDraftItemsUndoCommand(ImageEditorUndoStack* undoStack, ImageEditor* imageEditor, const std::map<Layer*, std::vector<DraftItem*>>& draftItems) : ImageEditorUndoCommand(undoStack, imageEditor, ImageEditorUndoCommandID::ConvertDraftItems, "") +{ + // Store all draft items and their layers pixels data + for (auto it : draftItems) + { + for (auto draftItem : it.second) + { + SerializationContext ctx{DraftItemVector::Serialization_Element, SerializationContext::Mode::ClipboardSerialization}; + draftItem->serialize(ctx); + + _draftItemData[draftItem->layer()].push_back(ctx.settings()); + } + + _pixelsData[it.first] = it.first->layerPixels().data(); + + // Listen for signals that will render this command obsolete + connect(it.first->imageBuild(), &ImageBuild::layerRemoved, this, &ConvertDraftItemsUndoCommand::layerRemoved); + } +} + +void ConvertDraftItemsUndoCommand::undoCommand() +{ + // Re-add all previously removed draft items + for (auto it : _draftItemData) + { + for (auto itemData : it.second) + { + DraftItemType type = itemData(DraftItem::Serialization_Value_Type, DraftItemType::Undefined).toString(); + + if (auto draftItem = _imageEditor->controller().createDraftItem(type, it.first, false)) + { + DeserializationContext ctx{itemData, DeserializationContext::Mode::ClipboardSerialization}; + draftItem->deserialize(ctx); + } + } + } + + _draftItemData.clear(); + + // Restore the previous layers pixels + for (auto it : _pixelsData) + it.first->layerPixels().data() = it.second; + + _pixelsData.clear(); +} + +bool ConvertDraftItemsUndoCommand::mergeCommand(const ConvertDraftItemsUndoCommand* otherCommand) +{ + // Store all draft items from the other command + for (auto it : otherCommand->_draftItemData) + { + auto& items = _draftItemData[it.first]; + items.insert(items.end(), it.second.cbegin(), it.second.cend()); + } + + // Store any non-existing layer pixels data + _pixelsData.insert(otherCommand->_pixelsData.begin(), otherCommand->_pixelsData.end()); + + return true; +} + +void ConvertDraftItemsUndoCommand::updateCommand() +{ + if (_draftItemData.size() > 1) + setText("converting items"); + else if (_draftItemData.size() == 1) + setText("converting an item"); + else + setText(""); + + + setObsolete(_draftItemData.empty() || _pixelsData.empty()); + + ImageEditorUndoCommand::updateCommand(); +} + +void ConvertDraftItemsUndoCommand::layerRemoved(const std::shared_ptr<Layer>& layer) +{ + // Remove items and pixels belonging to the removed layer + _draftItemData.erase(layer.get()); + _pixelsData.erase(layer.get()); + updateCommand(); +} diff --git a/Grinder/ui/image/commands/ConvertDraftItemsUndoCommand.h b/Grinder/ui/image/commands/ConvertDraftItemsUndoCommand.h new file mode 100644 index 0000000..d19b0b0 --- /dev/null +++ b/Grinder/ui/image/commands/ConvertDraftItemsUndoCommand.h @@ -0,0 +1,40 @@ +/****************************************************************************** + * File: ConvertDraftItemsUndoCommand.h + * Date: 09.11.2019 + *****************************************************************************/ + +#ifndef CONVERTDRAFTITEMSUNDOCOMMAND_H +#define CONVERTDRAFTITEMSUNDOCOMMAND_H + +#include "ui/image/ImageEditorUndoCommand.h" +#include "common/serialization/SettingsContainer.h" +#include "image/LayerPixelsData.h" + +namespace grndr +{ + class DraftItem; + class Layer; + + class ConvertDraftItemsUndoCommand : public ImageEditorUndoCommand<ConvertDraftItemsUndoCommand> + { + Q_OBJECT + + public: + ConvertDraftItemsUndoCommand(ImageEditorUndoStack* undoStack, ImageEditor* imageEditor, const std::map<Layer*, std::vector<DraftItem*>>& draftItems); + + protected: + virtual void undoCommand() override; + virtual bool mergeCommand(const ConvertDraftItemsUndoCommand* otherCommand) override; + + virtual void updateCommand() override; + + private slots: + void layerRemoved(const std::shared_ptr<Layer>& layer); + + private: + std::map<Layer*, std::vector<SettingsContainer>> _draftItemData; + std::map<Layer*, LayerPixelsData> _pixelsData; + }; +} + +#endif diff --git a/Grinder/ui/image/commands/ConvertPixelsUndoCommand.cpp b/Grinder/ui/image/commands/ConvertPixelsUndoCommand.cpp new file mode 100644 index 0000000..4411387 --- /dev/null +++ b/Grinder/ui/image/commands/ConvertPixelsUndoCommand.cpp @@ -0,0 +1,77 @@ +/****************************************************************************** + * File: ConvertPixelsUndoCommand.cpp + * Date: 09.11.2019 + *****************************************************************************/ + +#include "Grinder.h" +#include "ConvertPixelsUndoCommand.h" +#include "ui/image/ImageEditor.h" + +ConvertPixelsUndoCommand::ConvertPixelsUndoCommand(ImageEditorUndoStack* undoStack, ImageEditor* imageEditor, const std::vector<std::shared_ptr<DraftItem> >& draftItems, const std::vector<Layer*>& layers) : ImageEditorUndoCommand(undoStack, imageEditor, ImageEditorUndoCommandID::ConvertPixels, "converting pixels") +{ + // Store the draft items + for (auto draftItem : draftItems) + _draftItems.emplace_back(draftItem); + + // Store the layers pixels data + for (auto layer : layers) + { + _pixelsData[layer] = layer->layerPixels().data(); + + // Listen for signals that will render this command obsolete + connect(layer->imageBuild(), &ImageBuild::layerRemoved, this, &ConvertPixelsUndoCommand::layerRemoved); + connect(layer, &Layer::draftItemRemoved, this, &ConvertPixelsUndoCommand::draftItemRemoved); + } +} + +void ConvertPixelsUndoCommand::undoCommand() +{ + // Remove all previously created draft items + auto draftItems = _draftItems; // Need to copy the vector, as draftItemRemoved will be called during removal + + for (auto& item : draftItems) + { + if (auto draftItem = item.lock()) // Make sure the underlying draft item still exists + _imageEditor->controller().removeDraftItem(draftItem.get()); + } + + _draftItems.clear(); + + // Restore the previous layers pixels + for (auto it : _pixelsData) + it.first->layerPixels().data() = it.second; + + _pixelsData.clear(); +} + +bool ConvertPixelsUndoCommand::mergeCommand(const ConvertPixelsUndoCommand* otherCommand) +{ + // Store all draft items from the other command + _draftItems.insert(_draftItems.end(), otherCommand->_draftItems.cbegin(), otherCommand->_draftItems.cend()); + + // Store any non-existing layer pixels data + _pixelsData.insert(otherCommand->_pixelsData.begin(), otherCommand->_pixelsData.end()); + + return true; +} + +void ConvertPixelsUndoCommand::updateCommand() +{ + setObsolete(_draftItems.empty() || _pixelsData.empty()); + + ImageEditorUndoCommand::updateCommand(); +} + +void ConvertPixelsUndoCommand::layerRemoved(const std::shared_ptr<Layer>& layer) +{ + // Remove pixels belonging to the removed layer + _pixelsData.erase(layer.get()); + updateCommand(); +} + +void ConvertPixelsUndoCommand::draftItemRemoved(const std::shared_ptr<DraftItem>& item) +{ + // Remove the item from this command + _draftItems.erase(std::remove_if(_draftItems.begin(), _draftItems.end(), [&item](auto draftItem) { return draftItem.lock() == item; }), _draftItems.end()); + updateCommand(); +} diff --git a/Grinder/ui/image/commands/ConvertPixelsUndoCommand.h b/Grinder/ui/image/commands/ConvertPixelsUndoCommand.h new file mode 100644 index 0000000..8b7f45d --- /dev/null +++ b/Grinder/ui/image/commands/ConvertPixelsUndoCommand.h @@ -0,0 +1,41 @@ +/****************************************************************************** + * File: ConvertPixelsUndoCommand.h + * Date: 09.11.2019 + *****************************************************************************/ + +#ifndef CONVERTPIXELSUNDOCOMMAND_H +#define CONVERTPIXELSUNDOCOMMAND_H + +#include "ui/image/ImageEditorUndoCommand.h" +#include "common/serialization/SettingsContainer.h" +#include "image/LayerPixelsData.h" + +namespace grndr +{ + class DraftItem; + class Layer; + + class ConvertPixelsUndoCommand : public ImageEditorUndoCommand<ConvertPixelsUndoCommand> + { + Q_OBJECT + + public: + ConvertPixelsUndoCommand(ImageEditorUndoStack* undoStack, ImageEditor* imageEditor, const std::vector<std::shared_ptr<DraftItem>>& draftItems, const std::vector<Layer*>& layers); + + protected: + virtual void undoCommand() override; + virtual bool mergeCommand(const ConvertPixelsUndoCommand* otherCommand) override; + + virtual void updateCommand() override; + + private slots: + void layerRemoved(const std::shared_ptr<Layer>& layer); + void draftItemRemoved(const std::shared_ptr<DraftItem>& item); + + private: + std::vector<std::weak_ptr<DraftItem>> _draftItems; + std::map<Layer*, LayerPixelsData> _pixelsData; + }; +} + +#endif diff --git a/Grinder/ui/image/commands/CreateDraftItemUndoCommand.cpp b/Grinder/ui/image/commands/CreateDraftItemUndoCommand.cpp new file mode 100644 index 0000000..7c8b529 --- /dev/null +++ b/Grinder/ui/image/commands/CreateDraftItemUndoCommand.cpp @@ -0,0 +1,68 @@ +/****************************************************************************** + * File: CreateDraftItemUndoCommand.cpp + * Date: 01.11.2019 + *****************************************************************************/ + +#include "Grinder.h" +#include "CreateDraftItemUndoCommand.h" +#include "ui/image/ImageEditor.h" + +CreateDraftItemUndoCommand::CreateDraftItemUndoCommand(ImageEditorUndoStack* undoStack, ImageEditor* imageEditor, const std::shared_ptr<DraftItem>& draftItem, Layer* layer) : LayerUndoCommand(undoStack, imageEditor, ImageEditorUndoCommandID::CreateDraftItem, "", layer) +{ + if (draftItem) + _draftItems.emplace_back(draftItem); + + if (layer) + { + // Listen for signals that will render this command obsolete + connect(_layer, &Layer::draftItemRemoved, this, &CreateDraftItemUndoCommand::draftItemRemoved); + } + + updateCommand(); +} + +void CreateDraftItemUndoCommand::undoCommand() +{ + // Remove all previously created draft items + auto draftItems = _draftItems; // Need to copy the vector, as draftItemRemoved will be called during removal + + for (auto& item : draftItems) + { + if (auto draftItem = item.lock()) // Make sure the underlying draft item still exists + _imageEditor->controller().removeDraftItem(draftItem.get()); + } + + _draftItems.clear(); +} + +bool CreateDraftItemUndoCommand::mergeCommand(const CreateDraftItemUndoCommand* otherCommand) +{ + if (otherCommand->_layer == _layer) + { + _draftItems.insert(_draftItems.end(), otherCommand->_draftItems.cbegin(), otherCommand->_draftItems.cend()); + return true; + } + + return false; +} + +void CreateDraftItemUndoCommand::updateCommand() +{ + if (_draftItems.size() > 1) + setText("creating items"); + else if (_draftItems.size() == 1) + setText("creating an item"); + else + setText(""); + + setObsolete(_draftItems.empty()); + + LayerUndoCommand::updateCommand(); +} + +void CreateDraftItemUndoCommand::draftItemRemoved(const std::shared_ptr<DraftItem>& item) +{ + // Remove the item from this command + _draftItems.erase(std::remove_if(_draftItems.begin(), _draftItems.end(), [&item](auto draftItem) { return draftItem.lock() == item; }), _draftItems.end()); + updateCommand(); +} diff --git a/Grinder/ui/image/commands/CreateDraftItemUndoCommand.h b/Grinder/ui/image/commands/CreateDraftItemUndoCommand.h new file mode 100644 index 0000000..b054ef1 --- /dev/null +++ b/Grinder/ui/image/commands/CreateDraftItemUndoCommand.h @@ -0,0 +1,39 @@ +/****************************************************************************** + * File: CreateDraftItemUndoCommand.h + * Date: 01.11.2019 + *****************************************************************************/ + +#ifndef CREATEDRAFTITEMUNDOCOMMAND_H +#define CREATEDRAFTITEMUNDOCOMMAND_H + +#include <memory> + +#include "ui/image/commands/LayerUndoCommand.h" + +namespace grndr +{ + class DraftItem; + class Layer; + + class CreateDraftItemUndoCommand : public LayerUndoCommand<CreateDraftItemUndoCommand> + { + Q_OBJECT + + public: + CreateDraftItemUndoCommand(ImageEditorUndoStack* undoStack, ImageEditor* imageEditor, const std::shared_ptr<DraftItem>& draftItem, Layer* layer); + + protected: + virtual void undoCommand() override; + virtual bool mergeCommand(const CreateDraftItemUndoCommand* otherCommand) override; + + virtual void updateCommand() override; + + private slots: + void draftItemRemoved(const std::shared_ptr<DraftItem>& item); + + private: + std::vector<std::weak_ptr<DraftItem>> _draftItems; + }; +} + +#endif diff --git a/Grinder/ui/image/commands/LayerUndoCommand.h b/Grinder/ui/image/commands/LayerUndoCommand.h new file mode 100644 index 0000000..ae613de --- /dev/null +++ b/Grinder/ui/image/commands/LayerUndoCommand.h @@ -0,0 +1,38 @@ +/****************************************************************************** + * File: LayerUndoCommand.h + * Date: 06.11.2019 + *****************************************************************************/ + +#ifndef LAYERUNDOCOMMAND_H +#define LAYERUNDOCOMMAND_H + +#include "ui/image/ImageEditorUndoCommand.h" + +namespace grndr +{ + class Layer; + + template<typename ClassType> + class LayerUndoCommand : public ImageEditorUndoCommand<ClassType> + { + public: + using base_type = ImageEditorUndoCommand<ClassType>; + using class_type = typename base_type::class_type; + + public: + LayerUndoCommand(ImageEditorUndoStack* undoStack, ImageEditor* imageEditor, int id, QString text, Layer* layer); + + protected: + virtual void updateCommand() override; + + private slots: + void layerRemoved(const std::shared_ptr<Layer>& layer); + + protected: + Layer* _layer{nullptr}; + }; +} + +#include "LayerUndoCommand.impl.h" + +#endif diff --git a/Grinder/ui/image/commands/LayerUndoCommand.impl.h b/Grinder/ui/image/commands/LayerUndoCommand.impl.h new file mode 100644 index 0000000..94e1700 --- /dev/null +++ b/Grinder/ui/image/commands/LayerUndoCommand.impl.h @@ -0,0 +1,39 @@ +/****************************************************************************** + * File: LayerUndoCommand.impl.h + * Date: 06.11.2019 + *****************************************************************************/ + +#include "Grinder.h" +#include "LayerUndoCommand.h" +#include "image/ImageBuild.h" + +template<typename ClassType> +LayerUndoCommand<ClassType>::LayerUndoCommand(ImageEditorUndoStack* undoStack, ImageEditor* imageEditor, int id, QString text, Layer* layer) : ImageEditorUndoCommand<ClassType>(undoStack, imageEditor, id, text), + _layer{layer} +{ + if (layer) + { + // Listen for signals that will render this command obsolete + this->connect(_layer->imageBuild(), &ImageBuild::layerRemoved, this, &LayerUndoCommand::layerRemoved); + } +} + +template<typename ClassType> +void LayerUndoCommand<ClassType>::updateCommand() +{ + if (!_layer) + this->setObsolete(true); + + base_type::updateCommand(); +} + +template<typename ClassType> +void LayerUndoCommand<ClassType>::layerRemoved(const std::shared_ptr<Layer>& layer) +{ + // If the layer has been removed, render the command obsolete + if (layer.get() == _layer) + { + _layer = nullptr; + this->updateCommand(); + } +} diff --git a/Grinder/ui/image/commands/PaintPixelsUndoCommand.cpp b/Grinder/ui/image/commands/PaintPixelsUndoCommand.cpp new file mode 100644 index 0000000..526901f --- /dev/null +++ b/Grinder/ui/image/commands/PaintPixelsUndoCommand.cpp @@ -0,0 +1,41 @@ +/****************************************************************************** + * File: PaintPixelsUndoCommand.cpp + * Date: 06.11.2019 + *****************************************************************************/ + +#include "Grinder.h" +#include "PaintPixelsUndoCommand.h" + +PaintPixelsUndoCommand::PaintPixelsUndoCommand(ImageEditorUndoStack* undoStack, ImageEditor* imageEditor, Layer* layer, QString text) : LayerUndoCommand(undoStack, imageEditor, ImageEditorUndoCommandID::PaintPixels, text, layer) +{ + // Save the current layer pixels + _pixelsData = _layer->layerPixels().data(); + + updateCommand(); +} + +void PaintPixelsUndoCommand::undoCommand() +{ + // Restore the previous layer pixels + _layer->layerPixels().data() = _pixelsData; + + _pixelsData.reset(); +} + +bool PaintPixelsUndoCommand::mergeCommand(const PaintPixelsUndoCommand* otherCommand) +{ + if (otherCommand->_layer == _layer) + { + // Nothing needs to be done, as we want to keep the initial layer pixels + return true; + } + + return false; +} + +void PaintPixelsUndoCommand::updateCommand() +{ + setObsolete(_pixelsData.size().isEmpty()); + + LayerUndoCommand::updateCommand(); +} diff --git a/Grinder/ui/image/commands/PaintPixelsUndoCommand.h b/Grinder/ui/image/commands/PaintPixelsUndoCommand.h new file mode 100644 index 0000000..59cd6e5 --- /dev/null +++ b/Grinder/ui/image/commands/PaintPixelsUndoCommand.h @@ -0,0 +1,34 @@ +/****************************************************************************** + * File: PaintPixelsUndoCommand.h + * Date: 06.11.2019 + *****************************************************************************/ + +#ifndef PAINTPIXELSUNDOCOMMAND_H +#define PAINTPIXELSUNDOCOMMAND_H + +#include "ui/image/commands/LayerUndoCommand.h" + +namespace grndr +{ + class DraftItem; + class Layer; + + class PaintPixelsUndoCommand : public LayerUndoCommand<PaintPixelsUndoCommand> + { + Q_OBJECT + + public: + PaintPixelsUndoCommand(ImageEditorUndoStack* undoStack, ImageEditor* imageEditor, Layer* layer, QString text = "paint pixels"); + + protected: + virtual void undoCommand() override; + virtual bool mergeCommand(const PaintPixelsUndoCommand* otherCommand) override; + + virtual void updateCommand() override; + + private: + LayerPixelsData _pixelsData; + }; +} + +#endif diff --git a/Grinder/ui/image/commands/RemoveDraftItemUndoCommand.cpp b/Grinder/ui/image/commands/RemoveDraftItemUndoCommand.cpp new file mode 100644 index 0000000..5822ba2 --- /dev/null +++ b/Grinder/ui/image/commands/RemoveDraftItemUndoCommand.cpp @@ -0,0 +1,62 @@ +/****************************************************************************** + * File: RemoveDraftItemUndoCommand.cpp + * Date: 01.11.2019 + *****************************************************************************/ + +#include "Grinder.h" +#include "RemoveDraftItemUndoCommand.h" +#include "ui/image/ImageEditor.h" + +RemoveDraftItemUndoCommand::RemoveDraftItemUndoCommand(ImageEditorUndoStack* undoStack, ImageEditor* imageEditor, const DraftItem* draftItem, Layer* layer) : LayerUndoCommand(undoStack, imageEditor, ImageEditorUndoCommandID::RemoveDraftItem, "removing an item", layer) +{ + if (draftItem) + { + SerializationContext ctx{DraftItemVector::Serialization_Element, SerializationContext::Mode::ClipboardSerialization}; + draftItem->serialize(ctx); + _draftItemData.push_back(ctx.settings()); + } + + updateCommand(); +} + +void RemoveDraftItemUndoCommand::undoCommand() +{ + // Re-add all previously removed draft items + for (auto& itemData : _draftItemData) + { + DraftItemType type = itemData(DraftItem::Serialization_Value_Type, DraftItemType::Undefined).toString(); + + if (auto draftItem = _imageEditor->controller().createDraftItem(type, _layer, false)) + { + DeserializationContext ctx{itemData, DeserializationContext::Mode::ClipboardSerialization}; + draftItem->deserialize(ctx); + } + } + + _draftItemData.clear(); +} + +bool RemoveDraftItemUndoCommand::mergeCommand(const RemoveDraftItemUndoCommand* otherCommand) +{ + if (otherCommand->_layer == _layer) + { + _draftItemData.insert(_draftItemData.end(), otherCommand->_draftItemData.cbegin(), otherCommand->_draftItemData.cend()); + return true; + } + + return false; +} + +void RemoveDraftItemUndoCommand::updateCommand() +{ + if (_draftItemData.size() > 1) + setText("removing draft items"); + else if (_draftItemData.size() == 1) + setText("removing a draft item"); + else + setText(""); + + setObsolete(_draftItemData.empty()); + + LayerUndoCommand::updateCommand(); +} diff --git a/Grinder/ui/image/commands/RemoveDraftItemUndoCommand.h b/Grinder/ui/image/commands/RemoveDraftItemUndoCommand.h new file mode 100644 index 0000000..4961111 --- /dev/null +++ b/Grinder/ui/image/commands/RemoveDraftItemUndoCommand.h @@ -0,0 +1,34 @@ +/****************************************************************************** + * File: RemoveDraftItemUndoCommand.h + * Date: 01.11.2019 + *****************************************************************************/ + +#ifndef REMOVEDRAFTITEMUNDOCOMMAND_H +#define REMOVEDRAFTITEMUNDOCOMMAND_H + +#include "ui/image/commands/LayerUndoCommand.h" + +namespace grndr +{ + class DraftItem; + class Layer; + + class RemoveDraftItemUndoCommand : public LayerUndoCommand<RemoveDraftItemUndoCommand> + { + Q_OBJECT + + public: + RemoveDraftItemUndoCommand(ImageEditorUndoStack* undoStack, ImageEditor* imageEditor, const DraftItem* draftItem, Layer* layer); + + protected: + virtual void undoCommand() override; + virtual bool mergeCommand(const RemoveDraftItemUndoCommand* otherCommand) override; + + virtual void updateCommand() override; + + private: + std::vector<SettingsContainer> _draftItemData; + }; +} + +#endif diff --git a/Grinder/ui/image/tools/PaintbrushTool.cpp b/Grinder/ui/image/tools/PaintbrushTool.cpp index 9f99f0d..cbb154c 100644 --- a/Grinder/ui/image/tools/PaintbrushTool.cpp +++ b/Grinder/ui/image/tools/PaintbrushTool.cpp @@ -1,84 +1,111 @@ -/****************************************************************************** - * File: PaintbrushTool.cpp - * Date: 21.5.2018 - *****************************************************************************/ - -#include "Grinder.h" -#include "PaintbrushTool.h" -#include "ui/image/ImageEditor.h" -#include "image/ImageUtils.h" -#include "util/MathUtils.h" -#include "res/Resources.h" - -const char* PaintbrushTool::tool_type = "PaintbrushTool"; - -PaintbrushTool::PaintbrushTool(ImageEditor* imageEditor) : PaintbrushTool(imageEditor, "Paintbrush", FILE_ICON_EDITOR_PAINTBRUSH, "P", QCursor{QPixmap{FILE_CURSOR_EDITOR_PAINTBRUSH}, 0, 23}) -{ - -} - -PaintbrushTool::PaintbrushTool(ImageEditor* imageEditor, QString name, QString icon, QString shortcut, QCursor cursor) : ImageEditorTool(imageEditor, name, icon, shortcut, cursor) -{ - _supportRightMouseButton = true; -} - -void PaintbrushTool::createProperties() -{ - ImageEditorTool::createProperties(); - - // Create specific properties - setPropertyGroup("General"); - - _penWidth = createProperty<UIntProperty>(PropertyID::Width, "Brush width", 1); - penWidth()->createConstraint<RangeConstraint>(1, 100); - penWidth()->setDescription("The width of the pixels to paint."); -} - -ImageEditorTool::InputEventResult PaintbrushTool::mousePressed(const QGraphicsSceneMouseEvent* event) -{ - auto pos = MathUtils::floor(event->scenePos()); - _lastPixelPos = pos; - paintPixel(pos, _eraseByDefault); - return InputEventResult::Process; -} - -ImageEditorTool::InputEventResult PaintbrushTool::mouseMoved(const QGraphicsSceneMouseEvent* event) -{ - auto pos = MathUtils::floor(event->scenePos()); - paintPixel(pos, _eraseByDefault); - _lastPixelPos = pos; - return InputEventResult::Process; -} - -VisualSceneInputHandler::InputEventResult PaintbrushTool::rightMousePressed(const QGraphicsSceneMouseEvent* event) -{ - auto pos = MathUtils::floor(event->scenePos()); - _lastPixelPos = pos; - paintPixel(pos, !_eraseByDefault); - return InputEventResult::Process; -} - -VisualSceneInputHandler::InputEventResult PaintbrushTool::rightMouseMoved(const QGraphicsSceneMouseEvent* event) -{ - auto pos = MathUtils::floor(event->scenePos()); - paintPixel(pos, !_eraseByDefault); - _lastPixelPos = pos; - return InputEventResult::Process; -} - -void PaintbrushTool::paintPixel(QPoint pos, bool erase) const -{ - // Draw a straight line from the current position to the last painted pixel - int width = *penWidth(); - - if (_eraseByDefault) - { - auto adjustedWidth = static_cast<qreal>(width) / _imageEditor->controller().activeScene()->view()->getZoom(); - width = static_cast<int>(std::ceil(adjustedWidth)); - - if (width <= 0) - width = 1; - } - - _imageEditor->controller().paintPixels(nullptr, ImageUtils::getLinePoints(pos, _lastPixelPos, width), !erase ? _imageEditor->environment().getPrimaryColor() : QColor{}); -} +/****************************************************************************** + * File: PaintbrushTool.cpp + * Date: 21.5.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "PaintbrushTool.h" +#include "ui/image/ImageEditor.h" +#include "image/ImageUtils.h" +#include "util/MathUtils.h" +#include "res/Resources.h" + +const char* PaintbrushTool::tool_type = "PaintbrushTool"; + +PaintbrushTool::PaintbrushTool(ImageEditor* imageEditor) : PaintbrushTool(imageEditor, "Paintbrush", FILE_ICON_EDITOR_PAINTBRUSH, "P", QCursor{QPixmap{FILE_CURSOR_EDITOR_PAINTBRUSH}, 0, 23}) +{ + +} + +PaintbrushTool::PaintbrushTool(ImageEditor* imageEditor, QString name, QString icon, QString shortcut, QCursor cursor) : ImageEditorTool(imageEditor, name, icon, shortcut, cursor) +{ + _supportRightMouseButton = true; +} + +void PaintbrushTool::createProperties() +{ + ImageEditorTool::createProperties(); + + // Create specific properties + setPropertyGroup("General"); + + _penWidth = createProperty<UIntProperty>(PropertyID::Width, "Brush width", 1); + penWidth()->createConstraint<RangeConstraint>(1, 100); + penWidth()->setDescription("The width of the pixels to paint."); +} + +ImageEditorTool::InputEventResult PaintbrushTool::mousePressed(const QGraphicsSceneMouseEvent* event) +{ + // Merge any consecutive paint operations + _imageEditor->controller().undoStack().beginMerging(); + + auto pos = MathUtils::floor(event->scenePos()); + _lastPixelPos = pos; + paintPixel(pos, _eraseByDefault); + + return InputEventResult::Process; +} + +ImageEditorTool::InputEventResult PaintbrushTool::mouseMoved(const QGraphicsSceneMouseEvent* event) +{ + auto pos = MathUtils::floor(event->scenePos()); + paintPixel(pos, _eraseByDefault); + _lastPixelPos = pos; + return InputEventResult::Process; +} + +VisualSceneInputHandler::InputEventResult PaintbrushTool::mouseReleased(const QGraphicsSceneMouseEvent* event) +{ + Q_UNUSED(event); + + // Painting pixels in a single stroke have now been merged into one command + _imageEditor->controller().undoStack().endMerging(); + + return InputEventResult::Process; +} + +VisualSceneInputHandler::InputEventResult PaintbrushTool::rightMousePressed(const QGraphicsSceneMouseEvent* event) +{ + // Merge consecutive paint operations + _imageEditor->controller().undoStack().beginMerging(); + + auto pos = MathUtils::floor(event->scenePos()); + _lastPixelPos = pos; + paintPixel(pos, !_eraseByDefault); + return InputEventResult::Process; +} + +VisualSceneInputHandler::InputEventResult PaintbrushTool::rightMouseMoved(const QGraphicsSceneMouseEvent* event) +{ + auto pos = MathUtils::floor(event->scenePos()); + paintPixel(pos, !_eraseByDefault); + _lastPixelPos = pos; + return InputEventResult::Process; +} + +VisualSceneInputHandler::InputEventResult PaintbrushTool::rightMouseReleased(const QGraphicsSceneMouseEvent* event) +{ + Q_UNUSED(event); + + // Painting pixels in a single stroke have now been merged into one command + _imageEditor->controller().undoStack().endMerging(); + + return InputEventResult::Process; +} + +void PaintbrushTool::paintPixel(QPoint pos, bool erase) const +{ + // Draw a straight line from the current position to the last painted pixel + int width = *penWidth(); + + if (_eraseByDefault) + { + auto adjustedWidth = static_cast<qreal>(width) / _imageEditor->controller().activeScene()->view()->getZoom(); + width = static_cast<int>(std::ceil(adjustedWidth)); + + if (width <= 0) + width = 1; + } + + _imageEditor->controller().paintPixels(nullptr, ImageUtils::getLinePoints(pos, _lastPixelPos, width), !erase ? _imageEditor->environment().getPrimaryColor() : QColor{}); +} diff --git a/Grinder/ui/image/tools/PaintbrushTool.h b/Grinder/ui/image/tools/PaintbrushTool.h index 0761ea1..e5b4fcd 100644 --- a/Grinder/ui/image/tools/PaintbrushTool.h +++ b/Grinder/ui/image/tools/PaintbrushTool.h @@ -1,53 +1,53 @@ -/****************************************************************************** - * File: PaintbrushTool.h - * Date: 21.5.2018 - *****************************************************************************/ - -#ifndef PAINTBRUSHTOOL_H -#define PAINTBRUSHTOOL_H - -#include "ui/image/ImageEditorTool.h" - -namespace grndr -{ - class PaintbrushTool : public ImageEditorTool - { - Q_OBJECT - - public: - static const char* tool_type; - - public: - PaintbrushTool(ImageEditor* imageEditor); - PaintbrushTool(ImageEditor* imageEditor, QString name, QString icon, QString shortcut, QCursor cursor = QCursor{Qt::ArrowCursor}); - - public: - virtual QString getToolType() const override { return tool_type; } - - public: - auto penWidth() { return dynamic_cast<UIntProperty*>(_penWidth.get()); } - auto penWidth() const { return dynamic_cast<const UIntProperty*>(_penWidth.get()); } - - protected: - virtual void createProperties() override; - - protected: - virtual InputEventResult mousePressed(const QGraphicsSceneMouseEvent* event) override; - virtual InputEventResult mouseMoved(const QGraphicsSceneMouseEvent* event) override; - virtual InputEventResult mouseReleased(const QGraphicsSceneMouseEvent* event) override { Q_UNUSED(event); return InputEventResult::Process; } - virtual InputEventResult rightMousePressed(const QGraphicsSceneMouseEvent* event) override; - virtual InputEventResult rightMouseMoved(const QGraphicsSceneMouseEvent* event) override; - virtual InputEventResult rightMouseReleased(const QGraphicsSceneMouseEvent* event) override { Q_UNUSED(event); return InputEventResult::Process; } - - protected: - void paintPixel(QPoint pos, bool erase = false) const; - - protected: - std::shared_ptr<PropertyBase> _penWidth; - - bool _eraseByDefault{false}; - QPoint _lastPixelPos; - }; -} - -#endif +/****************************************************************************** + * File: PaintbrushTool.h + * Date: 21.5.2018 + *****************************************************************************/ + +#ifndef PAINTBRUSHTOOL_H +#define PAINTBRUSHTOOL_H + +#include "ui/image/ImageEditorTool.h" + +namespace grndr +{ + class PaintbrushTool : public ImageEditorTool + { + Q_OBJECT + + public: + static const char* tool_type; + + public: + PaintbrushTool(ImageEditor* imageEditor); + PaintbrushTool(ImageEditor* imageEditor, QString name, QString icon, QString shortcut, QCursor cursor = QCursor{Qt::ArrowCursor}); + + public: + virtual QString getToolType() const override { return tool_type; } + + public: + auto penWidth() { return dynamic_cast<UIntProperty*>(_penWidth.get()); } + auto penWidth() const { return dynamic_cast<const UIntProperty*>(_penWidth.get()); } + + protected: + virtual void createProperties() override; + + protected: + virtual InputEventResult mousePressed(const QGraphicsSceneMouseEvent* event) override; + virtual InputEventResult mouseMoved(const QGraphicsSceneMouseEvent* event) override; + virtual InputEventResult mouseReleased(const QGraphicsSceneMouseEvent* event) override; + virtual InputEventResult rightMousePressed(const QGraphicsSceneMouseEvent* event) override; + virtual InputEventResult rightMouseMoved(const QGraphicsSceneMouseEvent* event) override; + virtual InputEventResult rightMouseReleased(const QGraphicsSceneMouseEvent* event) override; + + protected: + void paintPixel(QPoint pos, bool erase = false) const; + + protected: + std::shared_ptr<PropertyBase> _penWidth; + + bool _eraseByDefault{false}; + QPoint _lastPixelPos; + }; +} + +#endif -- GitLab