diff --git a/Grinder/Grinder.pro b/Grinder/Grinder.pro
index 23518044d44d12ae8a24ac1be27822eb038b4c40..0ccdc20ec64ea0ffc29671f67b1babb30259f575 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 d09cac3b6a0fb0fad8012d290db57b9c511e9b4b..443017f2b5da8b0537d057587eda06787c7ce51e 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 cb0ef7feaefbcab47975700c3dc74db67d51571a..8d004e17191d7efbbe68d76be1829cec447de2fb 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 40037bde40de32fa5e41b043d62db703a22d7a68..b22551f806142fe9a940462827b66f016a1dc613 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 b2b2cf59b1899b31be7a90a2b3f204b1663e54b9..b26dc57e4f480c7a2bbe057546fa8eb513da3439 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 068a546425d0b44190926911104e42c4444389ec..6824698d427acdba8d88b72c555a891527562f50 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 6e3d54f8b8c0911ba83f692cb21a8f043e3d05fe..2ef5a4be2219df526c7d43ff65bc129131890037 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 7c13bb26ee82d38ca18c09d2d267749c9ba548e0..8dd58f9ca909867cf49b5278b61d2130edc187ce 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 a08f711b6b25babb4b7a35f5fa108db5db03b9f5..fc95e5e8b1edd7a1158401b3a3d28b1255bd9ba7 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 13dedae06077254dea6bd7d677270a0abe398f51..479bcc275764c8ea09777f496a3de3e41978c066 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 aaded77c016674e3e94beaef0c45657cfb5711f0..db9c61b2ffa37019cb72f0d7dceb6da07fe702cb 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 a16ab4ff76586c90490f44cf2249d445cf1e02c8..46dfa05a2a107bf77aaa0d4a37d7c3565dfa31af 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 7f6ebf199399fb85e76188efefa6fe213d19fb80..2c74c1bff348fc582f74825c26eecf4237d1056c 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 650a00a6f0336130563cfcb71dddd81b34c1bf5e..45b66f9690f5f4b7e8f1b149348dd15002cf610d 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 477e6c9e48b93d7ff27525ab5741aa5094ed1f21..527c355c477cea55715ded1c8c5ad7325ec116f5 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 d811d3a2ed4ec41df3ad68f2dc44240cff7b041f..d3cafdeac846b809cf2e353a2472b0c864e4d3a0 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 9c9e6e14d7cccd81c707632f1bb6a1344429db5e..3202331d3a16440511a729d2b3a72815bcd5ca1e 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 0000000000000000000000000000000000000000..6845e225452c1dec257da0071721524a5d9e88d6
--- /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 0000000000000000000000000000000000000000..1d878213fbd3cae914388ed3e0b92f86b63d3a8a
--- /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 0000000000000000000000000000000000000000..677203bffc25fc3d583cd8b15161dcdeca75d427
--- /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 0000000000000000000000000000000000000000..650089f3a218031e0740c179f55fc8c8a7e7c33e
--- /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 a858e8afa1b65d07d8dba2633681fae3ab603cbd..7113e0dfc192a1602b122855b66a1e43a09630a5 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 c1d6fe8e432c8a637e7dcc9fb00c23397eb82fcb..8abd1776c69f5c62a18ba026634069a9013a9e59 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 6bcc55e6acc2b9c3dbbfa3ff8b1d28a41650e38e..ee20b2fd898449b5b95f2531610263d6e404ccac 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 d11ebaa0f1263cfb63d66c10f0b0059219db5f08..9dd96cc8d1b9df8d5dd5811e5ff8ca648c45cf35 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 0fdc9c12977741773e0cdd75607f9d412c438adf..e30301cf917d1c3aee3e9f418d4d254becb9da32 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 31539462448456e8298b7da0a0f295ee37c5b1e4..9730a49e6f01f28fc3206e9213c453ecb299c681 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 34b9d9a614494762fb6c97897fdd243e74b22de0..1937fb66cd5ae250ffa109241cfe67549032afc1 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 a17f614af1cc7a50629d802f0f472ae55b33be2a..bd12c80e834da79667d40f685d6da6ebcc4df8de 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 2d46903816fb13467a98298e134b3138a46cf027..5fceeecb25aea8c5c2e62094960f3c8fd025ce7e 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 b87a67bfedc9c07bd177ceee4d480e41551d3d50..447990f2c0cba5fb5a697fd9be6cc858e09e8953 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 c6503119cdbbe89b02b126956a18944c1dab3fc4..deaee4ddedb943377b1b475557f4df6211212df3 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 fe2ea35dd9f4e5599829497aa467f74f07839d6e..d79f60d0528fd12a362173aa347803377ba9fdbb 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 2cf73aec48364ef76923fda295744c9b23e37750..f919e5b6a4cf3c5725a1207972722f66145db403 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 dd1cf27322fe8db5133f7ba3b7d4e1b5a622a213..21f9273606d6b44c563772ebbde7fbcf620505bf 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 948b0ab133d1e5c73f06e3057a01c4b785d710e9..ae1cd8346f56e70e5c021475964f5748d25edfe8 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 43074b3b5b83815379dc950d77bd3a9f3076008d..397908aa9da7d1dd07737fc493268f928c7f7e39 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 ee67d047db73543b56f8fe33b37315d0841c70c4..6c9b5dcbd989edd83c1b5e06a6e54876ed7f765f 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 b6dfc52c3a532c685fb240a983468e9cf0e4c959..3283adc03ba9ca4b9311af4da3f1d2fe033f0c9f 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 82359b4b04ddead391916616d559d64367adde09..10e6a301fed4a1eb20085df0ef4511f65ba9456f 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 66f1f3fe770ca1dc3c3e37d8cbdec7ee1ce05cee..7247ff78499cf20b093da3f044ada4047577d2a4 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 8cc7c051b9cbe8fe1ac971a726361c4317b8187a..1721dec49a79aae93c434727259ce25ca515dd72 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 0000000000000000000000000000000000000000..56f8a0749062cb4a333155f66ad83377e25d1c49
--- /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 0000000000000000000000000000000000000000..5a854b11f5208c79d073e0fd1861c4f8422fdd00
--- /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 5a99143d9edaed9bebe537f3bfcfb9de42c9ead7..028835c4a614095c7b40ec544aaab08576432368 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 b8a899fc2516b50c6d5adf3bd4513729899cd733..34af55e3286aa176c8c3221fcb1e487240ffc8fa 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 2b6e2f13846028deea9f04b864345ca8a0496ce4..62e84c5072fef0df7caf91d6c75a17fc56de3227 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 082419315da500a8e94a63fab40b4be9a763aca5..516d39e22f5781b7fe98da93a42ba385bea42ccd 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;