From 4eefe97e81f9ea3db9bb6b555d6000a2bfbfb3f3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20M=C3=BCller?= <d_muel20@uni-muenster.de>
Date: Fri, 29 Jun 2018 17:31:14 +0200
Subject: [PATCH] * Properties are now grouped * Properties are more dynamic
 now

---
 Grinder/Grinder.pro                           |  6 +-
 Grinder/Version.h                             |  6 +-
 Grinder/common/properties/PropertyBase.h      |  8 +-
 Grinder/common/properties/PropertyObject.cpp  | 56 ++++++++++++-
 Grinder/common/properties/PropertyObject.h    | 28 ++++++-
 .../common/properties/PropertyObject.impl.h   | 10 +--
 Grinder/common/properties/PropertyVector.cpp  |  5 ++
 Grinder/common/properties/PropertyVector.h    |  1 +
 .../processors/AdaptiveThresholdProcessor.cpp |  2 +-
 Grinder/image/DraftItem.cpp                   | 15 ++++
 Grinder/image/DraftItem.h                     |  1 +
 Grinder/image/ImageBuild.cpp                  |  1 +
 Grinder/image/draftitems/BoxDraftItem.cpp     |  6 +-
 Grinder/image/draftitems/LineDraftItem.cpp    |  4 +
 Grinder/image/draftitems/PixelsDraftItem.cpp  | 13 +++
 Grinder/image/draftitems/PixelsDraftItem.h    |  1 +
 Grinder/pipeline/PipelineItem.cpp             |  1 +
 .../blocks/AdaptiveThresholdBlock.cpp         |  5 +-
 .../pipeline/blocks/AdaptiveThresholdBlock.h  |  3 -
 .../pipeline/blocks/AlphaBlendingBlock.cpp    |  2 +
 .../pipeline/blocks/BinaryThresholdBlock.cpp  | 13 +++
 .../pipeline/blocks/BinaryThresholdBlock.h    |  1 +
 Grinder/pipeline/blocks/BlurBlock.cpp         | 27 +++++--
 Grinder/pipeline/blocks/BlurBlock.h           |  1 +
 Grinder/pipeline/blocks/ContoursBlock.cpp     | 16 +++-
 Grinder/pipeline/blocks/ContoursBlock.h       |  3 +-
 .../pipeline/blocks/ConvertToColorBlock.cpp   |  2 +
 Grinder/pipeline/blocks/DilateBlock.cpp       |  2 +
 .../blocks/DistanceTransformBlock.cpp         | 12 +++
 .../pipeline/blocks/DistanceTransformBlock.h  |  1 +
 Grinder/pipeline/blocks/ErodeBlock.cpp        |  2 +
 Grinder/pipeline/blocks/ImageTagsBlock.cpp    |  2 +
 Grinder/pipeline/blocks/InputBlock.cpp        |  2 +
 Grinder/pipeline/blocks/NormalizeBlock.cpp    |  2 +
 Grinder/pipeline/blocks/OutputBlock.cpp       |  2 +
 Grinder/pipeline/blocks/ReplaceColorBlock.cpp |  2 +
 Grinder/pipeline/blocks/SharpenBlock.cpp      |  2 +
 .../DistanceTransformMaskProperty.cpp         |  1 -
 .../ui/image/ImageEditorPropertyWidget.cpp    | 68 +++++++++++++---
 Grinder/ui/image/ImageEditorPropertyWidget.h  |  6 +-
 Grinder/ui/image/ImageEditorTool.cpp          |  1 +
 Grinder/ui/image/tools/BoxDraftItemTool.cpp   |  4 +
 Grinder/ui/image/tools/DraftItemTool.cpp      | 14 ++++
 Grinder/ui/image/tools/DraftItemTool.h        |  1 +
 Grinder/ui/image/tools/EraserTool.cpp         |  2 +
 Grinder/ui/image/tools/LineDraftItemTool.cpp  |  2 +
 Grinder/ui/image/tools/PaintbrushTool.cpp     |  2 +
 Grinder/ui/mainwnd/GrinderWindow.ui           |  3 -
 Grinder/ui/properties/BlockPropertyTreeItem.h |  2 +-
 .../ui/properties/GroupPropertyTreeItem.cpp   | 29 +++++++
 Grinder/ui/properties/GroupPropertyTreeItem.h | 28 +++++++
 Grinder/ui/properties/PropertyTreeWidget.cpp  | 81 ++++++++++++++++---
 Grinder/ui/properties/PropertyTreeWidget.h    |  4 +-
 .../ui/properties/ValuePropertyTreeItem.cpp   | 16 +++-
 .../editors/AnglePropertyEditor.cpp           |  1 +
 55 files changed, 463 insertions(+), 68 deletions(-)
 create mode 100644 Grinder/ui/properties/GroupPropertyTreeItem.cpp
 create mode 100644 Grinder/ui/properties/GroupPropertyTreeItem.h

diff --git a/Grinder/Grinder.pro b/Grinder/Grinder.pro
index 428f934..d50b0f1 100644
--- a/Grinder/Grinder.pro
+++ b/Grinder/Grinder.pro
@@ -276,7 +276,8 @@ SOURCES += \
     pipeline/blocks/ContoursBlock.cpp \
     engine/processors/ContoursProcessor.cpp \
     pipeline/blocks/WatershedBlock.cpp \
-    engine/processors/WatershedProcessor.cpp
+    engine/processors/WatershedProcessor.cpp \
+    ui/properties/GroupPropertyTreeItem.cpp
 
 HEADERS += \
 	ui/mainwnd/GrinderWindow.h \
@@ -601,7 +602,8 @@ HEADERS += \
     pipeline/blocks/ContoursBlock.h \
     engine/processors/ContoursProcessor.h \
     pipeline/blocks/WatershedBlock.h \
-    engine/processors/WatershedProcessor.h
+    engine/processors/WatershedProcessor.h \
+    ui/properties/GroupPropertyTreeItem.h
 
 FORMS += \
 	ui/mainwnd/GrinderWindow.ui \
diff --git a/Grinder/Version.h b/Grinder/Version.h
index 1fa6dd4..c69bad4 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			"26.06.2018"
+#define GRNDR_INFO_DATE			"29.06.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		5
+#define GRNDR_VERSION_MINOR		6
 #define GRNDR_VERSION_REVISION	0
-#define GRNDR_VERSION_BUILD		200
+#define GRNDR_VERSION_BUILD		203
 
 namespace grndr
 {
diff --git a/Grinder/common/properties/PropertyBase.h b/Grinder/common/properties/PropertyBase.h
index 2ef5a4b..ee796f5 100644
--- a/Grinder/common/properties/PropertyBase.h
+++ b/Grinder/common/properties/PropertyBase.h
@@ -26,9 +26,10 @@ namespace grndr
 			None = 0x0000,
 			ReadOnly = 0x0001,
 			Hidden = 0x0002,
+			Disabled = 0x0004,
 		};
 
-		Q_DECLARE_FLAGS(Flags, Flag)		
+		Q_DECLARE_FLAGS(Flags, Flag)
 
 	public:
 		PropertyBase(PropertyID id, QString name, Flags flags = Flag::None);
@@ -51,9 +52,11 @@ namespace grndr
 		void setName(QString name) { _name = name; }
 		QString getDescription() const { return _description; }
 		void setDescription(QString desc) { _description = desc; }
+		QString getGroup() const { return _group; }
+		void setGroup(QString group) { _group = group; }
 
 		bool hasFlag(Flag flag) const { return _flags.testFlag(flag); }
-		void setFlag(Flag flag) { _flags |= flag; }
+		void setFlag(Flag flag, bool set = true) { _flags.setFlag(flag, set); }
 		Flags getFlags() const { return _flags; }
 		void setFlags(Flags flags) { _flags = flags; }
 
@@ -76,6 +79,7 @@ namespace grndr
 		PropertyID _id{PropertyID::None};
 		QString _name{""};
 		QString _description{""};
+		QString _group;
 
 		Flags _flags{Flag::None};
 	};	
diff --git a/Grinder/common/properties/PropertyObject.cpp b/Grinder/common/properties/PropertyObject.cpp
index 107e5af..797cfbe 100644
--- a/Grinder/common/properties/PropertyObject.cpp
+++ b/Grinder/common/properties/PropertyObject.cpp
@@ -56,13 +56,51 @@ void PropertyObject::removeProperty(const PropertyBase* prop)
 		auto it = _properties.find(prop);
 
 		if (it != _properties.cend())
+		{
+			disconnect(prop, nullptr, this, nullptr);
 			_properties.erase(it);
+		}
 		else
 			throw PropertyItemException{this, _EXCPT("Tried to remove a property not belonging to this item")};
 	}
 }
 
-std::shared_ptr<PropertyBase> PropertyObject::_createProperty(PropertyID id, std::function<std::shared_ptr<PropertyBase>()> creator)
+void PropertyObject::setPropertyGroup(QString group, QString insertBefore)
+{
+	// Add the group if it doesn't exist yet
+	if (!_groups.contains(group, Qt::CaseInsensitive))
+	{
+		auto it = std::find(_groups.begin(), _groups.end(), insertBefore);
+		_groups.insert(it, group);
+	}
+
+	_currentGroup = group;
+}
+
+bool PropertyObject::enableDependantProperty(PropertyBase* updatedProp, std::shared_ptr<PropertyBase> superordinateProp, std::shared_ptr<PropertyBase> dependantProp, std::function<bool()> condition) const
+{
+	if (!updatedProp || updatedProp == superordinateProp.get())
+	{
+		dependantProp->setFlag(PropertyBase::Flag::Disabled, !condition());
+		return true;
+	}
+	else
+		return false;
+}
+
+bool PropertyObject::updateDependantProperty(PropertyBase* updatedProp, std::shared_ptr<PropertyBase> superordinateProp, std::shared_ptr<PropertyBase> dependantProp, QString name, QString desc, std::function<bool()> condition) const
+{
+	if ((!updatedProp || updatedProp == superordinateProp.get()) && condition())
+	{
+		dependantProp->setName(name);
+		dependantProp->setDescription(desc);
+		return true;
+	}
+	else
+		return false;
+}
+
+std::shared_ptr<PropertyBase> PropertyObject::_createProperty(PropertyID id, std::function<std::shared_ptr<PropertyBase>()> creator, PropertyID insertBefore)
 {
 	if (id == PropertyID::None)
 		throw std::invalid_argument{_EXCPT("id may not be empty")};
@@ -74,11 +112,25 @@ std::shared_ptr<PropertyBase> PropertyObject::_createProperty(PropertyID id, std
 
 	try {	// Propagate initialization errors to the caller
 		prop->initProperty();
+
+		if (!_currentGroup.isEmpty())
+			prop->setGroup(_currentGroup);
+
+		// Get notified about all property value changes
+		connect(prop.get(), &PropertyBase::valueChanged, this, &PropertyObject::propertyValueChanged);
 	}
 	catch (...) {
 		throw;
 	}
 
-	_properties.push_back(prop);
+	if (insertBefore != PropertyID::None)
+	{
+		auto propBefore = _properties.selectByID(insertBefore);
+		auto it = _properties.find(propBefore);
+		_properties.insert(it, prop);
+	}
+	else
+		_properties.push_back(prop);
+
 	return prop;
 }
diff --git a/Grinder/common/properties/PropertyObject.h b/Grinder/common/properties/PropertyObject.h
index e9d4f0c..bfd620f 100644
--- a/Grinder/common/properties/PropertyObject.h
+++ b/Grinder/common/properties/PropertyObject.h
@@ -20,26 +20,46 @@ namespace grndr
 	public:
 		PropertyVector& properties() { return _properties; }
 		const PropertyVector& properties() const { return _properties; }
+		auto properties(QString group) const { return _properties.selectByGroup(group); }
+
+		QStringList getPropertyGroups() const { return _groups; }
 
 	public:
 		virtual void serialize(SerializationContext& ctx) const;
 		virtual void deserialize(DeserializationContext& ctx);
 
 	protected:
-		virtual void createProperties() { }
+		virtual void createProperties() { setPropertyGroup("General"); }
+		virtual bool updateProperties(PropertyBase* updatedProp = nullptr) { Q_UNUSED(updatedProp); return false; }
 
 		template<typename PropType>
-		std::shared_ptr<PropertyBase> createProperty(PropertyID id, QString name, PropertyBase::Flags flags = PropertyBase::Flag::None);
+		std::shared_ptr<PropertyBase> createProperty(PropertyID id, QString name, PropertyBase::Flags flags = PropertyBase::Flag::None, PropertyID insertBefore = PropertyID::None);
 		template<typename PropType>
-		std::shared_ptr<PropertyBase> createProperty(PropertyID id, QString name, typename PropType::value_type defValue, PropertyBase::Flags flags = PropertyBase::Flag::None);
+		std::shared_ptr<PropertyBase> createProperty(PropertyID id, QString name, typename PropType::value_type defValue, PropertyBase::Flags flags = PropertyBase::Flag::None, PropertyID insertBefore = PropertyID::None);
 		void removeProperty(PropertyID id);
 		void removeProperty(const PropertyBase* prop);
 
+		void setPropertyGroup(QString group, QString insertBefore = "");
+
+	protected:
+		bool enableDependantProperty(PropertyBase* updatedProp, std::shared_ptr<PropertyBase> superordinateProp, std::shared_ptr<PropertyBase> dependantProp, std::function<bool()> condition) const;
+		bool updateDependantProperty(PropertyBase* updatedProp, std::shared_ptr<PropertyBase> superordinateProp, std::shared_ptr<PropertyBase> dependantProp, QString name, QString desc, std::function<bool()> condition) const;
+
+	signals:
+		void propertiesUpdated();
+
 	private:
-		std::shared_ptr<PropertyBase> _createProperty(PropertyID id, std::function<std::shared_ptr<PropertyBase>()> creator);
+		std::shared_ptr<PropertyBase> _createProperty(PropertyID id, std::function<std::shared_ptr<PropertyBase>()> creator, PropertyID insertBefore);
+
+	protected slots:
+		void propertyValueChanged() { if (updateProperties(dynamic_cast<PropertyBase*>(sender()))) emit propertiesUpdated(); }
 
 	protected:
 		PropertyVector _properties;
+
+	private:
+		QStringList _groups;
+		QString _currentGroup{""};
 	};
 }
 
diff --git a/Grinder/common/properties/PropertyObject.impl.h b/Grinder/common/properties/PropertyObject.impl.h
index dad8bf1..c931251 100644
--- a/Grinder/common/properties/PropertyObject.impl.h
+++ b/Grinder/common/properties/PropertyObject.impl.h
@@ -8,13 +8,13 @@
 #include "PropertyExceptions.h"
 
 template<typename PropType>
-std::shared_ptr<PropertyBase> PropertyObject::createProperty(PropertyID id, QString name, PropertyBase::Flags flags)
-{
-	return _createProperty(id, [id, name, flags](){ return std::make_shared<PropType>(id, name, flags); });
+std::shared_ptr<PropertyBase> PropertyObject::createProperty(PropertyID id, QString name, PropertyBase::Flags flags, PropertyID insertBefore)
+{	
+	return _createProperty(id, [id, name, flags](){ return std::make_shared<PropType>(id, name, flags); }, insertBefore);
 }
 
 template<typename PropType>
-std::shared_ptr<PropertyBase> PropertyObject::createProperty(PropertyID id, QString name, typename PropType::value_type defValue, PropertyBase::Flags flags)
+std::shared_ptr<PropertyBase> PropertyObject::createProperty(PropertyID id, QString name, typename PropType::value_type defValue, PropertyBase::Flags flags, PropertyID insertBefore)
 {
-	return _createProperty(id, [id, name, defValue, flags](){ return std::make_shared<PropType>(id, name, defValue, flags); });
+	return _createProperty(id, [id, name, defValue, flags](){ return std::make_shared<PropType>(id, name, defValue, flags); }, insertBefore);
 }
diff --git a/Grinder/common/properties/PropertyVector.cpp b/Grinder/common/properties/PropertyVector.cpp
index 6a61ceb..d78d6b6 100644
--- a/Grinder/common/properties/PropertyVector.cpp
+++ b/Grinder/common/properties/PropertyVector.cpp
@@ -23,3 +23,8 @@ PropertyVector::pointer_type PropertyVector::selectByID(PropertyID id) const
 {
 	return selectFirst([id](auto prop) { return prop->getID() == id; });
 }
+
+PropertyVector::pointer_vec_type PropertyVector::selectByGroup(QString group) const
+{
+	return select([group](auto prop) { return prop->getGroup().compare(group, Qt::CaseInsensitive) == 0; });
+}
diff --git a/Grinder/common/properties/PropertyVector.h b/Grinder/common/properties/PropertyVector.h
index efd0e0c..91f5392 100644
--- a/Grinder/common/properties/PropertyVector.h
+++ b/Grinder/common/properties/PropertyVector.h
@@ -26,6 +26,7 @@ namespace grndr
 		PropType* property(PropertyID id) const;
 
 		pointer_type selectByID(PropertyID id) const;
+		pointer_vec_type selectByGroup(QString group) const;
 	};
 }
 
diff --git a/Grinder/engine/processors/AdaptiveThresholdProcessor.cpp b/Grinder/engine/processors/AdaptiveThresholdProcessor.cpp
index 9bd2c53..08c5d33 100644
--- a/Grinder/engine/processors/AdaptiveThresholdProcessor.cpp
+++ b/Grinder/engine/processors/AdaptiveThresholdProcessor.cpp
@@ -22,7 +22,7 @@ void AdaptiveThresholdProcessor::execute(EngineExecutionContext& ctx)
 	if (auto dataBlob = portData(ctx, _block->inPort()))
 	{
 		cv::Mat processedImage;
-		cv::adaptiveThreshold(dataBlob->getMatrix(), processedImage, *_block->targetValue(), *_block->thresholdMethod(), *_block->thresholdType(), *_block->blockSize(), *_block->constant());
+		cv::adaptiveThreshold(dataBlob->getMatrix(), processedImage, *_block->targetValue(), *_block->thresholdMethod(), *_block->thresholdType(), *_block->blockSize(), 0.0);
 
 		ctx.setContextEntry(_block->outPort(), DataBlob{getPortDataDescriptor(_block->outPort()), std::move(processedImage), dataBlob->getColorSpace()});
 	}
diff --git a/Grinder/image/DraftItem.cpp b/Grinder/image/DraftItem.cpp
index f8dab2d..08fc694 100644
--- a/Grinder/image/DraftItem.cpp
+++ b/Grinder/image/DraftItem.cpp
@@ -19,6 +19,7 @@ DraftItem::DraftItem(Layer* layer, DraftItemType type) : LayerItem(layer),
 void DraftItem::initDraftItem()
 {
 	createProperties();
+	updateProperties();
 }
 
 int DraftItem::getZOrder() const
@@ -44,12 +45,16 @@ void DraftItem::createProperties()
 	PropertyObject::createProperties();
 
 	// Create standard properties
+	setPropertyGroup("General");
+
 	_primaryColor = createProperty<ColorProperty>(PropertyID::PrimaryColor, "Primary color", QColor{255, 255, 255}, PropertyBase::Flag::Hidden);
 	primaryColor()->setDescription("The primary color to use.");
 
 	_position = createProperty<PointProperty>(PropertyID::Position, "Position", QPoint{0, 0});
 	position()->setDescription("The item position.");
 
+	setPropertyGroup("Attributes");
+
 	_hasDirection = createProperty<BoolProperty>(PropertyID::HasDirection, "Has direction", false);
 	hasDirection()->setDescription("Specifies whether the box has a direction/orientation.");
 
@@ -61,3 +66,13 @@ void DraftItem::createProperties()
 	imageTagsAllotment()->setDescription("The assigned tags.");
 	imageTagsAllotment()->object().setImageBuild(_layer->imageBuild());
 }
+
+bool DraftItem::updateProperties(PropertyBase* updatedProp)
+{
+	bool updated = PropertyObject::updateProperties(updatedProp);
+
+	// Update properties to reflect property dependencies
+	updated |= enableDependantProperty(updatedProp, _hasDirection, _direction, [this]()->bool { return *hasDirection(); });
+
+	return updated;
+}
diff --git a/Grinder/image/DraftItem.h b/Grinder/image/DraftItem.h
index 012b3b9..89df8ec 100644
--- a/Grinder/image/DraftItem.h
+++ b/Grinder/image/DraftItem.h
@@ -55,6 +55,7 @@ namespace grndr
 
 	protected:
 		virtual void createProperties() override;
+		virtual bool updateProperties(PropertyBase* updatedProp = nullptr) override;
 
 	protected:
 		DraftItemType _type{DraftItemType::Undefined};
diff --git a/Grinder/image/ImageBuild.cpp b/Grinder/image/ImageBuild.cpp
index 8ac4d6c..f3feb68 100644
--- a/Grinder/image/ImageBuild.cpp
+++ b/Grinder/image/ImageBuild.cpp
@@ -32,6 +32,7 @@ ImageBuild::ImageBuild(const Block* block, const std::vector<const ImageReferenc
 void ImageBuild::initImageBuild()
 {
 	createProperties();
+	updateProperties();
 
 	// Create background image layers for all assigned image references
 	for (const auto imageRef : _imageReferences)
diff --git a/Grinder/image/draftitems/BoxDraftItem.cpp b/Grinder/image/draftitems/BoxDraftItem.cpp
index 24e7fbd..3e5e6ca 100644
--- a/Grinder/image/draftitems/BoxDraftItem.cpp
+++ b/Grinder/image/draftitems/BoxDraftItem.cpp
@@ -23,10 +23,14 @@ void BoxDraftItem::createProperties()
 {
 	DraftItem::createProperties();
 
-	// Create specific properties
+	// Create specific properties	
+	setPropertyGroup("General");
+
 	_boxSize = createProperty<SizeProperty>(PropertyID::Size, "Size", QSize{50, 50});
 	boxSize()->setDescription("The size of the box.");
 
+	setPropertyGroup("Bounding box", "Attributes");
+
 	_lineWidth = createProperty<UIntProperty>(PropertyID::Width, "Line width", 5);
 	lineWidth()->createConstraint<RangeConstraint>(1, 100);
 	lineWidth()->setDescription("The width of the box lines.");
diff --git a/Grinder/image/draftitems/LineDraftItem.cpp b/Grinder/image/draftitems/LineDraftItem.cpp
index 919db96..4eacd2d 100644
--- a/Grinder/image/draftitems/LineDraftItem.cpp
+++ b/Grinder/image/draftitems/LineDraftItem.cpp
@@ -39,9 +39,13 @@ void LineDraftItem::createProperties()
 	DraftItem::createProperties();
 
 	// Create specific properties
+	setPropertyGroup("General");
+
 	_endPosition = createProperty<PointProperty>(PropertyID::EndPosition, "End position", QPoint{0, 0});
 	endPosition()->setDescription("The end position of the line.");
 
+	setPropertyGroup("Line", "Attributes");
+
 	_lineWidth = createProperty<UIntProperty>(PropertyID::Width, "Line width", 3);
 	lineWidth()->createConstraint<RangeConstraint>(1, 100);
 	lineWidth()->setDescription("The width of the line.");
diff --git a/Grinder/image/draftitems/PixelsDraftItem.cpp b/Grinder/image/draftitems/PixelsDraftItem.cpp
index 423d3c9..e23db2a 100644
--- a/Grinder/image/draftitems/PixelsDraftItem.cpp
+++ b/Grinder/image/draftitems/PixelsDraftItem.cpp
@@ -46,6 +46,8 @@ void PixelsDraftItem::createProperties()
 	DraftItem::createProperties();
 
 	// Create specific properties
+	setPropertyGroup("Bounding box", "Attributes");
+
 	_showBox = createProperty<BoolProperty>(PropertyID::ShowBox, "Show box", false);
 	showBox()->setDescription("Show the bounding box enclosing the pixels.");
 
@@ -57,3 +59,14 @@ void PixelsDraftItem::createProperties()
 	boxLineWidth()->createConstraint<RangeConstraint>(1, 100);
 	boxLineWidth()->setDescription("The line width of the bounding box.");
 }
+
+bool PixelsDraftItem::updateProperties(PropertyBase* updatedProp)
+{
+	bool updated = DraftItem::updateProperties(updatedProp);
+
+	// Update properties to reflect property dependencies
+	updated |= enableDependantProperty(updatedProp, _showBox, _boxMargin, [this]()->bool { return *showBox(); });
+	updated |= enableDependantProperty(updatedProp, _showBox, _boxLineWidth, [this]()->bool { return *showBox(); });
+
+	return updated;
+}
diff --git a/Grinder/image/draftitems/PixelsDraftItem.h b/Grinder/image/draftitems/PixelsDraftItem.h
index 2086068..5d9cdeb 100644
--- a/Grinder/image/draftitems/PixelsDraftItem.h
+++ b/Grinder/image/draftitems/PixelsDraftItem.h
@@ -41,6 +41,7 @@ namespace grndr
 
 	protected:
 		virtual void createProperties() override;
+		virtual bool updateProperties(PropertyBase* updatedProp = nullptr) override;
 
 	private:
 		LayerPixelsData _pixelsData;
diff --git a/Grinder/pipeline/PipelineItem.cpp b/Grinder/pipeline/PipelineItem.cpp
index 8f39c5e..550e50c 100644
--- a/Grinder/pipeline/PipelineItem.cpp
+++ b/Grinder/pipeline/PipelineItem.cpp
@@ -19,6 +19,7 @@ PipelineItem::PipelineItem(Pipeline* pipeline, QString name) :
 void PipelineItem::initPipelineItem()
 {
 	createProperties();
+	updateProperties();
 }
 
 void PipelineItem::serialize(SerializationContext& ctx) const
diff --git a/Grinder/pipeline/blocks/AdaptiveThresholdBlock.cpp b/Grinder/pipeline/blocks/AdaptiveThresholdBlock.cpp
index 8cb8d10..0f3dd6b 100644
--- a/Grinder/pipeline/blocks/AdaptiveThresholdBlock.cpp
+++ b/Grinder/pipeline/blocks/AdaptiveThresholdBlock.cpp
@@ -25,6 +25,8 @@ void AdaptiveThresholdBlock::createProperties()
 {
 	Block::createProperties();
 
+	setPropertyGroup("General");
+
 	_thresholdMethod = createProperty<AdaptiveThresholdMethodProperty>(PropertyID::Method, "Method", static_cast<int>(AdaptiveThresholdMethod::Gaussian));
 	thresholdMethod()->setDescription("The adaptive method.");
 
@@ -39,9 +41,6 @@ void AdaptiveThresholdBlock::createProperties()
 	blockSize()->createConstraint<RangeConstraint>(3, std::numeric_limits<unsigned int>::max());
 	blockSize()->createConstraint<EvenOddConstraint>(EvenOddConstraint<unsigned int>::Type::Odd);
 	blockSize()->setDescription("The size of the pixel neighborhood used to calculate the threshold value for a pixel. Must be an odd value (3, 5, 7, and so on).");
-
-	_constant = createProperty<RealProperty>(PropertyID::Constant, "Constant", 0.0);
-	constant()->setDescription("Constant subtracted from the (weighted) mean.");
 }
 
 void AdaptiveThresholdBlock::createPorts()
diff --git a/Grinder/pipeline/blocks/AdaptiveThresholdBlock.h b/Grinder/pipeline/blocks/AdaptiveThresholdBlock.h
index 934bf6e..5b39f32 100644
--- a/Grinder/pipeline/blocks/AdaptiveThresholdBlock.h
+++ b/Grinder/pipeline/blocks/AdaptiveThresholdBlock.h
@@ -35,8 +35,6 @@ namespace grndr
 		auto targetValue() const { return dynamic_cast<const UIntProperty*>(_targetValue.get()); }
 		auto blockSize() { return dynamic_cast<UIntProperty*>(_blockSize.get()); }
 		auto blockSize() const { return dynamic_cast<const UIntProperty*>(_blockSize.get()); }
-		auto constant() { return dynamic_cast<RealProperty*>(_constant.get()); }
-		auto constant() const { return dynamic_cast<const RealProperty*>(_constant.get()); }
 
 		Port* inPort() { return _inPort.get(); }
 		const Port* inPort() const { return _inPort.get(); }
@@ -52,7 +50,6 @@ namespace grndr
 		std::shared_ptr<PropertyBase> _thresholdType;
 		std::shared_ptr<PropertyBase> _targetValue;
 		std::shared_ptr<PropertyBase> _blockSize;
-		std::shared_ptr<PropertyBase> _constant;
 
 		std::shared_ptr<Port> _inPort;
 		std::shared_ptr<Port> _outPort;
diff --git a/Grinder/pipeline/blocks/AlphaBlendingBlock.cpp b/Grinder/pipeline/blocks/AlphaBlendingBlock.cpp
index de8de1f..a894cd6 100644
--- a/Grinder/pipeline/blocks/AlphaBlendingBlock.cpp
+++ b/Grinder/pipeline/blocks/AlphaBlendingBlock.cpp
@@ -24,6 +24,8 @@ void AlphaBlendingBlock::createProperties()
 {
 	Block::createProperties();
 
+	setPropertyGroup("General");
+
 	_alpha = createProperty<RealProperty>(PropertyID::Alpha, "Alpha", 0.5);
 	alpha()->createConstraint<RangeConstraint>(0.0, 1.0);
 	alpha()->setDescription("The alpha blending factor (Out = In1*Alpha + In2*[1-Alpha]).");
diff --git a/Grinder/pipeline/blocks/BinaryThresholdBlock.cpp b/Grinder/pipeline/blocks/BinaryThresholdBlock.cpp
index 8b8287c..5f1dfe6 100644
--- a/Grinder/pipeline/blocks/BinaryThresholdBlock.cpp
+++ b/Grinder/pipeline/blocks/BinaryThresholdBlock.cpp
@@ -25,6 +25,8 @@ void BinaryThresholdBlock::createProperties()
 {
 	Block::createProperties();
 
+	setPropertyGroup("General");
+
 	_thresholdType = createProperty<BinaryThresholdTypeProperty>(PropertyID::Type, "Type", static_cast<int>(BinaryThresholdType::Binary));
 	thresholdType()->setDescription("The threshold type.");
 
@@ -40,6 +42,17 @@ void BinaryThresholdBlock::createProperties()
 	targetValue()->setDescription("The value to use for binary thresholding if a pixel is above (below if inverted) the threshold value; ranges from 0 to 255.");
 }
 
+bool BinaryThresholdBlock::updateProperties(PropertyBase* updatedProp)
+{
+	bool updated = Block::updateProperties(updatedProp);
+
+	// Update properties to reflect property dependencies
+	updated |= enableDependantProperty(updatedProp, _thresholdType, _targetValue, [this]() { return *thresholdType() == BinaryThresholdType::Binary || *thresholdType() == BinaryThresholdType::BinaryInv; });
+	updated |= enableDependantProperty(updatedProp, _thresholdMode, _threshold, [this]() { return *thresholdMode() == BinaryThresholdMode::Manual; });
+
+	return updated;
+}
+
 void BinaryThresholdBlock::createPorts()
 {
 	DataDescriptors inPortDataDescs = {DataDescriptor::imageDescriptor(true, DataDescriptor::ValueType::Any), DataDescriptor::imageDescriptor(false, DataDescriptor::ValueType::Any)};
diff --git a/Grinder/pipeline/blocks/BinaryThresholdBlock.h b/Grinder/pipeline/blocks/BinaryThresholdBlock.h
index 66b2e63..0c83dfe 100644
--- a/Grinder/pipeline/blocks/BinaryThresholdBlock.h
+++ b/Grinder/pipeline/blocks/BinaryThresholdBlock.h
@@ -43,6 +43,7 @@ namespace grndr
 
 	protected:
 		virtual void createProperties() override;
+		virtual bool updateProperties(PropertyBase* updatedProp = nullptr) override;
 		virtual void createPorts() override;
 
 	private:
diff --git a/Grinder/pipeline/blocks/BlurBlock.cpp b/Grinder/pipeline/blocks/BlurBlock.cpp
index 1753211..3564085 100644
--- a/Grinder/pipeline/blocks/BlurBlock.cpp
+++ b/Grinder/pipeline/blocks/BlurBlock.cpp
@@ -24,6 +24,8 @@ void BlurBlock::createProperties()
 {
 	Block::createProperties();
 
+	setPropertyGroup("General");
+
 	_blurType = createProperty<BlurTypeProperty>(PropertyID::Type, "Type", static_cast<int>(BlurType::Gauss));
 	blurType()->setDescription("The blur type.");
 
@@ -32,16 +34,31 @@ void BlurBlock::createProperties()
 	kernelSize()->createConstraint<EvenOddConstraint>(EvenOddConstraint<unsigned int>::Type::Odd);
 	kernelSize()->setDescription("The size of the kernel used for blurring. Must be an odd value (3, 5, 7, and so on).");
 
-	_sigmaX = createProperty<RealProperty>(PropertyID::SigmaX, "Sigma X/Color", 0.0);
-	sigmaX()->setDescription("For Gaussian blur, this is the deviation in X direction (if zero, it is computed from the Kernel size). For bilateral filtering, this is the filter sigma in color space.");
-
-	_sigmaY = createProperty<RealProperty>(PropertyID::SigmaY, "Sigma Y/Coords", 0.0);
-	sigmaY()->setDescription("For Gaussian blur, this is the deviation in Y direction (if zero, it is computed from the Kernel size). For bilateral filtering, this is the filter sigma in coordinate space.");
+	_sigmaX = createProperty<RealProperty>(PropertyID::SigmaX, "Sigma X", 0.0);
+	_sigmaY = createProperty<RealProperty>(PropertyID::SigmaY, "Sigma Y", 0.0);
 
 	_diameter = createProperty<UIntProperty>(PropertyID::Diameter, "Diameter", 5);
 	diameter()->setDescription("The pixel neighborhood diameter used for bilateral filtering.");
 }
 
+bool BlurBlock::updateProperties(PropertyBase* updatedProp)
+{
+	bool updated = Block::updateProperties(updatedProp);
+
+	// Update properties to reflect property dependencies
+	updated |= enableDependantProperty(updatedProp, _blurType, _kernelSize, [this]() { return *blurType() != BlurType::Bilateral; });
+	updated |= enableDependantProperty(updatedProp, _blurType, _sigmaX, [this]() { return *blurType() == BlurType::Gauss || *blurType() == BlurType::Bilateral; });
+	updated |= enableDependantProperty(updatedProp, _blurType, _sigmaY, [this]() { return *blurType() == BlurType::Gauss || *blurType() == BlurType::Bilateral; });
+	updated |= enableDependantProperty(updatedProp, _blurType, _diameter, [this]() { return *blurType() == BlurType::Bilateral; });
+
+	updated |= updateDependantProperty(updatedProp, _blurType, _sigmaX, "Sigma X", "Standard deviation in X direction; if zero, it is computed from the Kernel size.", [this]() { return *blurType() == BlurType::Gauss; });
+	updated |= updateDependantProperty(updatedProp, _blurType, _sigmaY, "Sigma Y", "Standard deviation in Y direction; if zero, it is computed from the Kernel size.", [this]() { return *blurType() == BlurType::Gauss; });
+	updated |= updateDependantProperty(updatedProp, _blurType, _sigmaX, "Sigma color", "Filter sigma in color space.", [this]() { return *blurType() == BlurType::Bilateral; });
+	updated |= updateDependantProperty(updatedProp, _blurType, _sigmaY, "Sigma coords", "Filter sigma in coordinate space.", [this]() { return *blurType() == BlurType::Bilateral; });
+
+	return updated;
+}
+
 void BlurBlock::createPorts()
 {
 	DataDescriptors inPortDataDescs = {DataDescriptor::imageDescriptor(true, DataDescriptor::ValueType::Any), DataDescriptor::imageDescriptor(false, DataDescriptor::ValueType::Any)};
diff --git a/Grinder/pipeline/blocks/BlurBlock.h b/Grinder/pipeline/blocks/BlurBlock.h
index 11d417d..2338c7f 100644
--- a/Grinder/pipeline/blocks/BlurBlock.h
+++ b/Grinder/pipeline/blocks/BlurBlock.h
@@ -44,6 +44,7 @@ namespace grndr
 
 	protected:
 		virtual void createProperties() override;
+		virtual bool updateProperties(PropertyBase* updatedProp = nullptr) override;
 		virtual void createPorts() override;
 
 	private:
diff --git a/Grinder/pipeline/blocks/ContoursBlock.cpp b/Grinder/pipeline/blocks/ContoursBlock.cpp
index 2765f23..f74fc6b 100644
--- a/Grinder/pipeline/blocks/ContoursBlock.cpp
+++ b/Grinder/pipeline/blocks/ContoursBlock.cpp
@@ -24,12 +24,24 @@ void ContoursBlock::createProperties()
 {
 	Block::createProperties();
 
+	setPropertyGroup("General");
+
+	_fill = createProperty<BoolProperty>(PropertyID::Fill, "Fill contours", false);
+	fill()->setDescription("Fill the contour interiors.");
+
 	_width = createProperty<UIntProperty>(PropertyID::Width, "Width", 1);
 	width()->createConstraint<RangeConstraint>(1, std::numeric_limits<unsigned int>::max());
 	width()->setDescription("The width (thickness) of the lines the contours are drawn with.");
+}
 
-	_fill = createProperty<BoolProperty>(PropertyID::Fill, "Fill contours", false);
-	fill()->setDescription("Fill the contour interiors.");
+bool ContoursBlock::updateProperties(PropertyBase* updatedProp)
+{
+	bool updated = Block::updateProperties(updatedProp);
+
+	// Update properties to reflect property dependencies
+	updated |= enableDependantProperty(updatedProp, _fill, _width, [this]()->bool { return !*fill(); });
+
+	return updated;
 }
 
 void ContoursBlock::createPorts()
diff --git a/Grinder/pipeline/blocks/ContoursBlock.h b/Grinder/pipeline/blocks/ContoursBlock.h
index 365209a..8e991a5 100644
--- a/Grinder/pipeline/blocks/ContoursBlock.h
+++ b/Grinder/pipeline/blocks/ContoursBlock.h
@@ -39,6 +39,7 @@ namespace grndr
 
 	protected:
 		virtual void createProperties() override;
+		virtual bool updateProperties(PropertyBase* updatedProp) override;
 		virtual void createPorts() override;
 
 	private:
@@ -47,7 +48,7 @@ namespace grndr
 
 		std::shared_ptr<Port> _maskPort;
 		std::shared_ptr<Port> _targetPort;
-		std::shared_ptr<Port> _outPort;
+		std::shared_ptr<Port> _outPort;			
 	};
 }
 
diff --git a/Grinder/pipeline/blocks/ConvertToColorBlock.cpp b/Grinder/pipeline/blocks/ConvertToColorBlock.cpp
index 087995a..8f0d9d7 100644
--- a/Grinder/pipeline/blocks/ConvertToColorBlock.cpp
+++ b/Grinder/pipeline/blocks/ConvertToColorBlock.cpp
@@ -25,6 +25,8 @@ void ConvertToColorBlock::createProperties()
 {
 	Block::createProperties();
 
+	setPropertyGroup("General");
+
 	_colorSpace = createProperty<ColorSpaceProperty>(PropertyID::ColorSpace, "Color space", static_cast<int>(ColorSpace::RGB));
 	colorSpace()->setDescription("The target color space.");
 }
diff --git a/Grinder/pipeline/blocks/DilateBlock.cpp b/Grinder/pipeline/blocks/DilateBlock.cpp
index 8422d8a..eb7bda7 100644
--- a/Grinder/pipeline/blocks/DilateBlock.cpp
+++ b/Grinder/pipeline/blocks/DilateBlock.cpp
@@ -24,6 +24,8 @@ void DilateBlock::createProperties()
 {
 	Block::createProperties();
 
+	setPropertyGroup("General");
+
 	_morphShape = createProperty<MorphShapeProperty>(PropertyID::Type, "Shape", static_cast<int>(MorphShape::Rectangle));
 	morphShape()->setDescription("The shape of the structuring element used for morphing.");
 
diff --git a/Grinder/pipeline/blocks/DistanceTransformBlock.cpp b/Grinder/pipeline/blocks/DistanceTransformBlock.cpp
index ab2e002..71b102e 100644
--- a/Grinder/pipeline/blocks/DistanceTransformBlock.cpp
+++ b/Grinder/pipeline/blocks/DistanceTransformBlock.cpp
@@ -24,6 +24,8 @@ void DistanceTransformBlock::createProperties()
 {
 	Block::createProperties();
 
+	setPropertyGroup("General");
+
 	_distanceType = createProperty<DistanceTypeProperty>(PropertyID::Type, "Distance type", static_cast<int>(DistanceType::L2));
 	distanceType()->setDescription("The distance type.");
 
@@ -31,6 +33,16 @@ void DistanceTransformBlock::createProperties()
 	maskSize()->setDescription("The mask size.");
 }
 
+bool DistanceTransformBlock::updateProperties(PropertyBase* updatedProp)
+{
+	bool updated = Block::updateProperties(updatedProp);
+
+	// Update properties to reflect property dependencies
+	updated |= enableDependantProperty(updatedProp, _distanceType, _maskSize, [this]() { return *distanceType() == DistanceType::L2; });
+
+	return updated;
+}
+
 void DistanceTransformBlock::createPorts()
 {
 	DataDescriptors inPortDataDescs = {DataDescriptor::imageDescriptor(false)};
diff --git a/Grinder/pipeline/blocks/DistanceTransformBlock.h b/Grinder/pipeline/blocks/DistanceTransformBlock.h
index e65d446..9efa309 100644
--- a/Grinder/pipeline/blocks/DistanceTransformBlock.h
+++ b/Grinder/pipeline/blocks/DistanceTransformBlock.h
@@ -39,6 +39,7 @@ namespace grndr
 
 	protected:
 		virtual void createProperties() override;
+		virtual bool updateProperties(PropertyBase* updatedProp = nullptr) override;
 		virtual void createPorts() override;
 
 	private:
diff --git a/Grinder/pipeline/blocks/ErodeBlock.cpp b/Grinder/pipeline/blocks/ErodeBlock.cpp
index 8579a76..61e6ca3 100644
--- a/Grinder/pipeline/blocks/ErodeBlock.cpp
+++ b/Grinder/pipeline/blocks/ErodeBlock.cpp
@@ -24,6 +24,8 @@ void ErodeBlock::createProperties()
 {
 	Block::createProperties();
 
+	setPropertyGroup("General");
+
 	_morphShape = createProperty<MorphShapeProperty>(PropertyID::Type, "Shape", static_cast<int>(MorphShape::Rectangle));
 	morphShape()->setDescription("The shape of the structuring element used for morphing.");
 
diff --git a/Grinder/pipeline/blocks/ImageTagsBlock.cpp b/Grinder/pipeline/blocks/ImageTagsBlock.cpp
index eab2f58..8836dcb 100644
--- a/Grinder/pipeline/blocks/ImageTagsBlock.cpp
+++ b/Grinder/pipeline/blocks/ImageTagsBlock.cpp
@@ -36,6 +36,8 @@ void ImageTagsBlock::createProperties()
 {
 	Block::createProperties();
 
+	setPropertyGroup("General");
+
 	_imageTags = createProperty<ImageTagsProperty>(PropertyID::ImageTags, "Tags");
 	imageTags()->setDescription("A list of user-definable tags.");
 }
diff --git a/Grinder/pipeline/blocks/InputBlock.cpp b/Grinder/pipeline/blocks/InputBlock.cpp
index bd5b00c..45d2a45 100644
--- a/Grinder/pipeline/blocks/InputBlock.cpp
+++ b/Grinder/pipeline/blocks/InputBlock.cpp
@@ -25,6 +25,8 @@ void InputBlock::createProperties()
 {
 	Block::createProperties();
 
+	setPropertyGroup("General");
+
 	_imageReference = createProperty<ImageReferenceProperty>(PropertyID::ImageReference, "Image", nullptr);
 	imageReference()->setDescription("The image to use as the input; can either be the currently active image or a fixed one.");
 }
diff --git a/Grinder/pipeline/blocks/NormalizeBlock.cpp b/Grinder/pipeline/blocks/NormalizeBlock.cpp
index c9f18df..f38df35 100644
--- a/Grinder/pipeline/blocks/NormalizeBlock.cpp
+++ b/Grinder/pipeline/blocks/NormalizeBlock.cpp
@@ -24,6 +24,8 @@ void NormalizeBlock::createProperties()
 {
 	Block::createProperties();
 
+	setPropertyGroup("General");
+
 	_lowerRange = createProperty<UIntProperty>(PropertyID::Alpha, "Lower range", 0);
 	lowerRange()->setDescription("The lower range value used for range normalization.");
 
diff --git a/Grinder/pipeline/blocks/OutputBlock.cpp b/Grinder/pipeline/blocks/OutputBlock.cpp
index fa1b57b..b43bce4 100644
--- a/Grinder/pipeline/blocks/OutputBlock.cpp
+++ b/Grinder/pipeline/blocks/OutputBlock.cpp
@@ -24,6 +24,8 @@ void OutputBlock::createProperties()
 {
 	Block::createProperties();
 
+	setPropertyGroup("Miscellaneous");
+
 	_scaleFactor = createProperty<RealProperty>(PropertyID::Factor, "Scale factor", 1.0);
 	scaleFactor()->setDescription("Scale factor for the image data.");
 
diff --git a/Grinder/pipeline/blocks/ReplaceColorBlock.cpp b/Grinder/pipeline/blocks/ReplaceColorBlock.cpp
index 92d2792..e997a74 100644
--- a/Grinder/pipeline/blocks/ReplaceColorBlock.cpp
+++ b/Grinder/pipeline/blocks/ReplaceColorBlock.cpp
@@ -24,6 +24,8 @@ void ReplaceColorBlock::createProperties()
 {
 	Block::createProperties();
 
+	setPropertyGroup("General");
+
 	_fromColor = createProperty<ColorProperty>(PropertyID::From, "From color", QColor{255, 255, 255});
 	fromColor()->setDescription("The color to replace.");
 
diff --git a/Grinder/pipeline/blocks/SharpenBlock.cpp b/Grinder/pipeline/blocks/SharpenBlock.cpp
index 22b0bf5..744c3ae 100644
--- a/Grinder/pipeline/blocks/SharpenBlock.cpp
+++ b/Grinder/pipeline/blocks/SharpenBlock.cpp
@@ -24,6 +24,8 @@ void SharpenBlock::createProperties()
 {
 	Block::createProperties();
 
+	setPropertyGroup("General");
+
 	_sharpenType = createProperty<SharpenTypeProperty>(PropertyID::Type, "Type", static_cast<int>(SharpenType::Laplacian));
 	sharpenType()->setDescription("The sharpening type.");
 
diff --git a/Grinder/pipeline/properties/DistanceTransformMaskProperty.cpp b/Grinder/pipeline/properties/DistanceTransformMaskProperty.cpp
index a80b40e..14af4bb 100644
--- a/Grinder/pipeline/properties/DistanceTransformMaskProperty.cpp
+++ b/Grinder/pipeline/properties/DistanceTransformMaskProperty.cpp
@@ -12,5 +12,4 @@ void DistanceTransformMaskProperty::initProperty()
 
 	_enumMap[DistanceTransformMask::Mask3] = "3";
 	_enumMap[DistanceTransformMask::Mask5] = "5";
-	_enumMap[DistanceTransformMask::Precise] = "Precise";
 }
diff --git a/Grinder/ui/image/ImageEditorPropertyWidget.cpp b/Grinder/ui/image/ImageEditorPropertyWidget.cpp
index f768046..e0b5720 100644
--- a/Grinder/ui/image/ImageEditorPropertyWidget.cpp
+++ b/Grinder/ui/image/ImageEditorPropertyWidget.cpp
@@ -14,7 +14,7 @@
 ImageEditorPropertyWidget::ImageEditorPropertyWidget(QWidget* parent) : QWidget(parent),
 	_layout{new QGridLayout{this}}
 {
-	_layout->setContentsMargins(6, 6, 6, 6);
+	_layout->setContentsMargins(4, 4, 10, 4);
 	_layout->setHorizontalSpacing(6);
 	_layout->setVerticalSpacing(4);
 	setLayout(_layout);
@@ -35,26 +35,51 @@ void ImageEditorPropertyWidget::assignUiComponents(ImageEditor* imageEditor)
 
 void ImageEditorPropertyWidget::clear()
 {
+	if (_currentPropertyObject)
+		disconnect(_currentPropertyObject, nullptr, this, nullptr);
+
+	_currentPropertyObject = nullptr;
+
 	// Remove all properties from the layout
 	UIUtils::removeChildrenFromLayout(_layout);
 }
 
-void ImageEditorPropertyWidget::addProperties(const PropertyVector* properties)
+void ImageEditorPropertyWidget::addProperties(const PropertyObject* propertyObject)
 {
 	clear();
 
+	_currentPropertyObject = propertyObject;
+
 	bool propertiesEmpty = true;
 
-	if (properties)
+	if (propertyObject)
 	{
-		for (auto& property : properties->sorted())
+		for (auto group : propertyObject->getPropertyGroups())
 		{
-			if (!property->hasFlag(PropertyBase::Flag::ReadOnly) && !property->hasFlag(PropertyBase::Flag::Hidden))
+			std::vector<std::shared_ptr<PropertyBase>> properties;
+
+			for (auto& property : propertyObject->properties(group))
+			{
+				// Ignore read-only properties
+				if (!property->hasFlag(PropertyBase::Flag::ReadOnly) && !property->hasFlag(PropertyBase::Flag::Hidden))
+					properties.push_back(property);
+			}
+
+			if (!properties.empty())
 			{
-				addProperty(property);
 				propertiesEmpty = false;
+
+				// Add an item for the property group
+				addPropertyGroup(group);
+
+				// Add items for all properties belonging to the current group
+				for (auto& property : properties)
+					addProperty(property);
 			}
 		}
+
+		// Reflect property updates
+		connect(propertyObject, &PropertyObject::propertiesUpdated, this, &ImageEditorPropertyWidget::updateProperties, Qt::QueuedConnection);	// Delay the update to prevent race conditions
 	}
 
 	if (propertiesEmpty)
@@ -68,10 +93,22 @@ void ImageEditorPropertyWidget::addProperties(const PropertyVector* properties)
 	_layout->addItem(new QSpacerItem{0, 0, QSizePolicy::Expanding, QSizePolicy::Expanding}, _layout->rowCount(), 1);
 }
 
+void ImageEditorPropertyWidget::addPropertyGroup(QString group)
+{
+	auto label = new QLabel{"<em>" + group + "</em>"};
+	label->setFont(GrinderApplication::boldFont(label));
+	label->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
+	label->setStyleSheet(QString{"background: qlineargradient(spread:pad, x1:0 y1:0, x2:1 y2:0, stop:0 %1, stop:1 %2); padding: 2px;"}
+						 .arg(QPalette{}.color(QPalette::Dark).lighter(120).name()).arg(QPalette{}.color(QPalette::Button).lighter(104).name()));
+
+	int row = _layout->rowCount();
+	_layout->addWidget(label, row, 0, 1, 2);
+}
+
 void ImageEditorPropertyWidget::addProperty(const std::shared_ptr<PropertyBase>& property)
 {
 	auto editor = property->createEditor(nullptr);
-	auto label = new QLabel{property->getName() + ":"};
+	auto label = new QLabel{"  " + property->getName() + ":"};
 	label->setFont(GrinderApplication::boldFont(label));
 
 	if (!editor)
@@ -82,11 +119,14 @@ void ImageEditorPropertyWidget::addProperty(const std::shared_ptr<PropertyBase>&
 	int row = _layout->rowCount();
 	_layout->addWidget(label, row, 0);
 	_layout->addWidget(editor, row, 1);
+
+	if (property->hasFlag(PropertyBase::Flag::Disabled))
+		editor->setEnabled(false);
 }
 
 void ImageEditorPropertyWidget::populateProperties()
-{
-	const PropertyVector* properties = &_imageEditor->editorTools().activeTool()->properties();	// Use the active tool's properties by default
+{	
+	const PropertyObject* propertyObject = _imageEditor->editorTools().activeTool();	// Use the active tool's properties by default
 
 	// See if there are any items selected in the editor
 	if (auto scene = _imageEditor->controller().activeScene())
@@ -99,12 +139,18 @@ void ImageEditorPropertyWidget::populateProperties()
 			if (selectedItems.size() == 1)
 			{
 				if (auto draftItem = selectedItems.front()->draftItem().lock())	// Make sure that the underlying draft item still exists
-					properties = &draftItem->properties();
+					propertyObject = draftItem.get();
 			}
 		}
 	}
 
-	addProperties(properties);
+	addProperties(propertyObject);
+}
+
+void ImageEditorPropertyWidget::updateProperties()
+{
+	// Just re-create all property editors to reflect the updated properties
+	populateProperties();
 }
 
 void ImageEditorPropertyWidget::imageEditorToolChanged(const ImageEditorTool* tool)
diff --git a/Grinder/ui/image/ImageEditorPropertyWidget.h b/Grinder/ui/image/ImageEditorPropertyWidget.h
index 24c64f9..2ddd69c 100644
--- a/Grinder/ui/image/ImageEditorPropertyWidget.h
+++ b/Grinder/ui/image/ImageEditorPropertyWidget.h
@@ -32,11 +32,13 @@ namespace grndr
 		void clear();
 
 	private:
-		void addProperties(const PropertyVector* properties);
+		void addProperties(const PropertyObject* propertyObject);
+		void addPropertyGroup(QString group);
 		void addProperty(const std::shared_ptr<PropertyBase>& property);
 
 	private slots:
 		void populateProperties();
+		void updateProperties();
 
 		void imageEditorToolChanged(const ImageEditorTool* tool);
 
@@ -45,6 +47,8 @@ namespace grndr
 
 	private:
 		QGridLayout* _layout{nullptr};
+
+		const PropertyObject* _currentPropertyObject{nullptr};
 	};
 }
 
diff --git a/Grinder/ui/image/ImageEditorTool.cpp b/Grinder/ui/image/ImageEditorTool.cpp
index 74f8efe..934dc29 100644
--- a/Grinder/ui/image/ImageEditorTool.cpp
+++ b/Grinder/ui/image/ImageEditorTool.cpp
@@ -19,6 +19,7 @@ ImageEditorTool::ImageEditorTool(ImageEditor* imageEditor, QString name, QString
 void ImageEditorTool::initImageEditorTool()
 {
 	createProperties();
+	updateProperties();
 }
 
 void ImageEditorTool::toolActivated(ImageEditorTool* prevTool)
diff --git a/Grinder/ui/image/tools/BoxDraftItemTool.cpp b/Grinder/ui/image/tools/BoxDraftItemTool.cpp
index a251400..07b15f1 100644
--- a/Grinder/ui/image/tools/BoxDraftItemTool.cpp
+++ b/Grinder/ui/image/tools/BoxDraftItemTool.cpp
@@ -19,9 +19,13 @@ void BoxDraftItemTool::createProperties()
 	DraftItemTool::createProperties();
 
 	// Create specific properties
+	setPropertyGroup("General");
+
 	_boxSize = createProperty<SizeProperty>(PropertyID::Size, "Size", QSize{50, 50});
 	boxSize()->setDescription("The size of the box.");
 
+	setPropertyGroup("Bounding box", "Attributes");
+
 	_lineWidth = createProperty<UIntProperty>(PropertyID::Width, "Line width", 5);
 	lineWidth()->createConstraint<RangeConstraint>(1, 100);
 	lineWidth()->setDescription("The width of the box lines.");	
diff --git a/Grinder/ui/image/tools/DraftItemTool.cpp b/Grinder/ui/image/tools/DraftItemTool.cpp
index 9b93e38..70733b3 100644
--- a/Grinder/ui/image/tools/DraftItemTool.cpp
+++ b/Grinder/ui/image/tools/DraftItemTool.cpp
@@ -66,12 +66,16 @@ void DraftItemTool::createProperties()
 	ImageEditorTool::createProperties();
 
 	// Create standard properties
+	setPropertyGroup("General");
+
 	_primaryColor = createProperty<ColorProperty>(PropertyID::PrimaryColor, "Primary color", QColor{255, 255, 255}, PropertyBase::Flag::Hidden);
 	primaryColor()->setDescription("The primary color to use.");
 
 	_position = createProperty<PointProperty>(PropertyID::Position, "Position", QPoint{0, 0}, PropertyBase::Flag::Hidden);
 	position()->setDescription("The item position.");
 
+	setPropertyGroup("Attributes");
+
 	_hasDirection = createProperty<BoolProperty>(PropertyID::HasDirection, "Has direction", false);
 	hasDirection()->setDescription("Specifies whether the box has a direction/orientation.");
 
@@ -83,6 +87,16 @@ void DraftItemTool::createProperties()
 	imageTagsAllotment()->setDescription("The assigned tags.");
 }
 
+bool DraftItemTool::updateProperties(PropertyBase* updatedProp)
+{
+	bool updated = PropertyObject::updateProperties(updatedProp);
+
+	// Update properties to reflect property dependencies
+	updated |= enableDependantProperty(updatedProp, _hasDirection, _direction, [this]()->bool { return *hasDirection(); });
+
+	return updated;
+}
+
 ImageEditorTool::InputEventResult DraftItemTool::mousePressed(const QGraphicsSceneMouseEvent* event)
 {
 	// Remember the initial position for later use
diff --git a/Grinder/ui/image/tools/DraftItemTool.h b/Grinder/ui/image/tools/DraftItemTool.h
index 97f1def..47a36c0 100644
--- a/Grinder/ui/image/tools/DraftItemTool.h
+++ b/Grinder/ui/image/tools/DraftItemTool.h
@@ -42,6 +42,7 @@ namespace grndr
 
 	protected:
 		virtual void createProperties() override;
+		virtual bool updateProperties(PropertyBase* updatedProp = nullptr) override;
 
 		virtual InputEventResult mousePressed(const QGraphicsSceneMouseEvent* event) override;
 		virtual InputEventResult mouseMoved(const QGraphicsSceneMouseEvent* event) override;
diff --git a/Grinder/ui/image/tools/EraserTool.cpp b/Grinder/ui/image/tools/EraserTool.cpp
index a077b53..8084c73 100644
--- a/Grinder/ui/image/tools/EraserTool.cpp
+++ b/Grinder/ui/image/tools/EraserTool.cpp
@@ -21,6 +21,8 @@ void EraserTool::createProperties()
 	PaintbrushTool::createProperties();
 
 	// The eraser should have a default width equal to the cursor size
+	setPropertyGroup("General");
+
 	penWidth()->setValue(_cursor.pixmap().width());
 	penWidth()->setName("Eraser width");
 	penWidth()->setDescription("The width of the eraser in (screen) pixels.");
diff --git a/Grinder/ui/image/tools/LineDraftItemTool.cpp b/Grinder/ui/image/tools/LineDraftItemTool.cpp
index bbb1321..a018bcf 100644
--- a/Grinder/ui/image/tools/LineDraftItemTool.cpp
+++ b/Grinder/ui/image/tools/LineDraftItemTool.cpp
@@ -19,6 +19,8 @@ void LineDraftItemTool::createProperties()
 	DraftItemTool::createProperties();
 
 	// Create specific properties
+	setPropertyGroup("Line", "Attributes");
+
 	_lineWidth = createProperty<UIntProperty>(PropertyID::Width, "Line width", 3);
 	lineWidth()->createConstraint<RangeConstraint>(1, 100);
 	lineWidth()->setDescription("The width of the line.");
diff --git a/Grinder/ui/image/tools/PaintbrushTool.cpp b/Grinder/ui/image/tools/PaintbrushTool.cpp
index 993ffb2..a50d898 100644
--- a/Grinder/ui/image/tools/PaintbrushTool.cpp
+++ b/Grinder/ui/image/tools/PaintbrushTool.cpp
@@ -26,6 +26,8 @@ void PaintbrushTool::createProperties()
 	ImageEditorTool::createProperties();
 
 	// Create specific properties
+	setPropertyGroup("General");
+
 	_penWidth = createProperty<UIntProperty>(PropertyID::Width, "Brush width", 1);
 	penWidth()->createConstraint<RangeConstraint>(1, 100);
 	penWidth()->setDescription("The width of the pixels to paint.");
diff --git a/Grinder/ui/mainwnd/GrinderWindow.ui b/Grinder/ui/mainwnd/GrinderWindow.ui
index b1a0e45..42bb2a0 100644
--- a/Grinder/ui/mainwnd/GrinderWindow.ui
+++ b/Grinder/ui/mainwnd/GrinderWindow.ui
@@ -358,9 +358,6 @@
        <property name="uniformRowHeights">
         <bool>true</bool>
        </property>
-       <property name="sortingEnabled">
-        <bool>true</bool>
-       </property>
        <property name="columnCount">
         <number>2</number>
        </property>
diff --git a/Grinder/ui/properties/BlockPropertyTreeItem.h b/Grinder/ui/properties/BlockPropertyTreeItem.h
index 567bb6f..6107ca9 100644
--- a/Grinder/ui/properties/BlockPropertyTreeItem.h
+++ b/Grinder/ui/properties/BlockPropertyTreeItem.h
@@ -21,7 +21,7 @@ namespace grndr
 		virtual void updateItem() override;
 
 	public:
-		const std::weak_ptr<Block>& pipelineItem() const { return _block; }
+		const std::weak_ptr<Block>& block() const { return _block; }
 
 	private:
 		std::weak_ptr<Block> _block;
diff --git a/Grinder/ui/properties/GroupPropertyTreeItem.cpp b/Grinder/ui/properties/GroupPropertyTreeItem.cpp
new file mode 100644
index 0000000..3a56b25
--- /dev/null
+++ b/Grinder/ui/properties/GroupPropertyTreeItem.cpp
@@ -0,0 +1,29 @@
+/******************************************************************************
+ * File: GroupPropertyTreeItem.cpp
+ * Date: 29.6.2018
+ *****************************************************************************/
+
+#include "Grinder.h"
+#include "GroupPropertyTreeItem.h"
+
+GroupPropertyTreeItem::GroupPropertyTreeItem(QString group) :
+	_group{group}
+{
+	if (group.isEmpty())
+		throw std::invalid_argument{_EXCPT("group may not be empty")};
+
+	setFlags(Qt::ItemIsEnabled);
+
+	QFont fontGroup = font(0);
+	fontGroup.setBold(true);
+	fontGroup.setItalic(true);
+	setFont(0, fontGroup);
+
+	updateItem();
+}
+
+void GroupPropertyTreeItem::updateItem()
+{
+	setText(0, _group);
+	setToolTip(0, text(0));
+}
diff --git a/Grinder/ui/properties/GroupPropertyTreeItem.h b/Grinder/ui/properties/GroupPropertyTreeItem.h
new file mode 100644
index 0000000..b37cfe9
--- /dev/null
+++ b/Grinder/ui/properties/GroupPropertyTreeItem.h
@@ -0,0 +1,28 @@
+/******************************************************************************
+ * File: GroupPropertyTreeItem.h
+ * Date: 29.6.2018
+ *****************************************************************************/
+
+#ifndef GROUPPROPERTYTREEITEM_H
+#define GROUPPROPERTYTREEITEM_H
+
+#include "PropertyTreeItem.h"
+
+namespace grndr
+{
+	class Block;
+
+	class GroupPropertyTreeItem : public PropertyTreeItem
+	{
+	public:
+		GroupPropertyTreeItem(QString group);
+
+	public:
+		virtual void updateItem() override;
+
+	private:
+		QString _group;
+	};
+}
+
+#endif
diff --git a/Grinder/ui/properties/PropertyTreeWidget.cpp b/Grinder/ui/properties/PropertyTreeWidget.cpp
index c47e7b8..881fe20 100644
--- a/Grinder/ui/properties/PropertyTreeWidget.cpp
+++ b/Grinder/ui/properties/PropertyTreeWidget.cpp
@@ -7,6 +7,7 @@
 #include "PropertyTreeWidget.h"
 #include "BlockPropertyTreeItem.h"
 #include "ValuePropertyTreeItem.h"
+#include "GroupPropertyTreeItem.h"
 #include "PropertyTreeItemDelegate.h"
 #include "core/GrinderApplication.h"
 #include "ui/graph/GraphBlockNode.h"
@@ -44,6 +45,23 @@ void PropertyTreeWidget::setupUi(QLabel* propertyDescLabel)
 	updatePropertyDescription();
 }
 
+void PropertyTreeWidget::clear()
+{
+	for (int i = 0; i < topLevelItemCount(); ++i)
+	{
+		if (auto blockItem = dynamic_cast<BlockPropertyTreeItem*>(topLevelItem(i)))
+		{
+			if (auto block = blockItem->block().lock())	// Make sure that the underlying block still exists
+				disconnect(block.get(), nullptr, this, nullptr);
+		}
+	}
+
+	QTreeWidget::clear();
+
+	updateActions();
+	updatePropertyDescription();
+}
+
 void PropertyTreeWidget::showEvent(QShowEvent* event)
 {
 	QTreeWidget::showEvent(event);
@@ -80,7 +98,7 @@ void PropertyTreeWidget::keyPressEvent(QKeyEvent* event)
 		QTreeWidget::keyPressEvent(event);
 }
 
-void PropertyTreeWidget::addBlocks(const std::vector<std::shared_ptr<grndr::Block> >& blocks)
+void PropertyTreeWidget::addBlocks(const std::vector<std::shared_ptr<Block>>& blocks)
 {
 	// Create a top-level item for every block
 	for (auto block : blocks)
@@ -88,10 +106,13 @@ void PropertyTreeWidget::addBlocks(const std::vector<std::shared_ptr<grndr::Bloc
 		BlockPropertyTreeItem* treeItem = new BlockPropertyTreeItem{block};
 		addTopLevelItem(treeItem);
 
+		// Add all properties of the block
 		addProperties(block.get(), treeItem);
 
 		treeItem->setExpanded(true);
 		treeItem->setFirstColumnSpanned(true);
+
+		connect(block.get(), &PropertyObject::propertiesUpdated, this, &PropertyTreeWidget::updateProperties);
 	}
 
 	updateColumnWidths();
@@ -99,24 +120,39 @@ void PropertyTreeWidget::addBlocks(const std::vector<std::shared_ptr<grndr::Bloc
 
 void PropertyTreeWidget::addProperties(const PipelineItem* pipelineItem, QTreeWidgetItem* parentItem)
 {
-	std::vector<std::shared_ptr<PropertyBase>> properties;
+	bool propertiesEmpty = true;
 
-	// Ignore read-only properties
-	for (auto& property : pipelineItem->properties())
+	for (auto group : pipelineItem->getPropertyGroups())
 	{
-		if (!property->hasFlag(PropertyBase::Flag::ReadOnly) && !property->hasFlag(PropertyBase::Flag::Hidden))
-			properties.push_back(property);
-	}
+		std::vector<std::shared_ptr<PropertyBase>> properties;
 
-	if (!properties.empty())
-	{
-		for (auto& property : properties)
+		for (auto& property : pipelineItem->properties(group))
 		{
-			ValuePropertyTreeItem* treeItem = new ValuePropertyTreeItem{property};
-			parentItem->addChild(treeItem);
+			// Ignore read-only properties
+			if (!property->hasFlag(PropertyBase::Flag::ReadOnly) && !property->hasFlag(PropertyBase::Flag::Hidden))
+				properties.push_back(property);
+		}
+
+		if (!properties.empty())
+		{
+			propertiesEmpty = false;
+
+			// Add an item for the property group
+			GroupPropertyTreeItem* groupItem = new GroupPropertyTreeItem{group};
+			parentItem->addChild(groupItem);
+
+			// Add items for all properties belonging to the current group
+			for (auto& property : properties)
+			{
+				ValuePropertyTreeItem* treeItem = new ValuePropertyTreeItem{property};
+				parentItem->addChild(treeItem);
+			}
+
+			groupItem->setFirstColumnSpanned(true);
 		}
 	}
-	else
+
+	if (propertiesEmpty)
 	{
 		// Add an item showing that the pipeline item has no properties
 		QTreeWidgetItem* emptyItem = new QTreeWidgetItem({"No properties to display"});
@@ -194,6 +230,25 @@ void PropertyTreeWidget::sceneSelectionChanged()
 	updateActions();
 }
 
+void PropertyTreeWidget::updateProperties()
+{
+	// Update all property items
+	for (int i = 0; i < topLevelItemCount(); ++i)
+	{
+		auto rootItem = topLevelItem(i);
+
+		for (int j = 0; j < rootItem->childCount(); ++j)
+		{
+			if (auto childItem = dynamic_cast<ValuePropertyTreeItem*>(rootItem->child(j)))
+				childItem->updateItem();
+		}
+	}
+
+	updateActions();
+	updatePropertyDescription();
+	updateColumnWidths();
+}
+
 void PropertyTreeWidget::updateActions()
 {
 	auto item = currentValuePropertyItem();
diff --git a/Grinder/ui/properties/PropertyTreeWidget.h b/Grinder/ui/properties/PropertyTreeWidget.h
index 6658e76..73bc9db 100644
--- a/Grinder/ui/properties/PropertyTreeWidget.h
+++ b/Grinder/ui/properties/PropertyTreeWidget.h
@@ -31,7 +31,7 @@ namespace grndr
 	public:
 		void setupUi(QLabel* propertyDescLabel);
 
-		void clear() { QTreeWidget::clear(); updateActions(); updatePropertyDescription(); }
+		void clear();
 
 	protected:
 		virtual void showEvent(QShowEvent* event) override;
@@ -54,6 +54,8 @@ namespace grndr
 
 		void sceneSelectionChanged();
 
+		void updateProperties();
+
 		void expandAllItems() { expandAllItems(true); }
 		void collapseAllItems() { expandAllItems(false); }
 
diff --git a/Grinder/ui/properties/ValuePropertyTreeItem.cpp b/Grinder/ui/properties/ValuePropertyTreeItem.cpp
index 10e6a30..ae1a884 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();
 
@@ -35,10 +34,21 @@ void ValuePropertyTreeItem::updateItem()
 {
 	if (auto property = _property.lock())	// Make sure that the underlying property still exists
 	{
-		setText(0, property->getName());
-		setToolTip(0, text(0));
+		setText(0, "  " + property->getName());
+		setToolTip(0, property->getName());
 
 		setText(1, property->toString());
 		setToolTip(1, text(1));
+
+		if (property->hasFlag(PropertyBase::Flag::Disabled))
+		{
+			setFlags(flags()&~Qt::ItemIsEnabled);
+			setTextColor(1, QPalette{}.color(QPalette::Disabled, QPalette::WindowText));
+		}
+		else
+		{
+			setFlags(flags()|Qt::ItemIsEnabled);
+			setTextColor(1, QColor{20, 105, 140});
+		}
 	}
 }
diff --git a/Grinder/ui/properties/editors/AnglePropertyEditor.cpp b/Grinder/ui/properties/editors/AnglePropertyEditor.cpp
index b0751e2..0349c29 100644
--- a/Grinder/ui/properties/editors/AnglePropertyEditor.cpp
+++ b/Grinder/ui/properties/editors/AnglePropertyEditor.cpp
@@ -19,6 +19,7 @@ AnglePropertyEditor::AnglePropertyEditor(AngleProperty* property, QWidget* paren
 	_angleDial->setNotchTarget(45);
 	_angleDial->setInvertedAppearance(true);
 	_angleDial->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
+	_angleDial->setValue(90);
 
 	_angleEdit->setRange(0, 360);
 	_angleEdit->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
-- 
GitLab