From 02657d41ccfe35e43e71dc75c04a3a73fa2343b7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20M=C3=BCller?= <d_muel20@uni-muenster.de>
Date: Fri, 4 May 2018 20:20:38 +0200
Subject: [PATCH] * Image tags can now be assigned to image builds and draft
 items

---
 Grinder/Grinder.pro                           |  10 +-
 Grinder/Version.h                             |   4 +-
 Grinder/common/ObjectVector.h                 |   8 +-
 Grinder/common/ObjectVector.impl.h            |  10 +-
 Grinder/common/properties/ObjectProperty.h    |   3 +
 .../common/properties/ObjectProperty.impl.h   |  20 ++++
 Grinder/common/properties/PropertyBase.h      |   2 +
 Grinder/common/properties/PropertyID.cpp      |   1 +
 Grinder/common/properties/PropertyID.h        |   1 +
 Grinder/engine/ProcessorBase.cpp              |  24 ++--
 Grinder/image/DraftItem.cpp                   |   4 +
 Grinder/image/DraftItem.h                     |   4 +
 Grinder/image/ImageBuild.cpp                  |  33 ++++++
 Grinder/image/ImageBuild.h                    |  29 ++++-
 Grinder/image/ImageBuildPool.cpp              |   7 ++
 Grinder/image/tags/ImageTags.cpp              |   5 -
 Grinder/image/tags/ImageTags.h                |   3 -
 Grinder/image/tags/ImageTagsAllotment.cpp     | 103 ++++++++++++++++++
 Grinder/image/tags/ImageTagsAllotment.h       |  66 +++++++++++
 .../image/tags/ImageTagsAllotmentProperty.cpp |  35 ++++++
 .../image/tags/ImageTagsAllotmentProperty.h   |  32 ++++++
 Grinder/image/tags/ImageTagsProperty.cpp      |  18 ---
 Grinder/image/tags/ImageTagsProperty.h        |   3 -
 Grinder/pipeline/Block.cpp                    |  31 ++++++
 Grinder/pipeline/Block.h                      |  14 ++-
 Grinder/pipeline/Block.impl.h                 |  14 +--
 Grinder/res/Grinder.qrc                       |   1 +
 Grinder/res/Resources.h                       |   1 +
 Grinder/res/css/controlBar.css                | 103 +-----------------
 Grinder/ui/dlg/CheckListDialog.cpp            |  21 +++-
 Grinder/ui/dlg/CheckListDialog.h              |  10 +-
 Grinder/ui/dlg/CheckListDialog.impl.h         |  31 ++----
 .../ui/image/ImageEditorPropertyWidget.cpp    |   2 +-
 Grinder/ui/image/ImageEditorWidget.cpp        |  23 +++-
 Grinder/ui/image/ImageEditorWidget.h          |   4 +
 Grinder/ui/image/tags/ImageTagsListWidget.cpp |  24 +---
 Grinder/ui/image/tools/DraftItemTool.cpp      |  12 ++
 Grinder/ui/image/tools/DraftItemTool.h        |   7 ++
 .../ui/properties/PropertyTreeItemDelegate.h  |   2 +-
 .../ui/properties/ValuePropertyTreeItem.cpp   |   1 -
 .../properties/editors/DialogPropertyEditor.h |   5 +-
 .../editors/DialogPropertyEditor.impl.h       |  13 ++-
 .../ImageTagsAllotmentPropertyEditor.cpp      |  41 +++++++
 .../ImageTagsAllotmentPropertyEditor.h        |  26 +++++
 .../editors/ImageTagsPropertyEditor.h         |   2 +-
 Grinder/ui/widgets/CheckListWidget.h          |   3 +
 Grinder/ui/widgets/CheckListWidget.impl.h     |  32 ++++++
 Grinder/ui/widgets/ObjectListWidget.h         |   2 +-
 48 files changed, 620 insertions(+), 230 deletions(-)
 create mode 100644 Grinder/image/tags/ImageTagsAllotment.cpp
 create mode 100644 Grinder/image/tags/ImageTagsAllotment.h
 create mode 100644 Grinder/image/tags/ImageTagsAllotmentProperty.cpp
 create mode 100644 Grinder/image/tags/ImageTagsAllotmentProperty.h
 create mode 100644 Grinder/ui/properties/editors/ImageTagsAllotmentPropertyEditor.cpp
 create mode 100644 Grinder/ui/properties/editors/ImageTagsAllotmentPropertyEditor.h

diff --git a/Grinder/Grinder.pro b/Grinder/Grinder.pro
index 2351804..0ccdc20 100644
--- a/Grinder/Grinder.pro
+++ b/Grinder/Grinder.pro
@@ -211,7 +211,10 @@ SOURCES += \
     ui/image/tags/ImageTagsDialog.cpp \
     ui/properties/editors/ImageTagsPropertyEditor.cpp \
     ui/image/tags/EditableImageTagsListWidget.cpp \
-    ui/image/tags/ImageTagsListWidget.cpp
+    ui/image/tags/ImageTagsListWidget.cpp \
+    image/tags/ImageTagsAllotment.cpp \
+    image/tags/ImageTagsAllotmentProperty.cpp \
+    ui/properties/editors/ImageTagsAllotmentPropertyEditor.cpp
 
 HEADERS += \        
 	ui/mainwnd/GrinderWindow.h \
@@ -455,7 +458,10 @@ HEADERS += \
     ui/image/tags/ImageTagsDialog.h \
     ui/properties/editors/ImageTagsPropertyEditor.h \
     ui/image/tags/EditableImageTagsListWidget.h \
-    ui/image/tags/ImageTagsListWidget.h
+    ui/image/tags/ImageTagsListWidget.h \
+    image/tags/ImageTagsAllotment.h \
+    image/tags/ImageTagsAllotmentProperty.h \
+    ui/properties/editors/ImageTagsAllotmentPropertyEditor.h
 
 FORMS += \        
 	ui/mainwnd/GrinderWindow.ui \
diff --git a/Grinder/Version.h b/Grinder/Version.h
index d09cac3..443017f 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			"03.05.2018"
+#define GRNDR_INFO_DATE			"04.05.2018"
 #define GRNDR_INFO_COMPANY		"WWU Muenster"
 #define GRNDR_INFO_WEBSITE		"http://www.uni-muenster.de"
 
 #define GRNDR_VERSION_MAJOR		0
 #define GRNDR_VERSION_MINOR		3
 #define GRNDR_VERSION_REVISION	0
-#define GRNDR_VERSION_BUILD		149
+#define GRNDR_VERSION_BUILD		154
 
 namespace grndr
 {
diff --git a/Grinder/common/ObjectVector.h b/Grinder/common/ObjectVector.h
index cb0ef7f..8d004e1 100644
--- a/Grinder/common/ObjectVector.h
+++ b/Grinder/common/ObjectVector.h
@@ -36,6 +36,10 @@ namespace grndr
 		template<typename T = vector_type>
 		void deepCopy(const std::enable_if_t<std::is_copy_constructible<ObjType>::value, T>& src);
 
+	public:
+		using std::vector<std::shared_ptr<ObjType>>::erase;
+		void erase(const object_type* obj);
+
 	public:
 		vector_type sorted() const;
 
@@ -48,9 +52,9 @@ namespace grndr
 		auto find(std::function<bool(const object_type*)> pred) const;
 
 		auto indexOf(const pointer_type& obj) const;
-		auto indexOf(const ObjType* obj) const;
+		auto indexOf(const object_type* obj) const;
 		bool contains(const pointer_type& obj) const { return indexOf(obj) != -1; }
-		bool contains(const ObjType* obj) const { return indexOf(obj) != -1; }
+		bool contains(const object_type* obj) const { return indexOf(obj) != -1; }
 
 	public:
 		void serialize(QString elemName, SerializationContext& ctx, std::function<bool(const pointer_type&)> predicate = nullptr) const;
diff --git a/Grinder/common/ObjectVector.impl.h b/Grinder/common/ObjectVector.impl.h
index 40037bd..b22551f 100644
--- a/Grinder/common/ObjectVector.impl.h
+++ b/Grinder/common/ObjectVector.impl.h
@@ -27,6 +27,14 @@ void ObjectVector<ObjType>::deepCopy(const std::enable_if_t<std::is_copy_constru
 	}
 }
 
+template<typename ObjType>
+void ObjectVector<ObjType>::erase(const object_type* obj)
+{
+	auto it = find(obj);
+
+	if (it != this->cend())
+		this->erase(it);
+}
 
 template<typename ObjType>
 typename ObjectVector<ObjType>::vector_type ObjectVector<ObjType>::sorted() const
@@ -80,7 +88,7 @@ auto ObjectVector<ObjType>::find(std::function<bool(const object_type*)> pred) c
 }
 
 template<typename ObjType>
-auto ObjectVector<ObjType>::indexOf(const ObjType* obj) const
+auto ObjectVector<ObjType>::indexOf(const object_type* obj) const
 {
 	auto it = find(obj);
 
diff --git a/Grinder/common/properties/ObjectProperty.h b/Grinder/common/properties/ObjectProperty.h
index b2b2cf5..b26dc57 100644
--- a/Grinder/common/properties/ObjectProperty.h
+++ b/Grinder/common/properties/ObjectProperty.h
@@ -39,6 +39,9 @@ namespace grndr
 		virtual QString toString() const override { return ""; }
 		virtual void fromString(const QString& data) override { Q_UNUSED(data); }
 
+		virtual void serialize(SerializationContext& ctx) const override;
+		virtual void deserialize(DeserializationContext& ctx) override;
+
 	protected:
 		object_type _object{};
 	};
diff --git a/Grinder/common/properties/ObjectProperty.impl.h b/Grinder/common/properties/ObjectProperty.impl.h
index 068a546..6824698 100644
--- a/Grinder/common/properties/ObjectProperty.impl.h
+++ b/Grinder/common/properties/ObjectProperty.impl.h
@@ -24,3 +24,23 @@ void ObjectProperty<ObjType>::copyValue(const PropertyBase* property)
 		}
 	}
 }
+
+template<typename ObjType>
+void ObjectProperty<ObjType>::serialize(SerializationContext& ctx) const
+{
+	PropertyBase::serialize(ctx);
+
+	// Serialize object
+	if (!hasFlag(Flag::ReadOnly))
+		_object.serialize(ctx);
+}
+
+template<typename ObjType>
+void ObjectProperty<ObjType>::deserialize(DeserializationContext& ctx)
+{
+	PropertyBase::deserialize(ctx);
+
+	// Deserialize object
+	if (!hasFlag(Flag::ReadOnly))
+		_object.deserialize(ctx);
+}
diff --git a/Grinder/common/properties/PropertyBase.h b/Grinder/common/properties/PropertyBase.h
index 6e3d54f..2ef5a4b 100644
--- a/Grinder/common/properties/PropertyBase.h
+++ b/Grinder/common/properties/PropertyBase.h
@@ -38,6 +38,8 @@ namespace grndr
 		PropertyBase& operator =(const PropertyBase& src) = default;
 		PropertyBase& operator =(PropertyBase&& src) = default;
 
+		bool operator <(const PropertyBase& prop) { return _name < prop._name; }
+
 	public:
 		virtual void initProperty() { }
 
diff --git a/Grinder/common/properties/PropertyID.cpp b/Grinder/common/properties/PropertyID.cpp
index 7c13bb2..8dd58f9 100644
--- a/Grinder/common/properties/PropertyID.cpp
+++ b/Grinder/common/properties/PropertyID.cpp
@@ -24,3 +24,4 @@ const char* PropertyID::Size = "Size";
 const char* PropertyID::LineWidth = "LineWidth";
 const char* PropertyID::HasDirection = "HasDirection";
 const char* PropertyID::Direction = "Direction";
+const char* PropertyID::ImageTagsAllotment = "ImageTagsAllotment";
diff --git a/Grinder/common/properties/PropertyID.h b/Grinder/common/properties/PropertyID.h
index a08f711..fc95e5e 100644
--- a/Grinder/common/properties/PropertyID.h
+++ b/Grinder/common/properties/PropertyID.h
@@ -31,6 +31,7 @@ namespace grndr
 		static const char* LineWidth;
 		static const char* HasDirection;
 		static const char* Direction;
+		static const char* ImageTagsAllotment;
 
 	public:
 		using QString::QString;
diff --git a/Grinder/engine/ProcessorBase.cpp b/Grinder/engine/ProcessorBase.cpp
index 13dedae..479bcc2 100644
--- a/Grinder/engine/ProcessorBase.cpp
+++ b/Grinder/engine/ProcessorBase.cpp
@@ -7,6 +7,7 @@
 #include "ProcessorBase.h"
 #include "EngineExceptions.h"
 #include "pipeline/Block.h"
+#include "pipeline/PipelineExceptions.h"
 
 ProcessorBase::ProcessorBase(const Block* block) :
 	_block{block}
@@ -70,22 +71,17 @@ void ProcessorBase::throwProcessorException(QString what) const
 
 const Port* ProcessorBase::resolveDataPort(const Port* port, bool required) const
 {
-	auto outPort = port;
+	try {
+		auto dataPort = _block->dataPort(port);
 
-	// If the given port is an in-port, find the out-port it is connected to
-	if (port->isIn())
-	{
-		outPort = nullptr;
-
-		auto connections = port->getConnections(Port::Direction::In);
-
-		if (connections.size() == 1)
-			outPort = connections.at(0)->sourcePort();
-		else if (connections.size() == 0 && required)
+		if (!dataPort && required)
 			throwProcessorException(QString{"Port '%1' is not connected to a source"}.arg(port->getName()));
-		else if (connections.size() > 1)
-			throwProcessorException(QString{"Port '%1' is connected to more than one source"}.arg(port->getName()));	// Should never happen
+
+		return dataPort;
+	} catch (BlockException& e) {
+		// Re-throw the exception
+		throwProcessorException(GetExceptionMessage(e.what()));
 	}
 
-	return outPort;
+	return nullptr;
 }
diff --git a/Grinder/image/DraftItem.cpp b/Grinder/image/DraftItem.cpp
index aaded77..db9c61b 100644
--- a/Grinder/image/DraftItem.cpp
+++ b/Grinder/image/DraftItem.cpp
@@ -56,4 +56,8 @@ void DraftItem::createProperties()
 	_direction = createProperty<AngleProperty>(PropertyID::Direction, "Direction", 0.0);
 	direction()->createConstraint<RangeConstraint>(0, 360);
 	direction()->setDescription("The direction/orientation of the box contents.");
+
+	_imageTagsAllotment = createProperty<ImageTagsAllotmentProperty>(PropertyID::ImageTagsAllotment, "Image tags");
+	imageTagsAllotment()->setDescription("The assigned image tags.");
+	imageTagsAllotment()->object().setImageBuild(_layer->imageBuild());
 }
diff --git a/Grinder/image/DraftItem.h b/Grinder/image/DraftItem.h
index a16ab4f..46dfa05 100644
--- a/Grinder/image/DraftItem.h
+++ b/Grinder/image/DraftItem.h
@@ -9,6 +9,7 @@
 #include "common/properties/PropertyObject.h"
 #include "DraftItemType.h"
 #include "DraftItemRendererBase.h"
+#include "tags/ImageTagsAllotmentProperty.h"
 
 namespace grndr
 {
@@ -45,6 +46,8 @@ namespace grndr
 		auto hasDirection() const { return dynamic_cast<BoolProperty*>(_hasDirection.get()); }
 		auto direction() { return dynamic_cast<AngleProperty*>(_direction.get()); }
 		auto direction() const { return dynamic_cast<AngleProperty*>(_direction.get()); }
+		auto imageTagsAllotment() { return dynamic_cast<ImageTagsAllotmentProperty*>(_imageTagsAllotment.get()); }
+		auto imageTagsAllotment() const { return dynamic_cast<ImageTagsAllotmentProperty*>(_imageTagsAllotment.get()); }
 
 	public:
 		virtual void setDefaultPropertyValues() { }
@@ -68,6 +71,7 @@ namespace grndr
 		std::shared_ptr<PropertyBase> _position;
 		std::shared_ptr<PropertyBase> _hasDirection;
 		std::shared_ptr<PropertyBase> _direction;
+		std::shared_ptr<PropertyBase> _imageTagsAllotment;
 	};
 }
 
diff --git a/Grinder/image/ImageBuild.cpp b/Grinder/image/ImageBuild.cpp
index 7f6ebf1..2c74c1b 100644
--- a/Grinder/image/ImageBuild.cpp
+++ b/Grinder/image/ImageBuild.cpp
@@ -6,6 +6,8 @@
 #include "Grinder.h"
 #include "ImageBuild.h"
 #include "ImageExceptions.h"
+#include "pipeline/Block.h"
+#include "image/tags/ImageTagsProperty.h"
 
 const char* ImageBuild::Serialization_Value_ImageReferences = "ImageReference";	// Not called ImageReferences due to backwards compatibility
 
@@ -17,6 +19,18 @@ ImageBuild::ImageBuild(const Block* block, const std::vector<const ImageReferenc
 
 	if (imageReferences.empty())
 		throw std::invalid_argument{_EXCPT("imageReferences may not be empty")};
+
+	// Listen for connection events on the image tags in-port
+	if (auto imageTagsInPort = _block->ports().selectByType(PortType::ImageTagsIn))
+	{
+		connect(imageTagsInPort.get(), SIGNAL(portConnected(const Connection*)), this, SLOT(imageTagsConnectionChanged()), Qt::QueuedConnection);
+		connect(imageTagsInPort.get(), SIGNAL(portDisconnected(const Connection*)), this, SLOT(imageTagsConnectionChanged()), Qt::QueuedConnection);
+	}
+}
+
+void ImageBuild::initImageBuild()
+{
+	createProperties();
 }
 
 std::shared_ptr<Layer> ImageBuild::createLayer(QString name)
@@ -88,8 +102,18 @@ void ImageBuild::moveLayer(const Layer* layer, bool up)
 		throw ImageBuildException{this, _EXCPT("Tried to move a layer not belonging to this image build")};
 }
 
+ImageTags* ImageBuild::inputImageTags() const
+{
+	if (auto imageTagsProperty = _block->portProperty<ImageTagsProperty>(PortType::ImageTagsIn, PropertyID::ImageTags))
+		return &imageTagsProperty->object();
+	else
+		return nullptr;
+}
+
 void ImageBuild::serialize(SerializationContext& ctx) const
 {
+	PropertyObject::serialize(ctx);
+
 	// Serialize image references
 	if (ctx.getMode() == SerializationContext::Mode::ProjectSerialization)
 	{
@@ -109,6 +133,8 @@ void ImageBuild::serialize(SerializationContext& ctx) const
 
 void ImageBuild::deserialize(DeserializationContext& ctx)
 {
+	PropertyObject::deserialize(ctx);
+
 	// Deserialize all layers
 	if (ctx.beginGroup(LayerVector::Serialization_Group))
 	{
@@ -120,3 +146,10 @@ void ImageBuild::deserialize(DeserializationContext& ctx)
 		ctx.endGroup();
 	}
 }
+
+void ImageBuild::createProperties()
+{
+	_imageTagsAllotment = createProperty<ImageTagsAllotmentProperty>(PropertyID::ImageTagsAllotment, "Image tags");
+	imageTagsAllotment()->setDescription("The assigned image tags.");
+	imageTagsAllotment()->object().setImageBuild(this);
+}
diff --git a/Grinder/image/ImageBuild.h b/Grinder/image/ImageBuild.h
index 650a00a..45b66f9 100644
--- a/Grinder/image/ImageBuild.h
+++ b/Grinder/image/ImageBuild.h
@@ -8,13 +8,16 @@
 
 #include <opencv2/core.hpp>
 
+#include "common/properties/PropertyObject.h"
 #include "LayerVector.h"
+#include "tags/ImageTagsAllotmentProperty.h"
 
 namespace grndr
 {
 	class ImageReference;
+	class ImageTags;
 
-	class ImageBuild : public QObject
+	class ImageBuild : public PropertyObject
 	{
 		Q_OBJECT
 
@@ -24,6 +27,9 @@ namespace grndr
 	public:
 		ImageBuild(const Block* block, const std::vector<const ImageReference*>& imageReferences);
 
+	public:
+		void initImageBuild();
+
 	public:
 		std::shared_ptr<Layer> createLayer(QString name = "");
 		void removeLayer(const Layer* layer);
@@ -41,16 +47,29 @@ namespace grndr
 
 		const LayerVector& layers() const { return _layers; }
 
+		ImageTags* inputImageTags() const;
+
+		auto imageTagsAllotment() { return dynamic_cast<ImageTagsAllotmentProperty*>(_imageTagsAllotment.get()); }
+		auto imageTagsAllotment() const { return dynamic_cast<ImageTagsAllotmentProperty*>(_imageTagsAllotment.get()); }
+
 	public:
-		void serialize(SerializationContext& ctx) const;
-		void deserialize(DeserializationContext& ctx);
+		void serialize(SerializationContext& ctx) const override;
+		void deserialize(DeserializationContext& ctx) override;
 
 	signals:
 		void layerCreated(const std::shared_ptr<Layer>&);
 		void layerRemoved(const std::shared_ptr<Layer>&);
 		void layerMoved(const std::shared_ptr<Layer>&, int, int);
 
-		void imageDataChanged();	
+		void imageDataChanged();
+
+		void inputImageTagsChanged(const ImageTags*) const;
+
+	protected:
+		virtual void createProperties() override;
+
+	private slots:
+		void imageTagsConnectionChanged() const { emit inputImageTagsChanged(inputImageTags()); }
 
 	private:
 		const Block* _block{nullptr};
@@ -58,6 +77,8 @@ namespace grndr
 		cv::Mat _imageData;
 
 		LayerVector _layers;
+
+		std::shared_ptr<PropertyBase> _imageTagsAllotment;
 	};
 }
 
diff --git a/Grinder/image/ImageBuildPool.cpp b/Grinder/image/ImageBuildPool.cpp
index 477e6c9..527c355 100644
--- a/Grinder/image/ImageBuildPool.cpp
+++ b/Grinder/image/ImageBuildPool.cpp
@@ -171,6 +171,13 @@ std::shared_ptr<ImageBuild> ImageBuildPool::imageBuild(std::shared_ptr<ImageBuil
 	{
 		auto build = std::make_shared<ImageBuild>(block, imageReferences);
 
+		try {	// Propagate initialization errors to the caller
+			build->initImageBuild();
+		}
+		catch (...) {
+			throw;
+		}
+
 		builds->push_back(build);
 		emit imageBuildCreated(build);
 
diff --git a/Grinder/image/tags/ImageTags.cpp b/Grinder/image/tags/ImageTags.cpp
index d811d3a..d3cafde 100644
--- a/Grinder/image/tags/ImageTags.cpp
+++ b/Grinder/image/tags/ImageTags.cpp
@@ -7,11 +7,6 @@
 #include "ImageTags.h"
 #include "image/ImageExceptions.h"
 
-ImageTags::ImageTags(const ImageTags& imageTags) : QObject()
-{
-	_tags.deepCopy(imageTags._tags);
-}
-
 ImageTags& ImageTags::operator =(const ImageTags& imageTags)
 {
 	_tags.deepCopy(imageTags._tags);
diff --git a/Grinder/image/tags/ImageTags.h b/Grinder/image/tags/ImageTags.h
index 9c9e6e1..3202331 100644
--- a/Grinder/image/tags/ImageTags.h
+++ b/Grinder/image/tags/ImageTags.h
@@ -21,9 +21,6 @@ namespace grndr
 		static const char* Serialization_Element;
 
 	public:
-		ImageTags() { }
-		ImageTags(const ImageTags& imageTags);
-
 		ImageTags& operator =(const ImageTags& imageTags);
 
 	public:
diff --git a/Grinder/image/tags/ImageTagsAllotment.cpp b/Grinder/image/tags/ImageTagsAllotment.cpp
new file mode 100644
index 0000000..6845e22
--- /dev/null
+++ b/Grinder/image/tags/ImageTagsAllotment.cpp
@@ -0,0 +1,103 @@
+/******************************************************************************
+ * File: ImageTagsAllotment.cpp
+ * Date: 03.5.2018
+ *****************************************************************************/
+
+#include "Grinder.h"
+#include "ImageTagsAllotment.h"
+#include "ImageTags.h"
+#include "image/ImageBuild.h"
+
+const char* ImageTagsAllotment::Serialization_Value_AssignedTags = "AssignedTags";
+
+ImageTagsAllotment& ImageTagsAllotment::operator =(const ImageTagsAllotment& imageTagsAllotment)
+{
+	unassignAllTags();
+
+	// Only copy the assigned tags if both allotments use the same input image tags
+	if (_inputImageTags == imageTagsAllotment._inputImageTags)
+	{
+		_assignedTags = imageTagsAllotment._assignedTags;
+		emit allotmentChanged();
+	}
+
+	return *this;
+}
+
+void ImageTagsAllotment::setImageBuild(const ImageBuild* imageBuild)
+{
+	// Disconnect any previously connected signals from the image build
+	if (_imageBuild)
+		disconnect(_imageBuild, nullptr, this, nullptr);
+
+	_imageBuild = imageBuild;
+
+	setInputImageTags(_imageBuild->inputImageTags());
+
+	// Listen for connection signals from the block's image tags in-port
+	connect(_imageBuild, &ImageBuild::inputImageTagsChanged, this, &ImageTagsAllotment::inputImageTagsChanged);
+}
+
+void ImageTagsAllotment::assignTag(ImageTag* imageTag)
+{
+	if (_inputImageTags && _inputImageTags->tags().contains(imageTag))
+	{
+		_assignedTags.emplace(imageTag);
+		emit allotmentChanged();
+	}
+}
+
+void ImageTagsAllotment::unassignTag(ImageTag* imageTag)
+{
+	_assignedTags.erase(imageTag);
+	emit allotmentChanged();
+}
+
+void ImageTagsAllotment::serialize(SerializationContext& ctx) const
+{
+	// Store the names of all assigned image tags; since tag names must be unique, this can be used to identify each tag
+	QStringList assignedTags;
+
+	for (const auto& imageTag : _assignedTags)
+		assignedTags << imageTag->getName();
+
+	ctx.settings()[Serialization_Value_AssignedTags] = assignedTags.join(",");
+}
+
+void ImageTagsAllotment::deserialize(DeserializationContext& ctx)
+{
+	unassignAllTags();
+
+	if (_inputImageTags)
+	{
+		// Search for each tag name in the current input image tags
+		for (auto tagName : ctx.settings()[Serialization_Value_AssignedTags].toString().split(","))
+		{
+			if (auto imageTag = _inputImageTags->tags().selectByName(tagName))
+				assignTag(imageTag.get());
+		}
+	}
+}
+
+void ImageTagsAllotment::setInputImageTags(const ImageTags* imageTags)
+{
+	if (imageTags == _inputImageTags)
+		return;
+
+	if (_inputImageTags)
+		disconnect(_inputImageTags, nullptr, this, nullptr);
+
+	// The input image tags have changed, so clear any assigned tags
+	unassignAllTags();
+
+	_inputImageTags = imageTags;
+
+	// If a tag has been removed, remove it from the allotment as well
+	if (_inputImageTags)
+		connect(_inputImageTags, &ImageTags::tagRemoved, this, &ImageTagsAllotment::imageTagRemoved);
+}
+
+void ImageTagsAllotment::imageTagRemoved(const std::shared_ptr<ImageTag>& imageTag)
+{
+	unassignTag(imageTag.get());
+}
diff --git a/Grinder/image/tags/ImageTagsAllotment.h b/Grinder/image/tags/ImageTagsAllotment.h
new file mode 100644
index 0000000..1d87821
--- /dev/null
+++ b/Grinder/image/tags/ImageTagsAllotment.h
@@ -0,0 +1,66 @@
+/******************************************************************************
+ * File: ImageTagsAllotment.h
+ * Date: 03.5.2018
+ *****************************************************************************/
+
+#ifndef IMAGETAGSALLOTMENT_H
+#define IMAGETAGSALLOTMENT_H
+
+#include <set>
+
+#include "common/serialization/SerializationContext.h"
+#include "common/serialization/DeserializationContext.h"
+
+namespace grndr
+{
+	class ImageBuild;
+	class ImageTag;
+	class ImageTags;
+
+	class ImageTagsAllotment : public QObject
+	{
+		Q_OBJECT
+
+	public:
+		static const char* Serialization_Value_AssignedTags;
+
+	public:
+		ImageTagsAllotment& operator =(const ImageTagsAllotment& imageTagsAllotment);
+
+	public:
+		void setImageBuild(const ImageBuild* imageBuild);
+
+	public:
+		const ImageTags* inputImageTags() const { return _inputImageTags; }
+
+	public:
+		void assignTag(ImageTag* imageTag);
+		void unassignTag(ImageTag* imageTag);
+		void unassignAllTags() { _assignedTags.clear(); emit allotmentChanged(); }
+
+		const std::set<ImageTag*>& assignedTags() const { return _assignedTags; }
+
+	public:
+		void serialize(SerializationContext& ctx) const;
+		void deserialize(DeserializationContext& ctx);
+
+	signals:
+		void allotmentChanged();
+
+	private:
+		void setInputImageTags(const ImageTags* imageTags);
+
+	private slots:
+		void inputImageTagsChanged(const ImageTags* imageTags) { setInputImageTags(imageTags); }
+
+		void imageTagRemoved(const std::shared_ptr<ImageTag>& imageTag);
+
+	private:
+		const ImageBuild* _imageBuild{nullptr};
+
+		const ImageTags* _inputImageTags{nullptr};
+		std::set<ImageTag*> _assignedTags;
+	};
+}
+
+#endif
diff --git a/Grinder/image/tags/ImageTagsAllotmentProperty.cpp b/Grinder/image/tags/ImageTagsAllotmentProperty.cpp
new file mode 100644
index 0000000..677203b
--- /dev/null
+++ b/Grinder/image/tags/ImageTagsAllotmentProperty.cpp
@@ -0,0 +1,35 @@
+/******************************************************************************
+ * File: ImageTagsAllotmentProperty.cpp
+ * Date: 3.5.2018
+ *****************************************************************************/
+
+#include "Grinder.h"
+#include "ImageTagsAllotmentProperty.h"
+#include "image/tags/ImageTag.h"
+#include "ui/properties/editors/ImageTagsAllotmentPropertyEditor.h"
+
+void ImageTagsAllotmentProperty::initProperty()
+{
+	ObjectProperty::initProperty();
+
+	// Whenever the allotment has changed, notify the property about this
+	connect(&_object, &ImageTagsAllotment::allotmentChanged, this, &ImageTagsAllotmentProperty::objectModified);
+}
+
+QWidget* ImageTagsAllotmentProperty::createEditor(QWidget* parent)
+{
+	return new ImageTagsAllotmentPropertyEditor{this, parent};
+}
+
+QString ImageTagsAllotmentProperty::toString() const
+{
+	QStringList tagNames;
+
+	for (const auto& imageTag : _object.assignedTags())
+		tagNames << imageTag->getName();
+
+	if (!tagNames.isEmpty())
+		return tagNames.join(", ");
+	else
+		return "No tags";
+}
diff --git a/Grinder/image/tags/ImageTagsAllotmentProperty.h b/Grinder/image/tags/ImageTagsAllotmentProperty.h
new file mode 100644
index 0000000..650089f
--- /dev/null
+++ b/Grinder/image/tags/ImageTagsAllotmentProperty.h
@@ -0,0 +1,32 @@
+/******************************************************************************
+ * File: ImageTagsAllotmentProperty.h
+ * Date: 3.5.2018
+ *****************************************************************************/
+
+#ifndef IMAGETAGSALLOTMENTPROPERTY_H
+#define IMAGETAGSALLOTMENTPROPERTY_H
+
+#include "common/properties/ObjectProperty.h"
+#include "image/tags/ImageTagsAllotment.h"
+
+namespace grndr
+{
+	class ImageTagsAllotmentProperty : public ObjectProperty<ImageTagsAllotment>
+	{
+		Q_OBJECT
+
+	public:
+		using ObjectProperty::ObjectProperty;
+
+	public:
+		virtual void initProperty() override;
+
+	public:
+		virtual QWidget* createEditor(QWidget* parent) override;
+
+	public:
+		virtual QString toString() const override;
+	};
+}
+
+#endif
diff --git a/Grinder/image/tags/ImageTagsProperty.cpp b/Grinder/image/tags/ImageTagsProperty.cpp
index a858e8a..7113e0d 100644
--- a/Grinder/image/tags/ImageTagsProperty.cpp
+++ b/Grinder/image/tags/ImageTagsProperty.cpp
@@ -16,21 +16,3 @@ QString ImageTagsProperty::toString() const
 {
 	return QString{"%1 tag(s)"}.arg(_object.tags().size());
 }
-
-void ImageTagsProperty::serialize(SerializationContext& ctx) const
-{
-	ObjectProperty::serialize(ctx);
-
-	// Serialize tags
-	if (!hasFlag(Flag::ReadOnly))
-		_object.serialize(ctx);
-}
-
-void ImageTagsProperty::deserialize(DeserializationContext& ctx)
-{
-	ObjectProperty::deserialize(ctx);
-
-	// Deserialize tags
-	if (!hasFlag(Flag::ReadOnly))
-		_object.deserialize(ctx);
-}
diff --git a/Grinder/image/tags/ImageTagsProperty.h b/Grinder/image/tags/ImageTagsProperty.h
index c1d6fe8..8abd177 100644
--- a/Grinder/image/tags/ImageTagsProperty.h
+++ b/Grinder/image/tags/ImageTagsProperty.h
@@ -23,9 +23,6 @@ namespace grndr
 
 	public:
 		virtual QString toString() const override;
-
-		virtual void serialize(SerializationContext& ctx) const override;
-		virtual void deserialize(DeserializationContext& ctx) override;
 	};
 }
 
diff --git a/Grinder/pipeline/Block.cpp b/Grinder/pipeline/Block.cpp
index 6bcc55e..ee20b2f 100644
--- a/Grinder/pipeline/Block.cpp
+++ b/Grinder/pipeline/Block.cpp
@@ -75,6 +75,37 @@ std::set<const ImageReference*> Block::assembleInputImageReferences(const ImageR
 	return imageReferences;
 }
 
+const Port* Block::dataPort(PortType portType) const
+{
+	if (auto port = _ports.selectByType(portType))
+		return dataPort(port.get());
+	else
+		return nullptr;
+}
+
+const Port* Block::dataPort(const Port* port) const
+{
+	if (!_ports.contains(port))
+		throw BlockException{this, _EXCPT("Tried to retrieve the data port for a port not belonging to this block")};
+
+	auto outPort = port;
+
+	// If the given port is an in-port, find the out-port it is connected to
+	if (port->isIn())
+	{
+		outPort = nullptr;
+
+		auto connections = port->getConnections(Port::Direction::In);
+
+		if (connections.size() == 1)
+			outPort = connections.at(0)->sourcePort();
+		else if (connections.size() > 1)
+			throw BlockException{this, QString{"Port '%1' is connected to more than one source"}.arg(port->getName())};	// Should never happen
+	}
+
+	return outPort;
+}
+
 QString Block::getFormattedName() const
 {
 	return QString{"%1::%2"}.arg(_pipeline->getName()).arg(_name);
diff --git a/Grinder/pipeline/Block.h b/Grinder/pipeline/Block.h
index d11ebaa..9dd96cc 100644
--- a/Grinder/pipeline/Block.h
+++ b/Grinder/pipeline/Block.h
@@ -42,17 +42,21 @@ namespace grndr
 		std::set<const ImageReference*> assembleInputImageReferences(const ImageReference* activeImageRef) const;
 
 	public:
-		QString getFormattedName() const;
-		BlockType getType() const { return _type; }
-		BlockCategory getCategory() const { return _category; }
-
-		const PortVector& ports() const { return _ports; }
+		const Port* dataPort(PortType portType) const;
+		const Port* dataPort(const Port* port) const;
 
 		template<typename PropType>
 		PropType* portProperty(PortType portType, PropertyID propertyID) const;
 		template<typename PropType>
 		PropType* portProperty(const Port* port, PropertyID propertyID) const;
 
+	public:
+		QString getFormattedName() const;
+		BlockType getType() const { return _type; }
+		BlockCategory getCategory() const { return _category; }
+
+		const PortVector& ports() const { return _ports; }
+
 	public:
 		virtual void serialize(SerializationContext& ctx) const override;
 		virtual void deserialize(DeserializationContext& ctx) override;
diff --git a/Grinder/pipeline/Block.impl.h b/Grinder/pipeline/Block.impl.h
index 0fdc9c1..e30301c 100644
--- a/Grinder/pipeline/Block.impl.h
+++ b/Grinder/pipeline/Block.impl.h
@@ -18,19 +18,7 @@ PropType* Block::portProperty(PortType portType, PropertyID propertyID) const
 template<typename PropType>
 PropType* Block::portProperty(const Port* port, PropertyID propertyID) const
 {
-	auto outPort = port;
-
-	if (port->isIn())	// Get property from the connected block (if any)
-	{
-		outPort = nullptr;
-
-		auto connections = port->getConnections(Port::Direction::In);
-
-		if (connections.size() == 1)
-			outPort = connections.at(0)->sourcePort();
-	}
-
-	if (outPort)
+	if (auto outPort = dataPort(port))
 		return outPort->block()->properties().property<PropType>(propertyID);
 	else
 		return nullptr;
diff --git a/Grinder/res/Grinder.qrc b/Grinder/res/Grinder.qrc
index 3153946..9730a49 100644
--- a/Grinder/res/Grinder.qrc
+++ b/Grinder/res/Grinder.qrc
@@ -42,6 +42,7 @@
         <file>icons/tag-black.png</file>
         <file>icons/check-mark-black-outline.png</file>
         <file>icons/multi_checks.png</file>
+        <file>icons/tags-black-couple-with-rings.png</file>
     </qresource>
     <qresource prefix="/">
         <file>css/global.css</file>
diff --git a/Grinder/res/Resources.h b/Grinder/res/Resources.h
index 34b9d9a..1937fb6 100644
--- a/Grinder/res/Resources.h
+++ b/Grinder/res/Resources.h
@@ -24,6 +24,7 @@
 #define FILE_ICON_IMAGEREFERENCE ":/icons/icons/map-of-roads.png"
 #define FILE_ICON_LAYER ":/icons/icons/documents-empty.png"
 #define FILE_ICON_TAG ":/icons/icons/tag-black.png"
+#define FILE_ICON_TAGS ":/icons/icons/tags-black-couple-with-rings.png"
 
 #define FILE_ICON_ADD ":/icons/icons/plus-sign.png"
 #define FILE_ICON_EDIT ":/icons/icons/edit.png"
diff --git a/Grinder/res/css/controlBar.css b/Grinder/res/css/controlBar.css
index a17f614..bd12c80 100644
--- a/Grinder/res/css/controlBar.css
+++ b/Grinder/res/css/controlBar.css
@@ -1,4 +1,4 @@
-QFrame
+grndr--ControlBar
 {
 	background: %LIGHTBG%;
 	border: 1px solid %BORDER%;
@@ -6,116 +6,23 @@ QFrame
 	margin: 0px;
 }
 
-QFrame QFrame
+grndr--ControlBar QFrame
 {
 	border: none;
 	border-right: 1px solid %LIGHTGRAY%;
 }
 
-QFrame QToolButton
+grndr--ControlBar QToolButton
 {
 	margin-bottom: -1px;	/** !win32 **/
 }
 
-QFrame QToolButton[popupMode="1"]
+grndr--ControlBar QToolButton[popupMode="1"]
 {
 	padding-right: 16px;
 }
 
-QFrame QLabel
-{
-	border: none;
-}
-
-/* ControlBar inside a splitter */
-
-QSplitter QFrame
-{
-	background: %LIGHTBG%;
-	border: 1px solid %BORDER%;
-	padding: 0px;
-	margin: 0px;
-}
-
-QSplitter QFrame QFrame
-{
-	border: none;
-	border-right: 1px solid %LIGHTGRAY%;
-}
-
-QSpitter QFrame QToolButton
-{
-	margin-bottom: -1px;	/** !win32 **/
-}
-
-QSplitter QFrame QToolButton[popupMode="1"]
-{
-	padding-right: 16px;
-}
-
-QSplitter QFrame QLabel
-{
-	border: none;
-}
-
-/* ControlBar inside two splitters */
-
-QSplitter QSplitter QFrame
-{
-	background: %LIGHTBG%;
-	border: 1px solid %BORDER%;
-	padding: 0px;
-	margin: 0px;
-}
-
-QSplitter QSplitter QFrame QFrame
-{
-	border: none;
-	border-right: 1px solid %LIGHTGRAY%;
-}
-
-QSplitter QSpitter QFrame QToolButton
-{
-	margin-bottom: -1px;	/** !win32 **/
-}
-
-QSplitter QSplitter QFrame QToolButton[popupMode="1"]
-{
-	padding-right: 16px;
-}
-
-QSplitter QSplitter QFrame QLabel
-{
-	border: none;
-}
-
-/* ControlBar inside a dialog */
-
-QDialog QFrame
-{
-	background: %LIGHTBG%;
-	border: 1px solid %BORDER%;
-	padding: 0px;
-	margin: 0px;
-}
-
-QDialog QFrame QFrame
-{
-	border: none;
-	border-right: 1px solid %LIGHTGRAY%;
-}
-
-QDialog QFrame QToolButton
-{
-	margin-bottom: -1px;	/** !win32 **/
-}
-
-QDialog QFrame QToolButton[popupMode="1"]
-{
-	padding-right: 16px;
-}
-
-QDialog QFrame QLabel
+grndr--ControlBar QLabel
 {
 	border: none;
 }
diff --git a/Grinder/ui/dlg/CheckListDialog.cpp b/Grinder/ui/dlg/CheckListDialog.cpp
index 2d46903..5fceeec 100644
--- a/Grinder/ui/dlg/CheckListDialog.cpp
+++ b/Grinder/ui/dlg/CheckListDialog.cpp
@@ -7,8 +7,8 @@
 #include "CheckListDialog.h"
 #include "ui_CheckListDialog.h"
 
-CheckListDialog::CheckListDialog(QString dialogTitle, QWidget *parent) : QDialog(parent, Qt::Dialog|Qt::WindowTitleHint|Qt::WindowCloseButtonHint),
-	ui{new Ui::CheckListDialog}
+CheckListDialog::CheckListDialog(QString dialogTitle, bool acceptIfNoItemsChecked, QWidget *parent) : QDialog(parent, Qt::Dialog|Qt::WindowTitleHint|Qt::WindowCloseButtonHint),
+	ui{new Ui::CheckListDialog}, _acceptIfNoItemsChecked{acceptIfNoItemsChecked}
 {
 	ui->setupUi(this);
 
@@ -47,5 +47,20 @@ void CheckListDialog::setCheckListWidget(QListWidget* listWidget)
 void CheckListDialog::checkListItemChanged(QListWidgetItem* item)
 {
 	Q_UNUSED(item);
-	ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!getCheckedItems().empty());
+
+	bool anyItemChecked = false;
+
+	if (!_acceptIfNoItemsChecked)
+	{
+		for (int i = 0; i < _checkListWidget->count(); ++i)
+		{
+			if (_checkListWidget->item(i)->checkState() == Qt::Checked)
+			{
+				anyItemChecked = true;
+				break;
+			}
+		}
+	}
+
+	ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(anyItemChecked || _acceptIfNoItemsChecked);
 }
diff --git a/Grinder/ui/dlg/CheckListDialog.h b/Grinder/ui/dlg/CheckListDialog.h
index b87a67b..447990f 100644
--- a/Grinder/ui/dlg/CheckListDialog.h
+++ b/Grinder/ui/dlg/CheckListDialog.h
@@ -21,12 +21,12 @@ namespace grndr
 		Q_OBJECT
 
 	public:
-		CheckListDialog(QString dialogTitle = "", QWidget* parent = nullptr);
+		CheckListDialog(QString dialogTitle = "", bool acceptIfNoItemsChecked = false, QWidget* parent = nullptr);
 		~CheckListDialog();
 
 	public:
 		template<typename ObjectType, typename ItemType, typename ContainerType>
-		std::vector<ObjectType*> exec(const ContainerType& container);
+		std::vector<ObjectType*> exec(const ContainerType& container, const std::vector<ObjectType*>& checkedObjects = {}, bool* accepted = nullptr);
 
 	private:
 		void setupUi();
@@ -37,11 +37,11 @@ namespace grndr
 	private:
 		void setCheckListWidget(QListWidget* listWidget);
 
-		template<typename ItemType = QListWidgetItem>
-		std::vector<ItemType*> getCheckedItems() const;
-
 	private slots:
 		void checkListItemChanged(QListWidgetItem* item);
+
+	private:
+		bool _acceptIfNoItemsChecked{false};
 	};
 }
 
diff --git a/Grinder/ui/dlg/CheckListDialog.impl.h b/Grinder/ui/dlg/CheckListDialog.impl.h
index c650311..deaee4d 100644
--- a/Grinder/ui/dlg/CheckListDialog.impl.h
+++ b/Grinder/ui/dlg/CheckListDialog.impl.h
@@ -8,40 +8,25 @@
 #include "ui/widgets/CheckListWidget.h"
 
 template<typename ObjectType, typename ItemType, typename ContainerType>
-std::vector<ObjectType*> CheckListDialog::exec(const ContainerType& container)
+std::vector<ObjectType*> CheckListDialog::exec(const ContainerType& container, const std::vector<ObjectType*>& checkedObjects, bool* accepted)
 {
 	// Create a check list widget for the given types and add it to the dialog
 	auto checkListWidget = new CheckListWidget<ObjectType, ItemType>{};
 	setCheckListWidget(checkListWidget);
 
 	checkListWidget->populateList(container);
+	checkListWidget->setCheckedObjects(checkedObjects);
+
+	if (accepted)
+		*accepted = false;
 
 	if (QDialog::exec() == QDialog::Accepted)
 	{
-		std::vector<ObjectType*> checkedObjects;
-
-		for (auto checkedItem : getCheckedItems<ItemType>())
-			checkedObjects.push_back(checkedItem->object());
+		if (accepted)
+			*accepted = true;
 
-		return checkedObjects;
+		return checkListWidget->getCheckedObjects();
 	}
 	else
 		return {};
 }
-
-template<typename ItemType>
-std::vector<ItemType*> CheckListDialog::getCheckedItems() const
-{
-	std::vector<ItemType*> checkedItems;
-
-	for (int i = 0; i < _checkListWidget->count(); ++i)
-	{
-		if (auto item = dynamic_cast<ItemType*>(_checkListWidget->item(i)))
-		{
-			if (item->checkState() == Qt::Checked)
-				checkedItems.push_back(item);
-		}
-	}
-
-	return checkedItems;
-}
diff --git a/Grinder/ui/image/ImageEditorPropertyWidget.cpp b/Grinder/ui/image/ImageEditorPropertyWidget.cpp
index fe2ea35..d79f60d 100644
--- a/Grinder/ui/image/ImageEditorPropertyWidget.cpp
+++ b/Grinder/ui/image/ImageEditorPropertyWidget.cpp
@@ -47,7 +47,7 @@ void ImageEditorPropertyWidget::addProperties(const PropertyVector* properties)
 
 	if (properties)
 	{
-		for (auto& property : *properties)
+		for (auto& property : properties->sorted())
 		{
 			if (!property->hasFlag(PropertyBase::Flag::ReadOnly) && !property->hasFlag(PropertyBase::Flag::Hidden))
 			{
diff --git a/Grinder/ui/image/ImageEditorWidget.cpp b/Grinder/ui/image/ImageEditorWidget.cpp
index 2cf73ae..f919e5b 100644
--- a/Grinder/ui/image/ImageEditorWidget.cpp
+++ b/Grinder/ui/image/ImageEditorWidget.cpp
@@ -10,7 +10,9 @@
 #include "core/GrinderApplication.h"
 #include "image/ImageBuild.h"
 #include "image/ImageExceptions.h"
+#include "image/tags/ImageTags.h"
 #include "ui/mainwnd/ImageReferencesListItem.h"
+#include "ui/properties/editors/ImageTagsAllotmentPropertyEditor.h"
 #include "ui/dlg/CheckListDialog.h"
 #include "ui/StyleSheet.h"
 #include "util/UIUtils.h"
@@ -33,6 +35,8 @@ ImageEditorWidget::ImageEditorWidget(ImageEditor* imageEditor, QWidget* parent)
 	_duplicateImageBuild = 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);
 	_duplicateImageBuildTo = UIUtils::createAction(this, "Copy image build to &multiple images...", "", SLOT(duplicateImageBuildTo()), "Copy the current image build to multiple images (Ctrl+Shift+D)", "Ctrl+Shift+D", Qt::WidgetWithChildrenShortcut);
 
+	_editImageBuildTags = UIUtils::createAction(this, "Edit the image build's tags...", FILE_ICON_TAGS, SLOT(editImageBuildTags()), "Edit the image tags assigned to the current image build (Ctrl+Shift+T)", "Ctrl+Shift+T", Qt::WidgetWithChildrenShortcut);
+
 	// Set up the duplicate image build menu
 	_duplicateImageBuildMenu.addAction(_duplicateImageBuild);
 	_duplicateImageBuildMenu.addSeparator();
@@ -73,6 +77,7 @@ void ImageEditorWidget::setImageBuild(const std::shared_ptr<ImageBuild>& imageBu
 	// Listen for various events to update our actions
 	connect(imageBuild.get(), SIGNAL(layerCreated(const std::shared_ptr<Layer>&)), this, SLOT(updateActions()));
 	connect(imageBuild.get(), SIGNAL(layerRemoved(const std::shared_ptr<Layer>&)), this, SLOT(updateActions()));
+	connect(imageBuild.get(), SIGNAL(inputImageTagsChanged(const ImageTags*)), this, SLOT(updateActions()));
 
 	updateActions();
 }
@@ -146,6 +151,8 @@ void ImageEditorWidget::setupImageControlBar()
 	ui->imageSceneControlBar->addAction(_copyImageBuild, Qt::ToolButtonFollowStyle, Qt::AlignLeft);
 	ui->imageSceneControlBar->addAction(_pasteImageBuild, Qt::ToolButtonFollowStyle, Qt::AlignLeft);
 	ui->imageSceneControlBar->addAction(_duplicateImageBuild, Qt::ToolButtonFollowStyle, Qt::AlignLeft, &_duplicateImageBuildMenu);
+	ui->imageSceneControlBar->addSeparator(Qt::AlignLeft);
+	ui->imageSceneControlBar->addAction(_editImageBuildTags, Qt::ToolButtonFollowStyle, Qt::AlignLeft);
 
 	ui->imageScene->setupUi(static_cast<QMenu*>(nullptr), ui->imageSceneControlBar);
 
@@ -213,6 +220,8 @@ void ImageEditorWidget::updateActions()
 	_copyImageBuild->setEnabled(imageBuild);
 	_duplicateImageBuild->setEnabled(imageBuild && grinder()->project().imageReferences().size() > 1);
 	_pasteImageBuild->setEnabled(imageBuild && grinder()->clipboardManager().hasData(ImageBuildVector::Serialization_Element));
+
+	_editImageBuildTags->setEnabled(_imageEditor->controller().activeImageBuild() && _imageEditor->controller().activeImageBuild()->inputImageTags());
 }
 
 void ImageEditorWidget::updateSceneZoomLevel(qreal zoomLevel)
@@ -282,7 +291,9 @@ void ImageEditorWidget::duplicateImageBuild() const
 void ImageEditorWidget::duplicateImageBuildTo() const
 {
 	CheckListDialog dlg{"Select target images"};
-	auto selectedImages = dlg.exec<ImageReference, ImageReferencesListItem>(grinder()->project().imageReferences().sorted());
+	auto imageReferences = grinder()->project().imageReferences().sorted();
+	imageReferences.erase(grinder()->projectController().activeImageReference());	// Do not include the current image reference in the available targets
+	auto selectedImages = dlg.exec<ImageReference, ImageReferencesListItem>(imageReferences);
 
 	if (!selectedImages.empty())
 	{
@@ -291,6 +302,16 @@ void ImageEditorWidget::duplicateImageBuildTo() const
 	}
 }
 
+void ImageEditorWidget::editImageBuildTags()
+{
+	if (auto imageBuild = _imageEditor->controller().activeImageBuild())
+	{
+		// Use the image tags allotment property editor to edit the image build's tags
+		ImageTagsAllotmentPropertyEditor propertyEditor{imageBuild->imageTagsAllotment(), this};
+		propertyEditor.invokeDialog();
+	}
+}
+
 void ImageEditorWidget::primaryColorChanged(QColor color)
 {
 	ui->primaryColorWidget->setColor(color);
diff --git a/Grinder/ui/image/ImageEditorWidget.h b/Grinder/ui/image/ImageEditorWidget.h
index dd1cf27..21f9273 100644
--- a/Grinder/ui/image/ImageEditorWidget.h
+++ b/Grinder/ui/image/ImageEditorWidget.h
@@ -67,6 +67,8 @@ namespace grndr
 		void duplicateImageBuild() const;
 		void duplicateImageBuildTo() const;		
 
+		void editImageBuildTags();
+
 	private:
 		QAction* _newLayerAction{nullptr};
 
@@ -75,6 +77,8 @@ namespace grndr
 		QAction* _duplicateImageBuild{nullptr};
 		QAction* _duplicateImageBuildTo{nullptr};
 
+		QAction* _editImageBuildTags{nullptr};
+
 		QMenu _duplicateImageBuildMenu;
 	};
 }
diff --git a/Grinder/ui/image/tags/ImageTagsListWidget.cpp b/Grinder/ui/image/tags/ImageTagsListWidget.cpp
index 948b0ab..ae1cd83 100644
--- a/Grinder/ui/image/tags/ImageTagsListWidget.cpp
+++ b/Grinder/ui/image/tags/ImageTagsListWidget.cpp
@@ -28,24 +28,15 @@ void ImageTagsListWidget::assignUiComponents(ImageEditor* imageEditor)
 
 void ImageTagsListWidget::imageBuildSwitched(ImageBuild* imageBuild)
 {
-	// Remove any previously connected signals from the block's image tags in-port
+	// Remove any previously connected signals from the image build's block
 	if (_imageBuild)
-	{
-		if (auto imageTagsInPort = _imageBuild->block()->ports().selectByType(PortType::ImageTagsIn))
-			disconnect(imageTagsInPort.get(), nullptr, this, nullptr);
-	}
+		disconnect(_imageBuild, nullptr, this, nullptr);
 
 	_imageBuild = imageBuild;
 
 	// Listen for connection signals from the block's image tags in-port
 	if (_imageBuild)
-	{
-		if (auto imageTagsInPort = _imageBuild->block()->ports().selectByType(PortType::ImageTagsIn))
-		{
-			connect(imageTagsInPort.get(), SIGNAL(portConnected(const Connection*)), this, SLOT(populateList()), Qt::QueuedConnection);
-			connect(imageTagsInPort.get(), SIGNAL(portDisconnected(const Connection*)), this, SLOT(populateList()), Qt::QueuedConnection);
-		}
-	}
+		connect(_imageBuild, SIGNAL(inputImageTagsChanged(const ImageTags*)), this, SLOT(populateList()));
 
 	// A new image build is shown in the editor, so populate its associated image tags
 	populateList();
@@ -53,12 +44,5 @@ void ImageTagsListWidget::imageBuildSwitched(ImageBuild* imageBuild)
 
 void ImageTagsListWidget::populateList()
 {
-	if (_imageBuild)
-	{
-		// See if the block has an image tags in-port and if an image tags property can be retrieved from it
-		auto imageTagsProperty = _imageBuild->block()->portProperty<ImageTagsProperty>(PortType::ImageTagsIn, PropertyID::ImageTags);
-		assignImageTags(imageTagsProperty ? &imageTagsProperty->object() : nullptr);
-	}
-	else
-		assignImageTags(nullptr);
+	assignImageTags(_imageBuild ? _imageBuild->inputImageTags() : nullptr);
 }
diff --git a/Grinder/ui/image/tools/DraftItemTool.cpp b/Grinder/ui/image/tools/DraftItemTool.cpp
index 43074b3..397908a 100644
--- a/Grinder/ui/image/tools/DraftItemTool.cpp
+++ b/Grinder/ui/image/tools/DraftItemTool.cpp
@@ -16,6 +16,9 @@ DraftItemTool::DraftItemTool(ImageEditor* imageEditor, DraftItemType itemType, Q
 {
 	if (itemType == DraftItemType::Undefined)
 		throw std::invalid_argument{_EXCPT("itemType may not be DraftItemType::Undefined")};
+
+	// We need to update the image tags allotment property whenever the image build has been switched
+	connect(&_imageEditor->controller(), &ImageEditorController::imageBuildSwitched, this, &DraftItemTool::imageBuildSwitched);
 }
 
 std::shared_ptr<DraftItem> DraftItemTool::createDraftItem(QPointF pos, bool selectItem, bool setDefaultProperties)
@@ -75,6 +78,9 @@ void DraftItemTool::createProperties()
 	_direction = createProperty<AngleProperty>(PropertyID::Direction, "Direction", 0.0);
 	direction()->createConstraint<RangeConstraint>(0, 360);
 	direction()->setDescription("The main direction/orientation of the box contents.");
+
+	_imageTagsAllotment = createProperty<ImageTagsAllotmentProperty>(PropertyID::ImageTagsAllotment, "Image tags");
+	imageTagsAllotment()->setDescription("The assigned image tags.");
 }
 
 ImageEditorTool::InputEventResult DraftItemTool::mousePressed(const QGraphicsSceneMouseEvent* event)
@@ -153,3 +159,9 @@ void DraftItemTool::cancelDraftItemDrag()
 		_dragInfo.draftItem = nullptr;
 	}
 }
+
+void DraftItemTool::imageBuildSwitched(ImageBuild* imageBuild)
+{
+	// Assign the current image build to the image tags allotment property
+	imageTagsAllotment()->object().setImageBuild(imageBuild);
+}
diff --git a/Grinder/ui/image/tools/DraftItemTool.h b/Grinder/ui/image/tools/DraftItemTool.h
index ee67d04..6c9b5dc 100644
--- a/Grinder/ui/image/tools/DraftItemTool.h
+++ b/Grinder/ui/image/tools/DraftItemTool.h
@@ -8,6 +8,7 @@
 
 #include "ui/image/ImageEditorTool.h"
 #include "image/DraftItemType.h"
+#include "image/tags/ImageTagsAllotmentProperty.h"
 
 namespace grndr
 {
@@ -32,6 +33,8 @@ namespace grndr
 		auto hasDirection() const { return dynamic_cast<BoolProperty*>(_hasDirection.get()); }
 		auto direction() { return dynamic_cast<AngleProperty*>(_direction.get()); }
 		auto direction() const { return dynamic_cast<AngleProperty*>(_direction.get()); }
+		auto imageTagsAllotment() { return dynamic_cast<ImageTagsAllotmentProperty*>(_imageTagsAllotment.get()); }
+		auto imageTagsAllotment() const { return dynamic_cast<ImageTagsAllotmentProperty*>(_imageTagsAllotment.get()); }
 
 	protected:
 		std::shared_ptr<DraftItem> createDraftItem(QPointF pos, bool selectItem, bool setDefaultProperties);
@@ -57,6 +60,10 @@ namespace grndr
 		std::shared_ptr<PropertyBase> _position;
 		std::shared_ptr<PropertyBase> _hasDirection;
 		std::shared_ptr<PropertyBase> _direction;
+		std::shared_ptr<PropertyBase> _imageTagsAllotment;
+
+	private slots:
+		void imageBuildSwitched(ImageBuild* imageBuild);
 
 	private:
 		struct
diff --git a/Grinder/ui/properties/PropertyTreeItemDelegate.h b/Grinder/ui/properties/PropertyTreeItemDelegate.h
index b6dfc52..3283adc 100644
--- a/Grinder/ui/properties/PropertyTreeItemDelegate.h
+++ b/Grinder/ui/properties/PropertyTreeItemDelegate.h
@@ -28,7 +28,7 @@ namespace grndr
 		virtual void initStyleOption(QStyleOptionViewItem* option, const QModelIndex& index) const override;
 
 	private:
-		PropertyTreeWidget* _widget{nullptr};			
+		PropertyTreeWidget* _widget{nullptr};
 	};
 }
 
diff --git a/Grinder/ui/properties/ValuePropertyTreeItem.cpp b/Grinder/ui/properties/ValuePropertyTreeItem.cpp
index 82359b4..10e6a30 100644
--- a/Grinder/ui/properties/ValuePropertyTreeItem.cpp
+++ b/Grinder/ui/properties/ValuePropertyTreeItem.cpp
@@ -14,7 +14,6 @@ ValuePropertyTreeItem::ValuePropertyTreeItem(const std::shared_ptr<PropertyBase>
 		throw std::invalid_argument{_EXCPT("property may not be null")};
 
 	setFlags(Qt::ItemIsEnabled|Qt::ItemIsSelectable|Qt::ItemIsEditable);
-
 	setTextColor(1, QColor{20, 105, 140});
 
 	updateItem();
diff --git a/Grinder/ui/properties/editors/DialogPropertyEditor.h b/Grinder/ui/properties/editors/DialogPropertyEditor.h
index 66f1f3f..7247ff7 100644
--- a/Grinder/ui/properties/editors/DialogPropertyEditor.h
+++ b/Grinder/ui/properties/editors/DialogPropertyEditor.h
@@ -16,6 +16,9 @@ namespace grndr
 	public:
 		DialogPropertyEditor(PropertyType* property, QWidget *parent = nullptr);
 
+	public:
+		virtual void invokeDialog() = 0;
+
 	protected:
 		virtual void showEvent(QShowEvent* event) override;
 		virtual void mouseDoubleClickEvent(QMouseEvent* event) override;
@@ -24,7 +27,7 @@ namespace grndr
 		virtual void applyPropertyValue() override;
 
 	protected:
-		virtual void invokeDialog() = 0;
+		void setValueLabelText(QString text);
 
 	private:
 		void editPropertyClicked(bool checked);
diff --git a/Grinder/ui/properties/editors/DialogPropertyEditor.impl.h b/Grinder/ui/properties/editors/DialogPropertyEditor.impl.h
index 8cc7c05..1721dec 100644
--- a/Grinder/ui/properties/editors/DialogPropertyEditor.impl.h
+++ b/Grinder/ui/properties/editors/DialogPropertyEditor.impl.h
@@ -12,10 +12,11 @@ DialogPropertyEditor<PropertyType>::DialogPropertyEditor(PropertyType* property,
 {
 	this->setFocusPolicy(Qt::NoFocus);
 
-	_valueLabel->setStyleSheet("margin-left: 1px;");
 	_valueLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
+	_valueLabel->setStyleSheet("QTreeWidget QLabel { margin-left: 1px; }");
 
 	_editButton->setText("...");
+	_editButton->setToolTip("Edit items");
 
 	// Create a horizontal layout and add all controls to it
 	auto layout = new QHBoxLayout{};
@@ -51,10 +52,18 @@ void DialogPropertyEditor<PropertyType>::mouseDoubleClickEvent(QMouseEvent* even
 		invokeDialog();
 }
 
+template<typename PropertyType>
+void DialogPropertyEditor<PropertyType>::setValueLabelText(QString text)
+{
+	QFontMetrics fontMetrics(_valueLabel->font());
+	_valueLabel->setText(fontMetrics.elidedText(text, Qt::ElideRight, _valueLabel->width() - 2));
+	_valueLabel->setToolTip(this->getPropertyValue());
+}
+
 template<typename PropertyType>
 void DialogPropertyEditor<PropertyType>::applyPropertyValue()
 {
-	_valueLabel->setText(this->getPropertyValue());
+	setValueLabelText(this->getPropertyValue());
 }
 
 template<typename PropertyType>
diff --git a/Grinder/ui/properties/editors/ImageTagsAllotmentPropertyEditor.cpp b/Grinder/ui/properties/editors/ImageTagsAllotmentPropertyEditor.cpp
new file mode 100644
index 0000000..56f8a07
--- /dev/null
+++ b/Grinder/ui/properties/editors/ImageTagsAllotmentPropertyEditor.cpp
@@ -0,0 +1,41 @@
+/******************************************************************************
+ * File: ImageTagsAllotmentPropertyEditor.cpp
+ * Date: 18.4.2018
+ *****************************************************************************/
+
+#include "Grinder.h"
+#include "ImageTagsAllotmentPropertyEditor.h"
+#include "image/tags/ImageTags.h"
+#include "ui/dlg/CheckListDialog.h"
+#include "ui/image/tags/ImageTagsListItem.h"
+
+ImageTagsAllotmentPropertyEditor::ImageTagsAllotmentPropertyEditor(ImageTagsAllotmentProperty* property, QWidget* parent) : DialogPropertyEditor(property, parent)
+{
+
+}
+
+void ImageTagsAllotmentPropertyEditor::invokeDialog()
+{
+	CheckListDialog dlg{"Assign image tags", true};
+
+	if (auto imageTags = _property->object().inputImageTags())
+	{
+		bool accepted = false;
+		std::vector<ImageTag*> assignedTags{_property->object().assignedTags().cbegin(), _property->object().assignedTags().cend()};
+		auto selectedTags = dlg.exec<ImageTag, ImageTagsListItem>(imageTags->tags(), assignedTags, &accepted);
+
+		if (accepted)
+		{
+			// Assign all selected image tags
+			_property->object().unassignAllTags();
+
+			for (auto imageTag : selectedTags)
+				_property->object().assignTag(imageTag);
+
+			// The associated image tags have been modified, so notify the property
+			_property->objectModified();
+		}
+	}
+	else
+		QMessageBox::information(nullptr, "Assigning image tags", "There are currently no associated input image tags.");
+}
diff --git a/Grinder/ui/properties/editors/ImageTagsAllotmentPropertyEditor.h b/Grinder/ui/properties/editors/ImageTagsAllotmentPropertyEditor.h
new file mode 100644
index 0000000..5a854b1
--- /dev/null
+++ b/Grinder/ui/properties/editors/ImageTagsAllotmentPropertyEditor.h
@@ -0,0 +1,26 @@
+/******************************************************************************
+ * File: ImageTagsAllotmentPropertyEditor.h
+ * Date: 18.4.2018
+ *****************************************************************************/
+
+#ifndef IMAGETAGSALLOTMENTPROPERTYEDITOR_H
+#define IMAGETAGSALLOTMENTPROPERTYEDITOR_H
+
+#include "DialogPropertyEditor.h"
+#include "image/tags/ImageTagsAllotmentProperty.h"
+
+namespace grndr
+{
+	class ImageTagsAllotmentPropertyEditor : public DialogPropertyEditor<ImageTagsAllotmentProperty>
+	{
+		Q_OBJECT
+
+	public:
+		ImageTagsAllotmentPropertyEditor(ImageTagsAllotmentProperty* property, QWidget *parent = nullptr);
+
+	public:
+		virtual void invokeDialog() override;
+	};
+}
+
+#endif
diff --git a/Grinder/ui/properties/editors/ImageTagsPropertyEditor.h b/Grinder/ui/properties/editors/ImageTagsPropertyEditor.h
index 5a99143..028835c 100644
--- a/Grinder/ui/properties/editors/ImageTagsPropertyEditor.h
+++ b/Grinder/ui/properties/editors/ImageTagsPropertyEditor.h
@@ -18,7 +18,7 @@ namespace grndr
 	public:
 		ImageTagsPropertyEditor(ImageTagsProperty* property, QWidget *parent = nullptr);
 
-	protected:
+	public:
 		virtual void invokeDialog() override;
 	};
 }
diff --git a/Grinder/ui/widgets/CheckListWidget.h b/Grinder/ui/widgets/CheckListWidget.h
index b8a899f..34af55e 100644
--- a/Grinder/ui/widgets/CheckListWidget.h
+++ b/Grinder/ui/widgets/CheckListWidget.h
@@ -23,6 +23,9 @@ namespace grndr
 		template<typename ContainerType>
 		void populateList(const ContainerType& container);
 
+		std::vector<ObjectType*> getCheckedObjects() const;
+		void setCheckedObjects(const std::vector<ObjectType*>& checkedObjects);
+
 	protected:
 		virtual void contextMenuEvent(QContextMenuEvent* event) override;
 
diff --git a/Grinder/ui/widgets/CheckListWidget.impl.h b/Grinder/ui/widgets/CheckListWidget.impl.h
index 2b6e2f1..62e84c5 100644
--- a/Grinder/ui/widgets/CheckListWidget.impl.h
+++ b/Grinder/ui/widgets/CheckListWidget.impl.h
@@ -36,6 +36,38 @@ void CheckListWidget<ObjectType, ItemType>::populateList(const ContainerType& co
 	}
 }
 
+template<typename ObjectType, typename ItemType>
+std::vector<ObjectType*> CheckListWidget<ObjectType, ItemType>::getCheckedObjects() const
+{
+	std::vector<ObjectType*> checkedItems;
+
+	for (int i = 0; i < this->count(); ++i)
+	{
+		if (auto item = dynamic_cast<ItemType*>(this->item(i)))
+		{
+			if (item->checkState() == Qt::Checked)
+				checkedItems.push_back(item->object());
+		}
+	}
+
+	return checkedItems;
+}
+
+template<typename ObjectType, typename ItemType>
+void CheckListWidget<ObjectType, ItemType>::setCheckedObjects(const std::vector<ObjectType*>& checkedObjects)
+{
+	for (int i = 0; i < this->count(); ++i)
+	{
+		if (auto item = dynamic_cast<ItemType*>(this->item(i)))
+		{
+			if (std::find(checkedObjects.cbegin(), checkedObjects.cend(), item->object()) != checkedObjects.cend())
+				item->setCheckState(Qt::Checked);
+			else
+				item->setCheckState(Qt::Unchecked);
+		}
+	}
+}
+
 template<typename ObjectType, typename ItemType>
 void CheckListWidget<ObjectType, ItemType>::contextMenuEvent(QContextMenuEvent* event)
 {
diff --git a/Grinder/ui/widgets/ObjectListWidget.h b/Grinder/ui/widgets/ObjectListWidget.h
index 0824193..516d39e 100644
--- a/Grinder/ui/widgets/ObjectListWidget.h
+++ b/Grinder/ui/widgets/ObjectListWidget.h
@@ -17,7 +17,7 @@ namespace grndr
 	template<typename ObjectType, typename ItemType>
 	class ObjectListWidget : public BaseListWidget
 	{
-		static_assert(std::is_base_of<ObjectListItem<ObjectType>, ItemType>::value, "ItemType must be derived from ObjectListItem<ObjectType>");
+		static_assert(std::is_base_of<ObjectListItem<std::remove_const_t<ObjectType>>, ItemType>::value, "ItemType must be derived from ObjectListItem<ObjectType>");
 
 	public:
 		using object_type = ObjectType;
-- 
GitLab