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