From 9e3b0fed1aa11fa651ea1a27416a26e7e05e2b55 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20M=C3=BCller?= <d_muel20@uni-muenster.de>
Date: Wed, 28 Aug 2019 16:05:47 +0200
Subject: [PATCH] * Spawned tasks now offer less user interaction and aren't
 saved anymore * Tags port moved from ML method to learning block

---
 Grinder/Grinder.pro                           |   6 -
 Grinder/Version.h                             |   4 +-
 Grinder/engine/ProcessorBase.impl.h           |  14 +-
 Grinder/engine/data/DataBlob.cpp              |   3 +
 Grinder/ml/MachineLearningConfiguration.cpp   |   7 -
 Grinder/ml/MachineLearningConfiguration.h     |   9 +-
 .../BaristaClassifierTaskSpawner.impl.h       |   2 -
 .../ml/barista/tasks/BaristaInferenceTask.cpp |   6 -
 .../ml/barista/tasks/BaristaInferenceTask.h   |   3 -
 Grinder/ml/barista/tasks/BaristaTask.h        |   4 +-
 Grinder/ml/barista/tasks/BaristaTask.impl.h   |   9 +
 .../ml/barista/tasks/BaristaTrainingTask.cpp  |   6 -
 .../ml/barista/tasks/BaristaTrainingTask.h    |   3 -
 Grinder/ml/blocks/MachineLearningBlock.cpp    |   3 +
 Grinder/ml/blocks/MachineLearningBlock.h      |   3 +
 .../ml/blocks/MachineLearningMethodBlock.h    |   8 +-
 .../blocks/MachineLearningMethodBlock.impl.h  |  18 +-
 .../MachineLearningMethodProcessor.impl.h     |   1 -
 .../MachineLearningProcessor.impl.h           |  16 +-
 Grinder/ml/tasks/MachineLearningTask.h        |   2 +
 .../cmd/tasks/CommandInterfaceTask.cpp        |   2 +-
 .../cmd/tasks/CommandInterfaceTestTask.cpp    |   2 +-
 Grinder/task/Task.cpp                         |   3 +
 Grinder/task/Task.h                           |   9 +-
 Grinder/task/TaskCatalog.cpp                  |  42 +-
 Grinder/task/TaskCatalog.h                    |  11 +-
 Grinder/task/TaskPool.cpp                     |   2 +-
 Grinder/task/tasks/GenericTask.cpp            |   2 +-
 .../tasks/BaristaInferenceTaskWidget.cpp      | 131 ----
 .../tasks/BaristaInferenceTaskWidget.h        |  52 --
 .../tasks/BaristaInferenceTaskWidget.ui       | 298 --------
 .../tasks/BaristaTrainingTaskWidget.cpp       |  71 --
 .../barista/tasks/BaristaTrainingTaskWidget.h |  43 --
 .../tasks/BaristaTrainingTaskWidget.ui        | 263 -------
 Grinder/ui/task/TaskPoolWidget.cpp            | 231 +++----
 Grinder/ui/task/TaskWidget.cpp                | 365 +++++-----
 Grinder/ui/task/TaskWidget.ui                 | 640 +++++++++---------
 37 files changed, 727 insertions(+), 1567 deletions(-)
 delete mode 100644 Grinder/ui/barista/tasks/BaristaInferenceTaskWidget.cpp
 delete mode 100644 Grinder/ui/barista/tasks/BaristaInferenceTaskWidget.h
 delete mode 100644 Grinder/ui/barista/tasks/BaristaInferenceTaskWidget.ui
 delete mode 100644 Grinder/ui/barista/tasks/BaristaTrainingTaskWidget.cpp
 delete mode 100644 Grinder/ui/barista/tasks/BaristaTrainingTaskWidget.h
 delete mode 100644 Grinder/ui/barista/tasks/BaristaTrainingTaskWidget.ui

diff --git a/Grinder/Grinder.pro b/Grinder/Grinder.pro
index 4b70225..1c9170d 100644
--- a/Grinder/Grinder.pro
+++ b/Grinder/Grinder.pro
@@ -346,9 +346,7 @@ SOURCES += \
     ui/task/tasks/GenericTaskWidget.cpp \
 	ml/barista/BaristaMessage.cpp \
 	ml/barista/tasks/BaristaTrainingTask.cpp \
-	ui/barista/tasks/BaristaTrainingTaskWidget.cpp \
 	ml/barista/tasks/BaristaInferenceTask.cpp \
-	ui/barista/tasks/BaristaInferenceTaskWidget.cpp \
 	ui/widgets/project/LabelsComboBox.cpp \
 	ui/widgets/project/ImageReferencesCheckListWidget.cpp \
 	network/cmd/tasks/CommandInterfaceTask.cpp \
@@ -849,9 +847,7 @@ HEADERS += \
     ui/task/tasks/GenericTaskWidget.h \
 	ml/barista/BaristaMessage.h \
 	ml/barista/tasks/BaristaTrainingTask.h \
-	ui/barista/tasks/BaristaTrainingTaskWidget.h \
 	ml/barista/tasks/BaristaInferenceTask.h \
-	ui/barista/tasks/BaristaInferenceTaskWidget.h \
 	ui/widgets/project/LabelsComboBox.h \
 	ui/widgets/project/ImageReferencesCheckListWidget.h \
 	network/cmd/tasks/CommandInterfaceTask.h \
@@ -1006,8 +1002,6 @@ FORMS += \
     ui/dlg/TextViewerDialog.ui \
     ui/task/ConfigureTaskDialog.ui \
     ui/task/tasks/GenericTaskWidget.ui \
-	ui/barista/tasks/BaristaTrainingTaskWidget.ui \
-	ui/barista/tasks/BaristaInferenceTaskWidget.ui \
 	ui/cmd/tasks/CommandInterfaceTaskWidget.ui \
 	ui/cmd/tasks/CommandInterfaceTestTaskWidget.ui \
     ui/dlg/BrowseDialog.ui
diff --git a/Grinder/Version.h b/Grinder/Version.h
index 12a1b6a..5dc0163 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			"27.8.2019"
+#define GRNDR_INFO_DATE			"28.8.2019"
 #define GRNDR_INFO_COMPANY		"WWU Muenster"
 #define GRNDR_INFO_WEBSITE		"http://www.uni-muenster.de"
 
 #define GRNDR_VERSION_MAJOR		0
 #define GRNDR_VERSION_MINOR		15
 #define GRNDR_VERSION_REVISION	0
-#define GRNDR_VERSION_BUILD		380
+#define GRNDR_VERSION_BUILD		382
 
 namespace grndr
 {
diff --git a/Grinder/engine/ProcessorBase.impl.h b/Grinder/engine/ProcessorBase.impl.h
index fa7f8ae..d7fc501 100644
--- a/Grinder/engine/ProcessorBase.impl.h
+++ b/Grinder/engine/ProcessorBase.impl.h
@@ -37,15 +37,13 @@ ValueType ProcessorBase::portData(EngineExecutionContext& ctx, const Port* port,
 template<typename PropType>
 PropType* ProcessorBase::portProperty(const Port* port, PropertyID propertyID, const Connection* connection, bool required) const
 {
+	PropType* property = nullptr;
+
 	if (auto dataPort = resolveDataPort(port, connection, required))
-	{
-		auto property = _block->portProperty<PropType>(dataPort, propertyID);
+		property = dataPort->block()->portProperty<PropType>(dataPort, propertyID);
 
-		if (!property && required)
-			throwProcessorException(QString{"No property with ID '%2' could be retrieved for port '%1'"}.arg(port->getName()).arg(propertyID));
+	if (!property && required)
+		throwProcessorException(QString{"No property with ID '%2' could be retrieved for port '%1'"}.arg(port->getName()).arg(propertyID));
 
-		return property;
-	}
-	else
-		return nullptr;
+	return property;
 }
diff --git a/Grinder/engine/data/DataBlob.cpp b/Grinder/engine/data/DataBlob.cpp
index 5711a23..1dc41b2 100644
--- a/Grinder/engine/data/DataBlob.cpp
+++ b/Grinder/engine/data/DataBlob.cpp
@@ -126,6 +126,9 @@ void DataBlob::mergeMetaData(const std::vector<MetaData>& metaData)
 	{
 		for (auto data : metaData)
 		{
+			if (data.empty())
+				continue;
+
 			if (_metaData.isEmpty())
 			{
 				_metaData = data;
diff --git a/Grinder/ml/MachineLearningConfiguration.cpp b/Grinder/ml/MachineLearningConfiguration.cpp
index 0e0b7c8..8e8585d 100644
--- a/Grinder/ml/MachineLearningConfiguration.cpp
+++ b/Grinder/ml/MachineLearningConfiguration.cpp
@@ -6,10 +6,3 @@
 #include "Grinder.h"
 #include "MachineLearningConfiguration.h"
 #include "MachineLearningExceptions.h"
-
-void MachineLearningConfiguration::verifyConfiguration() const
-{
-	// Verify general settings
-	if (!_imageTags)
-		throw MachineLearningException{_EXCPT("Image tags may not be null")};
-}
diff --git a/Grinder/ml/MachineLearningConfiguration.h b/Grinder/ml/MachineLearningConfiguration.h
index 89afff1..0bd8b89 100644
--- a/Grinder/ml/MachineLearningConfiguration.h
+++ b/Grinder/ml/MachineLearningConfiguration.h
@@ -17,11 +17,7 @@ namespace grndr
 		Q_OBJECT
 
 	public:
-		virtual void verifyConfiguration() const;
-
-	public:
-		const ImageTags* imageTags() const { return _imageTags; }
-		void setImageTags(const ImageTags* imageTags) { setValue(_imageTags, imageTags); }
+		virtual void verifyConfiguration() const { }
 
 	protected:
 		template<typename ValueType>
@@ -29,9 +25,6 @@ namespace grndr
 
 	signals:
 		void configurationChanged();
-
-	private:
-		const ImageTags* _imageTags{nullptr};
 	};
 }
 
diff --git a/Grinder/ml/barista/BaristaClassifierTaskSpawner.impl.h b/Grinder/ml/barista/BaristaClassifierTaskSpawner.impl.h
index fb5a6e1..2295da8 100644
--- a/Grinder/ml/barista/BaristaClassifierTaskSpawner.impl.h
+++ b/Grinder/ml/barista/BaristaClassifierTaskSpawner.impl.h
@@ -16,6 +16,4 @@ void BaristaClassifierTaskSpawner::configureTask(TaskType* task) const
 	task->setNetwork(_config.network());
 	task->setOutputDirectory(_config.getOutputDirectory());
 	task->setRemoteDirectory(_config.getRemoteDirectory());
-
-	task->setInputImageTags(_config.imageTags());
 }
diff --git a/Grinder/ml/barista/tasks/BaristaInferenceTask.cpp b/Grinder/ml/barista/tasks/BaristaInferenceTask.cpp
index 1174f39..fc1b45b 100644
--- a/Grinder/ml/barista/tasks/BaristaInferenceTask.cpp
+++ b/Grinder/ml/barista/tasks/BaristaInferenceTask.cpp
@@ -13,7 +13,6 @@
 #include "image/draftitems/PixelsDraftItem.h"
 #include "task/TaskExceptions.h"
 #include "project/ImageReference.h"
-#include "ui/barista/tasks/BaristaInferenceTaskWidget.h"
 
 #include "ml/barista/msgs/BaristaInferImageMessage.h"
 
@@ -38,11 +37,6 @@ void BaristaInferenceTask::registerMessageHandlers()
 	registerMessageHandler<BaristaInferImageMessage>(BaristaInferImageMessage::Key, &BaristaInferenceTask::handleInferImageMessage);
 }
 
-ConfigureTaskWidgetBase* BaristaInferenceTask::createEditor(bool newTask, QWidget* parent)
-{
-	return new BaristaInferenceTaskWidget{this, newTask, parent};
-}
-
 void BaristaInferenceTask::serialize(SerializationContext& ctx) const
 {
 	BaristaTask::serialize(ctx);
diff --git a/Grinder/ml/barista/tasks/BaristaInferenceTask.h b/Grinder/ml/barista/tasks/BaristaInferenceTask.h
index acab89c..f712651 100644
--- a/Grinder/ml/barista/tasks/BaristaInferenceTask.h
+++ b/Grinder/ml/barista/tasks/BaristaInferenceTask.h
@@ -49,9 +49,6 @@ namespace grndr
 	public:
 		virtual void registerMessageHandlers() override;
 
-	public:
-		virtual ConfigureTaskWidgetBase* createEditor(bool newTask, QWidget* parent) override;
-
 	public:
 		QString getNetworkStateFile() const { return _networkStateFile; }
 		void setNetworkStateFile(QString path) { _networkStateFile = path; }
diff --git a/Grinder/ml/barista/tasks/BaristaTask.h b/Grinder/ml/barista/tasks/BaristaTask.h
index 759e887..915a10c 100644
--- a/Grinder/ml/barista/tasks/BaristaTask.h
+++ b/Grinder/ml/barista/tasks/BaristaTask.h
@@ -35,6 +35,9 @@ namespace grndr
 	public:
 		virtual void registerMessageHandlers() override;
 
+	public:
+		virtual void processEngineStart(EngineExecutionContext& ctx, const data_type& data) override;
+
 	public:
 		unsigned int getBaristaPort() const { return _baristaPort; }
 		void setBaristaPort(unsigned int port) { _baristaPort = port; }
@@ -51,7 +54,6 @@ namespace grndr
 
 	public:
 		const ImageTags* inputImageTags() const { return _inputImageTags; }
-		void setInputImageTags(const ImageTags* imageTags) { _inputImageTags = imageTags; }
 
 	public:
 		virtual void serialize(SerializationContext& ctx) const override;
diff --git a/Grinder/ml/barista/tasks/BaristaTask.impl.h b/Grinder/ml/barista/tasks/BaristaTask.impl.h
index 7f4d1ff..7453d8b 100644
--- a/Grinder/ml/barista/tasks/BaristaTask.impl.h
+++ b/Grinder/ml/barista/tasks/BaristaTask.impl.h
@@ -42,6 +42,15 @@ void BaristaTask<ClassType>::registerMessageHandlers()
 
 }
 
+template<typename ClassType>
+void BaristaTask<ClassType>::processEngineStart(EngineExecutionContext& ctx, const PipelineTask::data_type& data)
+{
+	MachineLearningTask::processEngineStart(ctx, data);
+
+	// Store the image tags, as they will be used during context preparation
+	_inputImageTags = data.imageTags;
+}
+
 template<typename ClassType>
 void BaristaTask<ClassType>::setNetwork(BaristaNetwork* network)
 {
diff --git a/Grinder/ml/barista/tasks/BaristaTrainingTask.cpp b/Grinder/ml/barista/tasks/BaristaTrainingTask.cpp
index 24b9a1c..af0b95c 100644
--- a/Grinder/ml/barista/tasks/BaristaTrainingTask.cpp
+++ b/Grinder/ml/barista/tasks/BaristaTrainingTask.cpp
@@ -10,7 +10,6 @@
 #include "pipeline/Block.h"
 #include "engine/EngineExecutionContext.h"
 #include "image/properties/ImageTagsProperty.h"
-#include "ui/barista/tasks/BaristaTrainingTaskWidget.h"
 #include "res/Filenames.h"
 
 #include "ml/barista/msgs/BaristaStartTrainingMessage.h"
@@ -37,11 +36,6 @@ void BaristaTrainingTask::registerMessageHandlers()
 	registerMessageHandler<BaristaFinishTrainingMessage>(BaristaFinishTrainingMessage::Key, &BaristaTrainingTask::handleFinishTrainingMessage);
 }
 
-ConfigureTaskWidgetBase* BaristaTrainingTask::createEditor(bool newTask, QWidget* parent)
-{
-	return new BaristaTrainingTaskWidget{this, newTask, parent};
-}
-
 void BaristaTrainingTask::processEngineStart(EngineExecutionContext& ctx, const MachineLearningTaskData& data)
 {
 	BaristaTask::processEngineStart(ctx, data);
diff --git a/Grinder/ml/barista/tasks/BaristaTrainingTask.h b/Grinder/ml/barista/tasks/BaristaTrainingTask.h
index 3fda464..1de0109 100644
--- a/Grinder/ml/barista/tasks/BaristaTrainingTask.h
+++ b/Grinder/ml/barista/tasks/BaristaTrainingTask.h
@@ -28,9 +28,6 @@ namespace grndr
 	public:
 		virtual void registerMessageHandlers() override;
 
-	public:
-		virtual ConfigureTaskWidgetBase* createEditor(bool newTask, QWidget* parent) override;
-
 	public:
 		virtual void processEngineStart(EngineExecutionContext& ctx, const MachineLearningTaskData& data) override;
 		virtual void processEnginePass(EngineExecutionContext& ctx, const MachineLearningTaskData& data) override;
diff --git a/Grinder/ml/blocks/MachineLearningBlock.cpp b/Grinder/ml/blocks/MachineLearningBlock.cpp
index 1841dda..b94d333 100644
--- a/Grinder/ml/blocks/MachineLearningBlock.cpp
+++ b/Grinder/ml/blocks/MachineLearningBlock.cpp
@@ -14,6 +14,9 @@ void MachineLearningBlock::createPorts()
 	DataDescriptors statePortDataDescs = {DataDescriptor::customDescriptor("Model state", DataType::MachineLearningState)};
 	_statePort = createPort(PortType::State, Port::Direction::In, statePortDataDescs, "State");
 
+	DataDescriptors imageTagsPortDataDescs = {DataDescriptor::customDescriptor("Image tags", DataType::ImageTags)};
+	_imageTagsPort = createPort(PortType::ImageTagsIn, Port::Direction::In, imageTagsPortDataDescs, "Tags");
+
 	DataDescriptors inPortDataDescs = {DataDescriptor::imageDescriptor(true, DataDescriptor::ValueType::Any), DataDescriptor::imageDescriptor(false, DataDescriptor::ValueType::Any)};
 	_inPort = createPort(PortType::ImageIn, Port::Direction::In, inPortDataDescs, "In");
 }
diff --git a/Grinder/ml/blocks/MachineLearningBlock.h b/Grinder/ml/blocks/MachineLearningBlock.h
index e790f04..62df5c3 100644
--- a/Grinder/ml/blocks/MachineLearningBlock.h
+++ b/Grinder/ml/blocks/MachineLearningBlock.h
@@ -22,6 +22,8 @@ namespace grndr
 		const Port* methodPort() const { return _methodPort.get(); }
 		Port* statePort() { return _statePort.get(); }
 		const Port* statePort() const { return _statePort.get(); }
+		Port* imageTagsPort() { return _imageTagsPort.get(); }
+		const Port* imageTagsPort() const { return _imageTagsPort.get(); }
 		Port* inPort() { return _inPort.get(); }
 		const Port* inPort() const { return _inPort.get(); }
 
@@ -31,6 +33,7 @@ namespace grndr
 	private:
 		std::shared_ptr<Port> _methodPort;
 		std::shared_ptr<Port> _statePort;
+		std::shared_ptr<Port> _imageTagsPort;
 		std::shared_ptr<Port> _inPort;
 	};
 }
diff --git a/Grinder/ml/blocks/MachineLearningMethodBlock.h b/Grinder/ml/blocks/MachineLearningMethodBlock.h
index e826047..3399a17 100644
--- a/Grinder/ml/blocks/MachineLearningMethodBlock.h
+++ b/Grinder/ml/blocks/MachineLearningMethodBlock.h
@@ -38,8 +38,6 @@ namespace grndr
 		auto state() { return dynamic_cast<MachineLearningStateProperty*>(_state.get()); }
 		auto state() const { return dynamic_cast<const MachineLearningStateProperty*>(_state.get()); }
 
-		Port* imageTagsPort() { return _imageTagsPort.get(); }
-		const Port* imageTagsPort() const { return _imageTagsPort.get(); }
 		Port* methodPort() { return _methodPort.get(); }
 		const Port* methodPort() const { return _methodPort.get(); }
 		Port* statePort() { return _statePort.get(); }
@@ -51,21 +49,17 @@ namespace grndr
 		virtual void createPorts() override;
 
 	protected:
-		virtual void updateConfiguration();
+		virtual void updateConfiguration() { }
 
 	protected:
 		bool checkStateAvailability();
 
-	private:
-		void imageTagsPortConnectionChanged(const Connection* connection) { Q_UNUSED(connection); updateConfiguration(); }
-
 	protected:
 		method_type _method;
 
 	protected:
 		std::shared_ptr<PropertyBase> _state;
 
-		std::shared_ptr<Port> _imageTagsPort;
 		std::shared_ptr<Port> _methodPort;
 		std::shared_ptr<Port> _statePort;
 	};
diff --git a/Grinder/ml/blocks/MachineLearningMethodBlock.impl.h b/Grinder/ml/blocks/MachineLearningMethodBlock.impl.h
index e4393ea..5837ac8 100644
--- a/Grinder/ml/blocks/MachineLearningMethodBlock.impl.h
+++ b/Grinder/ml/blocks/MachineLearningMethodBlock.impl.h
@@ -18,10 +18,6 @@ void MachineLearningMethodBlock<MethodType>::initBlock()
 {
 	Block::initBlock();
 
-	// Listen to connection changes on the image tags port
-	connect(_imageTagsPort.get(), &Port::portConnected, this, &MachineLearningMethodBlock<MethodType>::imageTagsPortConnectionChanged);
-	connect(_imageTagsPort.get(), &Port::portDisconnected, this, &MachineLearningMethodBlock<MethodType>::imageTagsPortConnectionChanged);
-
 	updateConfiguration();
 }
 
@@ -56,10 +52,7 @@ bool MachineLearningMethodBlock<MethodType>::updateProperties(PropertyBase* upda
 
 template<typename MethodType>
 void MachineLearningMethodBlock<MethodType>::createPorts()
-{
-	DataDescriptors imageTagsPortDataDescs = {DataDescriptor::customDescriptor("Image tags", DataType::ImageTags)};
-	_imageTagsPort = createPort(PortType::ImageTagsIn, Port::Direction::In, imageTagsPortDataDescs, "Tags");
-
+{	
 	DataDescriptors methodPortDataDescs = {DataDescriptor::customDescriptor("Machine learning method", DataType::MachineLearningMethod)};
 	_methodPort = createPort(PortType::Method, Port::Direction::Out, methodPortDataDescs, "Method");
 
@@ -67,15 +60,6 @@ void MachineLearningMethodBlock<MethodType>::createPorts()
 	_statePort = createPort(PortType::State, Port::Direction::Out, statePortDataDescs, "State");
 }
 
-template<typename MethodType>
-void MachineLearningMethodBlock<MethodType>::updateConfiguration()
-{
-	if (auto imageTagsProperty = this->template portProperty<ImageTagsProperty>(PortType::ImageTagsIn, PropertyID::ImageTags))
-		_method.config().setImageTags(&imageTagsProperty->object());
-	else
-		_method.config().setImageTags(nullptr);
-}
-
 template<typename MethodType>
 bool MachineLearningMethodBlock<MethodType>::checkStateAvailability()
 {
diff --git a/Grinder/ml/processors/MachineLearningMethodProcessor.impl.h b/Grinder/ml/processors/MachineLearningMethodProcessor.impl.h
index 94a7df7..9c88e62 100644
--- a/Grinder/ml/processors/MachineLearningMethodProcessor.impl.h
+++ b/Grinder/ml/processors/MachineLearningMethodProcessor.impl.h
@@ -6,7 +6,6 @@
 #include "Grinder.h"
 #include "MachineLearningMethodProcessor.h"
 #include "ml/MachineLearningMethodBase.h"
-#include "image/properties/ImageTagsProperty.h"
 
 Q_DECLARE_METATYPE(const MachineLearningMethodBase*)
 
diff --git a/Grinder/ml/processors/MachineLearningProcessor.impl.h b/Grinder/ml/processors/MachineLearningProcessor.impl.h
index 2fa4f3e..bda120a 100644
--- a/Grinder/ml/processors/MachineLearningProcessor.impl.h
+++ b/Grinder/ml/processors/MachineLearningProcessor.impl.h
@@ -8,7 +8,7 @@
 #include "ml/MachineLearningMethodBase.h"
 #include "ml/MachineLearningTaskSpawnerBase.h"
 #include "ml/MachineLearningConfiguration.h"
-#include "image/ImageTags.h"
+#include "image/properties/ImageTagsProperty.h"
 
 template<typename BlockType>
 const char* MachineLearningProcessor<BlockType>::Data_Value_Method = "Method";
@@ -37,6 +37,11 @@ void MachineLearningProcessor<BlockType>::execute(EngineExecutionContext& ctx)
 		// Get the used machine learning method and model state
 		const MachineLearningMethodBase* method = this->template portData<const MachineLearningMethodBase*>(ctx, this->_block->methodPort(), Data_Value_Method);
 		QString state = this->template portData<QString>(ctx, this->_block->statePort(), Data_Value_State, false);
+		const ImageTags* imageTags = nullptr;
+
+		//if (auto imageTagsProperty = this->_block->template portProperty<ImageTagsProperty>(PortType::ImageTagsIn, PropertyID::ImageTags))
+		if (auto imageTagsProperty = this->template portProperty<ImageTagsProperty>(this->_block->imageTagsPort(), PropertyID::ImageTags))
+			imageTags = &imageTagsProperty->object();
 
 		if (!execute(ctx, method, state))
 		{
@@ -58,8 +63,9 @@ void MachineLearningProcessor<BlockType>::execute(EngineExecutionContext& ctx)
 				}
 
 				// Get the task data for machine learning
-				MachineLearningTaskData taskData;				
-				fillTaskData(ctx, method, taskData);
+				MachineLearningTaskData taskData;
+				taskData.imageTags = imageTags;
+				fillTaskData(ctx, method, taskData);				
 
 				if (ctx.isFirstImage())
 					_spawnedTask->processEngineStart(ctx, taskData);
@@ -100,7 +106,7 @@ void MachineLearningProcessor<BlockType>::spawnTask(const MachineLearningMethodB
 
 	if (auto spawner = method->createTaskSpawner())
 	{
-		QString taskName = QString{"%1 %2 (%3)"}.arg(method->getMethodName()).arg(getSpawnTypeName(_spawnType)).arg(this->_block->getFormattedName());
+		QString taskName = QString{"%1 %2"}.arg(method->getMethodName()).arg(getSpawnTypeName(_spawnType));
 
 		switch (_spawnType)
 		{
@@ -115,6 +121,8 @@ void MachineLearningProcessor<BlockType>::spawnTask(const MachineLearningMethodB
 
 		if (!_spawnedTask)
 			this->throwProcessorException("Unable to spawn the machine learning task");
+
+		_spawnedTask->setInfo(this->_block->getFormattedName());
 	}
 	else
 		this->throwProcessorException("Unable to create a task spawner");
diff --git a/Grinder/ml/tasks/MachineLearningTask.h b/Grinder/ml/tasks/MachineLearningTask.h
index c3ea9b6..7eb7a9c 100644
--- a/Grinder/ml/tasks/MachineLearningTask.h
+++ b/Grinder/ml/tasks/MachineLearningTask.h
@@ -16,6 +16,8 @@ namespace grndr
 
 	struct MachineLearningTaskData
 	{
+		const ImageTags* imageTags{nullptr};
+
 		cv::Mat imageData;
 		cv::Mat imageTagsData;
 	};
diff --git a/Grinder/network/cmd/tasks/CommandInterfaceTask.cpp b/Grinder/network/cmd/tasks/CommandInterfaceTask.cpp
index 3c339a8..9bcd248 100644
--- a/Grinder/network/cmd/tasks/CommandInterfaceTask.cpp
+++ b/Grinder/network/cmd/tasks/CommandInterfaceTask.cpp
@@ -26,7 +26,7 @@ const char* CommandInterfaceTask::Serialization_Value_Label = "Label";
 const char* CommandInterfaceTask::Serialization_Value_CanvasBlock = "CanvasBlock";
 const char* CommandInterfaceTask::Serialization_Value_UserData = "UserData";
 
-CommandInterfaceTask::CommandInterfaceTask(TaskPool* taskPool, QString name) : CommandInterfaceTaskBase(this, CommandInterface::CoreType::Server, taskPool, type_value, Capability::CanBeStopped|Capability::HasProgress|Capability::CanBeRefreshed, name)
+CommandInterfaceTask::CommandInterfaceTask(TaskPool* taskPool, QString name) : CommandInterfaceTaskBase(this, CommandInterface::CoreType::Server, taskPool, type_value, Capability::CanBeStopped|Capability::CanBeRefreshed|Capability::HasProgress|Capability::UserControllable|Capability::Persistent, name)
 {
 
 }
diff --git a/Grinder/network/cmd/tasks/CommandInterfaceTestTask.cpp b/Grinder/network/cmd/tasks/CommandInterfaceTestTask.cpp
index 5abd34e..00441c9 100644
--- a/Grinder/network/cmd/tasks/CommandInterfaceTestTask.cpp
+++ b/Grinder/network/cmd/tasks/CommandInterfaceTestTask.cpp
@@ -41,7 +41,7 @@ const char* CommandInterfaceTestTask::Serialization_Value_Address = "Address";
 const char* CommandInterfaceTestTask::Serialization_Value_RenderItems = "RenderItems";
 const char* CommandInterfaceTestTask::Serialization_Value_BackgroundColor = "BackgroundColor";
 
-CommandInterfaceTestTask::CommandInterfaceTestTask(TaskPool* taskPool, QString name) : CommandInterfaceTaskBase(this, CommandInterface::CoreType::Client, taskPool, type_value, Capability::CanBeStopped|Capability::HasProgress, name)
+CommandInterfaceTestTask::CommandInterfaceTestTask(TaskPool* taskPool, QString name) : CommandInterfaceTaskBase(this, CommandInterface::CoreType::Client, taskPool, type_value, Capability::CanBeStopped|Capability::HasProgress|Capability::UserControllable|Capability::Persistent, name)
 {
 
 }
diff --git a/Grinder/task/Task.cpp b/Grinder/task/Task.cpp
index 1fc32d8..2e05d32 100644
--- a/Grinder/task/Task.cpp
+++ b/Grinder/task/Task.cpp
@@ -8,6 +8,7 @@
 
 const char* Task::Serialization_Value_Type = "Type";
 const char* Task::Serialization_Value_Name = "Name";
+const char* Task::Serialization_Value_Info = "Info";
 
 Task::Task(TaskPool* taskPool, TaskType type, Capabilities caps, QString name) :
 	_taskPool{taskPool}, _type{type}, _capabilities{caps}, _name{name}
@@ -153,12 +154,14 @@ void Task::serialize(SerializationContext& ctx) const
 	// Serialize values
 	ctx.settings()(Serialization_Value_Type) = _type;
 	ctx.settings()(Serialization_Value_Name) = _name;
+	ctx.settings()(Serialization_Value_Info) = _info;
 }
 
 void Task::deserialize(DeserializationContext& ctx)
 {
 	// Deserialize values
 	_name = ctx.settings()(Serialization_Value_Name).toString();
+	_info = ctx.settings()(Serialization_Value_Info).toString();
 }
 
 void Task::reportStatus(QString status, bool preLine, bool postLine)
diff --git a/Grinder/task/Task.h b/Grinder/task/Task.h
index 102eba0..deeab34 100644
--- a/Grinder/task/Task.h
+++ b/Grinder/task/Task.h
@@ -22,6 +22,7 @@ namespace grndr
 	public:
 		static const char* Serialization_Value_Type;
 		static const char* Serialization_Value_Name;
+		static const char* Serialization_Value_Info;
 
 	public:
 		enum class Status
@@ -47,6 +48,9 @@ namespace grndr
 
 			HasProgress = 0x0008,
 
+			UserControllable = 0x0010,
+			Persistent = 0x0020,
+
 			All = 0xFFFF,
 		};
 
@@ -78,6 +82,8 @@ namespace grndr
 		Capabilities getCapabilities() const { return _capabilities; }
 		QString getName() const { return _name; }
 		void setName(QString name) { if (_name != name) { _name = name; emit taskUpdated(); } }
+		QString getInfo() const { return _info; }
+		void setInfo(QString info) { if (_info != info) { _info = info; emit taskUpdated(); } }
 
 		Status getStatus() const { return _status; }
 		void setStatus(Status status) { if (_status != status) { _status = status; emit taskUpdated(); } }
@@ -126,7 +132,8 @@ namespace grndr
 
 		TaskType _type{TaskType::Undefined};
 		Capabilities _capabilities{Capability::None};
-		QString _name;
+		QString _name{""};
+		QString _info{""};
 
 		Status _status{Status::Pending};
 		Result _result{Result::None};
diff --git a/Grinder/task/TaskCatalog.cpp b/Grinder/task/TaskCatalog.cpp
index 17529a7..f0a413d 100644
--- a/Grinder/task/TaskCatalog.cpp
+++ b/Grinder/task/TaskCatalog.cpp
@@ -13,29 +13,40 @@
 #include "ml/barista/tasks/BaristaTrainingTask.h"
 #include "ml/barista/tasks/BaristaInferenceTask.h"
 
-#define REGISTER_TASK_TYPE(cls)	registerTaskType(cls::type_value, [](TaskPool* taskPool, QString name) { return std::make_unique<cls>(taskPool, name); })
+#define REGISTER_TASK_TYPE(cls, userCreatable)	registerTaskType(cls::type_value, {[](TaskPool* taskPool, QString name) { return std::make_unique<cls>(taskPool, name); }, userCreatable})
 
-std::map<TaskType, TaskCatalog::task_creator_type> TaskCatalog::s_creators;
+std::map<TaskType, TaskCatalog::CatalogEntry> TaskCatalog::s_entries;
 
 std::set<TaskType> TaskCatalog::getTypes()
 {
 	std::set<TaskType> types;
 
-	for (const auto& creator : s_creators)
-		types.insert(creator.first);
+	for (const auto& entry : s_entries)
+		types.insert(entry.first);
 
 	return types;
 }
 
-void TaskCatalog::registerTaskType(TaskType type, task_creator_type creator)
+bool TaskCatalog::isUserCreatable(TaskType type)
+{
+	if (s_entries.find(type) != s_entries.end())
+	{
+		auto entry = s_entries.at(type);
+		return entry.isUserCreatable;
+	}
+	else
+		throw std::invalid_argument{_EXCPT("The given task type has not been registered")};
+}
+
+void TaskCatalog::registerTaskType(TaskType type, CatalogEntry entry)
 {
 	if (type == TaskType::Undefined)
 		throw std::invalid_argument{_EXCPT("type may not be TaskType::Undefined")};
 
-	if (!creator)
+	if (!entry.creator)
 		throw std::invalid_argument{_EXCPT("creator may not be null")};
 
-	s_creators[type] = creator;
+	s_entries[type] = entry;
 }
 
 std::unique_ptr<Task> TaskCatalog::createTask(TaskPool* taskPool, TaskType type, QString name)
@@ -43,10 +54,10 @@ std::unique_ptr<Task> TaskCatalog::createTask(TaskPool* taskPool, TaskType type,
 	if (type == TaskType::Undefined)
 		throw std::invalid_argument{_EXCPT("type may not be TaskType::Undefined")};
 
-	if (s_creators.find(type) != s_creators.end())
+	if (s_entries.find(type) != s_entries.end())
 	{
-		auto creator = s_creators.at(type);
-		auto task = creator(taskPool, name);
+		auto entry = s_entries.at(type);
+		auto task = entry.creator(taskPool, name);
 
 		if (!task)
 			throw std::runtime_error{_EXCPT(QString{"Failed to create a task of type %1"}.arg(type))};
@@ -59,9 +70,10 @@ std::unique_ptr<Task> TaskCatalog::createTask(TaskPool* taskPool, TaskType type,
 
 void TaskCatalog::registerStandardTasks()
 {
-	REGISTER_TASK_TYPE(GenericTask);
-	REGISTER_TASK_TYPE(CommandInterfaceTask);
-	REGISTER_TASK_TYPE(CommandInterfaceTestTask);
-	REGISTER_TASK_TYPE(BaristaTrainingTask);
-	REGISTER_TASK_TYPE(BaristaInferenceTask);
+	REGISTER_TASK_TYPE(GenericTask, true);
+	REGISTER_TASK_TYPE(CommandInterfaceTask, true);
+	REGISTER_TASK_TYPE(CommandInterfaceTestTask, true);
+
+	REGISTER_TASK_TYPE(BaristaTrainingTask, false);
+	REGISTER_TASK_TYPE(BaristaInferenceTask, false);
 }
diff --git a/Grinder/task/TaskCatalog.h b/Grinder/task/TaskCatalog.h
index 3e5de14..957e0de 100644
--- a/Grinder/task/TaskCatalog.h
+++ b/Grinder/task/TaskCatalog.h
@@ -24,6 +24,12 @@ namespace grndr
 	private:
 		using task_creator_type = std::function<std::unique_ptr<Task>(TaskPool*, QString)>;
 
+		struct CatalogEntry
+		{
+			task_creator_type creator{nullptr};
+			bool isUserCreatable{true};
+		};
+
 	private:
 		TaskCatalog() { }
 
@@ -34,12 +40,13 @@ namespace grndr
 
 	public:
 		static std::set<TaskType> getTypes();
+		static bool isUserCreatable(TaskType type);
 
 	private:
-		static void registerTaskType(TaskType type, task_creator_type creator);
+		static void registerTaskType(TaskType type, CatalogEntry entry);
 
 	private:
-		static std::map<TaskType, task_creator_type> s_creators;
+		static std::map<TaskType, CatalogEntry> s_entries;
 	};
 }
 
diff --git a/Grinder/task/TaskPool.cpp b/Grinder/task/TaskPool.cpp
index 8e789e1..43336f1 100644
--- a/Grinder/task/TaskPool.cpp
+++ b/Grinder/task/TaskPool.cpp
@@ -81,7 +81,7 @@ void TaskPool::serialize(SerializationContext& ctx) const
 {
 	// Serialize all tasks
 	ctx.beginGroup(TaskVector::Serialization_Group, true);
-	_tasks.serialize(TaskVector::Serialization_Element, ctx);
+	_tasks.serialize(TaskVector::Serialization_Element, ctx, [](auto task) { return task->getCapabilities().testFlag(Task::Capability::Persistent); });
 	ctx.endGroup();
 }
 
diff --git a/Grinder/task/tasks/GenericTask.cpp b/Grinder/task/tasks/GenericTask.cpp
index 8f57977..aa4f97d 100644
--- a/Grinder/task/tasks/GenericTask.cpp
+++ b/Grinder/task/tasks/GenericTask.cpp
@@ -13,7 +13,7 @@ const TaskType GenericTask::type_value = TaskType::Generic;
 const char* GenericTask::Serialization_Value_Command = "Command";
 const char* GenericTask::Serialization_Value_Arguments = "Arguments";
 
-GenericTask::GenericTask(TaskPool* taskPool, QString name) : Task(taskPool, type_value, Task::Capability::CanBeStopped, name)
+GenericTask::GenericTask(TaskPool* taskPool, QString name) : Task(taskPool, type_value, Capability::CanBeStopped|Capability::UserControllable|Capability::Persistent, name)
 {
 
 }
diff --git a/Grinder/ui/barista/tasks/BaristaInferenceTaskWidget.cpp b/Grinder/ui/barista/tasks/BaristaInferenceTaskWidget.cpp
deleted file mode 100644
index f7f9a4e..0000000
--- a/Grinder/ui/barista/tasks/BaristaInferenceTaskWidget.cpp
+++ /dev/null
@@ -1,131 +0,0 @@
-/******************************************************************************
- * File: BaristaInferenceTaskWidget.cpp
- * Date: 07.12.2018
- *****************************************************************************/
-
-#include "Grinder.h"
-#include "BaristaInferenceTaskWidget.h"
-#include "ui_BaristaInferenceTaskWidget.h"
-#include "core/GrinderApplication.h"
-
-BaristaInferenceTaskWidget::BaristaInferenceTaskWidget(BaristaInferenceTask* task, bool newTask, QWidget* parent) : ConfigureTaskWidget(task, newTask, parent),
-	ui{new Ui::BaristaInferenceTaskWidget}
-{
-	setupUi();	
-}
-
-BaristaInferenceTaskWidget::~BaristaInferenceTaskWidget()
-{
-	delete ui;
-}
-
-void BaristaInferenceTaskWidget::verifySettings()
-{
-	if (ui->txtLibraryPath->text().isEmpty())
-		showError("Please enter a library path.", ui->txtLibraryPath);
-
-	if (!ui->lstNetworks->getSelectedNetwork())
-		showError("Please select a network.", ui->lstNetworks);
-
-	if (ui->txtOutputDir->text().isEmpty())
-		showError("Please enter an output directory.", ui->txtOutputDir);
-
-	if (ui->lstNetworkState->currentText().isEmpty())
-		showError("Please select a network state.", ui->lstNetworkState);
-
-	if (!ui->chkGenerateItems->isChecked() && !ui->chkGenerateMaps->isChecked())
-		showError("Please select at least one result type.", ui->chkGenerateItems);
-}
-
-void BaristaInferenceTaskWidget::applySettings(bool save)
-{
-	if (save)
-	{
-		_task->setBaristaPort(ui->txtWorkerPort->value());
-		_task->setLibraryPath(ui->txtLibraryPath->text());
-
-		_task->setNetwork(ui->lstNetworks->getSelectedNetwork());
-		_task->setOutputDirectory(ui->txtOutputDir->text());
-		_task->setRemoteDirectory(ui->txtRemoteDir->text());
-		_task->setNetworkStateFile(ui->lstNetworkState->currentText());
-
-		_task->setProbabilityResultTypes(getProbabilityResultTypes());
-		_task->setProbabilityThreshold(ui->txtThreshold->value());
-	}
-	else
-	{
-		ui->txtWorkerPort->setValue(_task->getBaristaPort());
-		ui->txtLibraryPath->setText(_task->getLibraryPath());
-
-		ui->lstNetworks->setSelectedNetwork(_task->getNetwork());
-		ui->txtOutputDir->setText(_task->getOutputDirectory());
-		ui->txtRemoteDir->setText(_task->getRemoteDirectory());		
-
-		setProbabilityResultTypes(_task->getProbabilityResultTypes());
-		ui->txtThreshold->setValue(_task->getProbabilityThreshold());
-
-		fillNetworkStateFiles();
-	}
-}
-
-void BaristaInferenceTaskWidget::on_txtOutputDir_editingFinished()
-{
-	fillNetworkStateFiles();
-}
-
-void BaristaInferenceTaskWidget::setupUi()
-{
-	ui->setupUi(this);
-
-	ui->lstNetworks->populate();
-}
-
-void BaristaInferenceTaskWidget::fillNetworkStateFiles()
-{
-	ui->lstNetworkState->clear();
-
-	QDir dir{ui->txtOutputDir->text()};
-	auto modelFiles = dir.entryList({"*.caffemodel"}, QDir::Files);
-
-	// Sort the model filenames naturally
-	QCollator collator;
-	collator.setNumericMode(true);
-	std::sort(modelFiles.begin(), modelFiles.end(), collator);
-
-	int curIndex = 0;
-	int selIndex = -1;
-
-	for (auto modelFile : modelFiles)
-	{
-		ui->lstNetworkState->addItem(modelFile);
-
-		if (modelFile.compare(_task->getNetworkStateFile(), Qt::CaseInsensitive) == 0)
-			selIndex = curIndex;
-
-		curIndex += 1;
-	}
-
-	if (selIndex != -1)
-		ui->lstNetworkState->setCurrentIndex(selIndex);
-	else
-		ui->lstNetworkState->setCurrentIndex(ui->lstNetworkState->count() - 1);
-}
-
-BaristaInferenceTask::ProbabilityResultTypes BaristaInferenceTaskWidget::getProbabilityResultTypes() const
-{
-	BaristaInferenceTask::ProbabilityResultTypes types = BaristaInferenceTask::ProbabilityResultType::None;
-
-	if (ui->chkGenerateItems->isChecked())
-		types |= BaristaInferenceTask::ProbabilityResultType::Items;
-
-	if (ui->chkGenerateMaps->isChecked())
-		types |= BaristaInferenceTask::ProbabilityResultType::Maps;
-
-	return types;
-}
-
-void BaristaInferenceTaskWidget::setProbabilityResultTypes(BaristaInferenceTask::ProbabilityResultTypes types)
-{
-	ui->chkGenerateItems->setChecked(types.testFlag(BaristaInferenceTask::ProbabilityResultType::Items));
-	ui->chkGenerateMaps->setChecked(types.testFlag(BaristaInferenceTask::ProbabilityResultType::Maps));
-}
diff --git a/Grinder/ui/barista/tasks/BaristaInferenceTaskWidget.h b/Grinder/ui/barista/tasks/BaristaInferenceTaskWidget.h
deleted file mode 100644
index 3ec6668..0000000
--- a/Grinder/ui/barista/tasks/BaristaInferenceTaskWidget.h
+++ /dev/null
@@ -1,52 +0,0 @@
-/******************************************************************************
- * File: BaristaInferenceTaskWidget.h
- * Date: 07.12.2018
- *****************************************************************************/
-
-#ifndef BARISTAINFERENCETASKWIDGET_H
-#define BARISTAINFERENCETASKWIDGET_H
-
-#include "ui/task/ConfigureTaskWidget.h"
-#include "ml/barista/tasks/BaristaInferenceTask.h"
-
-namespace Ui
-{
-	class BaristaInferenceTaskWidget;
-}
-
-namespace grndr
-{
-	class Label;
-	class LabelVector;
-	class ImageReference;
-	class ImageReferenceVector;
-	class Block;
-
-	class BaristaInferenceTaskWidget : public ConfigureTaskWidget<BaristaInferenceTask>
-	{
-		Q_OBJECT
-
-	public:
-		explicit BaristaInferenceTaskWidget(BaristaInferenceTask* task, bool newTask, QWidget *parent = nullptr);
-		virtual ~BaristaInferenceTaskWidget();
-
-	public:
-		virtual void verifySettings() override;
-		virtual void applySettings(bool save) override;
-
-	private slots:
-		void on_txtOutputDir_editingFinished();
-
-	private:
-		Ui::BaristaInferenceTaskWidget *ui;
-		void setupUi();
-
-	private:
-		void fillNetworkStateFiles();
-
-		BaristaInferenceTask::ProbabilityResultTypes getProbabilityResultTypes() const;
-		void setProbabilityResultTypes(BaristaInferenceTask::ProbabilityResultTypes types);
-	};
-}
-
-#endif
diff --git a/Grinder/ui/barista/tasks/BaristaInferenceTaskWidget.ui b/Grinder/ui/barista/tasks/BaristaInferenceTaskWidget.ui
deleted file mode 100644
index 9c059b0..0000000
--- a/Grinder/ui/barista/tasks/BaristaInferenceTaskWidget.ui
+++ /dev/null
@@ -1,298 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>BaristaInferenceTaskWidget</class>
- <widget class="QWidget" name="BaristaInferenceTaskWidget">
-  <property name="geometry">
-   <rect>
-    <x>0</x>
-    <y>0</y>
-    <width>404</width>
-    <height>367</height>
-   </rect>
-  </property>
-  <property name="windowTitle">
-   <string>Form</string>
-  </property>
-  <layout class="QGridLayout" name="gridLayout_6">
-   <item row="0" column="0">
-    <widget class="QGroupBox" name="groupBox">
-     <property name="title">
-      <string>Barista settings</string>
-     </property>
-     <layout class="QGridLayout" name="gridLayout_2">
-      <item row="0" column="1">
-       <widget class="QSpinBox" name="txtWorkerPort">
-        <property name="minimum">
-         <number>1024</number>
-        </property>
-        <property name="maximum">
-         <number>65536</number>
-        </property>
-       </widget>
-      </item>
-      <item row="1" column="0">
-       <widget class="QLabel" name="label_2">
-        <property name="text">
-         <string>Library &amp;path:</string>
-        </property>
-        <property name="buddy">
-         <cstring>txtLibraryPath</cstring>
-        </property>
-       </widget>
-      </item>
-      <item row="1" column="1" colspan="2">
-       <widget class="QLineEdit" name="txtLibraryPath"/>
-      </item>
-      <item row="0" column="0">
-       <widget class="QLabel" name="label">
-        <property name="text">
-         <string>&amp;Worker port:</string>
-        </property>
-        <property name="buddy">
-         <cstring>txtWorkerPort</cstring>
-        </property>
-       </widget>
-      </item>
-      <item row="0" column="2">
-       <spacer name="horizontalSpacer">
-        <property name="orientation">
-         <enum>Qt::Horizontal</enum>
-        </property>
-        <property name="sizeHint" stdset="0">
-         <size>
-          <width>185</width>
-          <height>20</height>
-         </size>
-        </property>
-       </spacer>
-      </item>
-     </layout>
-    </widget>
-   </item>
-   <item row="1" column="0">
-    <widget class="QGroupBox" name="groupBox_3">
-     <property name="title">
-      <string>Network settings</string>
-     </property>
-     <layout class="QGridLayout" name="gridLayout_3">
-      <item row="2" column="1">
-       <widget class="QLineEdit" name="txtRemoteDir"/>
-      </item>
-      <item row="3" column="1">
-       <widget class="QLabel" name="label_10">
-        <property name="enabled">
-         <bool>false</bool>
-        </property>
-        <property name="text">
-         <string>Leave empty to use the network directory</string>
-        </property>
-       </widget>
-      </item>
-      <item row="1" column="1">
-       <widget class="QLineEdit" name="txtOutputDir"/>
-      </item>
-      <item row="0" column="1">
-       <widget class="BaristaNetworksComboBox" name="lstNetworks"/>
-      </item>
-      <item row="2" column="0">
-       <widget class="QLabel" name="label_9">
-        <property name="text">
-         <string>&amp;Remote directory:</string>
-        </property>
-        <property name="buddy">
-         <cstring>txtRemoteDir</cstring>
-        </property>
-       </widget>
-      </item>
-      <item row="0" column="0">
-       <widget class="QLabel" name="label_7">
-        <property name="text">
-         <string>&amp;Network:</string>
-        </property>
-        <property name="buddy">
-         <cstring>lstNetworks</cstring>
-        </property>
-       </widget>
-      </item>
-      <item row="1" column="0">
-       <widget class="QLabel" name="label_8">
-        <property name="text">
-         <string>&amp;Output directory:</string>
-        </property>
-        <property name="buddy">
-         <cstring>txtOutputDir</cstring>
-        </property>
-       </widget>
-      </item>
-      <item row="4" column="0">
-       <widget class="QLabel" name="label_3">
-        <property name="text">
-         <string>Network &amp;state:</string>
-        </property>
-        <property name="buddy">
-         <cstring>lstNetworkState</cstring>
-        </property>
-       </widget>
-      </item>
-      <item row="4" column="1">
-       <widget class="QComboBox" name="lstNetworkState"/>
-      </item>
-     </layout>
-    </widget>
-   </item>
-   <item row="2" column="0">
-    <widget class="QGroupBox" name="groupBox_4">
-     <property name="title">
-      <string>Result settings</string>
-     </property>
-     <layout class="QGridLayout" name="gridLayout_5">
-      <item row="1" column="0">
-       <widget class="QFrame" name="frame">
-        <property name="frameShape">
-         <enum>QFrame::StyledPanel</enum>
-        </property>
-        <property name="frameShadow">
-         <enum>QFrame::Raised</enum>
-        </property>
-        <layout class="QGridLayout" name="gridLayout_4">
-         <property name="leftMargin">
-          <number>18</number>
-         </property>
-         <property name="topMargin">
-          <number>0</number>
-         </property>
-         <property name="bottomMargin">
-          <number>0</number>
-         </property>
-         <item row="0" column="0">
-          <widget class="QLabel" name="label_4">
-           <property name="text">
-            <string>&amp;Threshold:</string>
-           </property>
-           <property name="buddy">
-            <cstring>sldThreshold</cstring>
-           </property>
-          </widget>
-         </item>
-         <item row="0" column="1">
-          <widget class="QSlider" name="sldThreshold">
-           <property name="maximum">
-            <number>100</number>
-           </property>
-           <property name="orientation">
-            <enum>Qt::Horizontal</enum>
-           </property>
-           <property name="tickPosition">
-            <enum>QSlider::TicksBelow</enum>
-           </property>
-           <property name="tickInterval">
-            <number>10</number>
-           </property>
-          </widget>
-         </item>
-         <item row="0" column="2">
-          <widget class="QSpinBox" name="txtThreshold">
-           <property name="suffix">
-            <string>%</string>
-           </property>
-           <property name="maximum">
-            <number>100</number>
-           </property>
-           <property name="value">
-            <number>0</number>
-           </property>
-          </widget>
-         </item>
-        </layout>
-       </widget>
-      </item>
-      <item row="0" column="0">
-       <widget class="QCheckBox" name="chkGenerateItems">
-        <property name="text">
-         <string>Generate &amp;items</string>
-        </property>
-       </widget>
-      </item>
-      <item row="2" column="0">
-       <widget class="QCheckBox" name="chkGenerateMaps">
-        <property name="text">
-         <string>Generate probability &amp;maps</string>
-        </property>
-       </widget>
-      </item>
-     </layout>
-    </widget>
-   </item>
-  </layout>
- </widget>
- <customwidgets>
-  <customwidget>
-   <class>BaristaNetworksComboBox</class>
-   <extends>QComboBox</extends>
-   <header>ui/barista/widgets/BaristaNetworksComboBox.h</header>
-  </customwidget>
- </customwidgets>
- <tabstops>
-  <tabstop>txtWorkerPort</tabstop>
-  <tabstop>txtLibraryPath</tabstop>
-  <tabstop>lstNetworks</tabstop>
-  <tabstop>txtOutputDir</tabstop>
-  <tabstop>txtRemoteDir</tabstop>
-  <tabstop>lstNetworkState</tabstop>
-  <tabstop>chkGenerateItems</tabstop>
-  <tabstop>sldThreshold</tabstop>
-  <tabstop>txtThreshold</tabstop>
-  <tabstop>chkGenerateMaps</tabstop>
- </tabstops>
- <resources/>
- <connections>
-  <connection>
-   <sender>txtThreshold</sender>
-   <signal>valueChanged(int)</signal>
-   <receiver>sldThreshold</receiver>
-   <slot>setValue(int)</slot>
-   <hints>
-    <hint type="sourcelabel">
-     <x>335</x>
-     <y>602</y>
-    </hint>
-    <hint type="destinationlabel">
-     <x>201</x>
-     <y>606</y>
-    </hint>
-   </hints>
-  </connection>
-  <connection>
-   <sender>chkGenerateItems</sender>
-   <signal>toggled(bool)</signal>
-   <receiver>frame</receiver>
-   <slot>setEnabled(bool)</slot>
-   <hints>
-    <hint type="sourcelabel">
-     <x>48</x>
-     <y>579</y>
-    </hint>
-    <hint type="destinationlabel">
-     <x>29</x>
-     <y>601</y>
-    </hint>
-   </hints>
-  </connection>
-  <connection>
-   <sender>sldThreshold</sender>
-   <signal>valueChanged(int)</signal>
-   <receiver>txtThreshold</receiver>
-   <slot>setValue(int)</slot>
-   <hints>
-    <hint type="sourcelabel">
-     <x>177</x>
-     <y>608</y>
-    </hint>
-    <hint type="destinationlabel">
-     <x>343</x>
-     <y>615</y>
-    </hint>
-   </hints>
-  </connection>
- </connections>
-</ui>
diff --git a/Grinder/ui/barista/tasks/BaristaTrainingTaskWidget.cpp b/Grinder/ui/barista/tasks/BaristaTrainingTaskWidget.cpp
deleted file mode 100644
index e036d08..0000000
--- a/Grinder/ui/barista/tasks/BaristaTrainingTaskWidget.cpp
+++ /dev/null
@@ -1,71 +0,0 @@
-/******************************************************************************
- * File: BaristaTrainingTaskWidget.cpp
- * Date: 20.11.2018
- *****************************************************************************/
-
-#include "Grinder.h"
-#include "BaristaTrainingTaskWidget.h"
-#include "ui_BaristaTrainingTaskWidget.h"
-#include "core/GrinderApplication.h"
-#include "image/properties/ImageTagsProperty.h"
-#include "ml/barista/tasks/BaristaTrainingTask.h"
-
-BaristaTrainingTaskWidget::BaristaTrainingTaskWidget(BaristaTrainingTask* task, bool newTask, QWidget *parent) : ConfigureTaskWidget(task, newTask, parent),
-	ui{new Ui::BaristaTrainingTaskWidget}
-{
-	setupUi();	
-}
-
-BaristaTrainingTaskWidget::~BaristaTrainingTaskWidget()
-{
-	delete ui;
-}
-
-void BaristaTrainingTaskWidget::verifySettings()
-{
-	if (ui->txtLibraryPath->text().isEmpty())
-		showError("Please enter a library path.", ui->txtLibraryPath);
-
-	if (!ui->lstNetworks->getSelectedNetwork())
-		showError("Please select a network.", ui->lstNetworks);
-
-	if (ui->txtOutputDir->text().isEmpty())
-		showError("Please enter an output directory.", ui->txtOutputDir);
-}
-
-void BaristaTrainingTaskWidget::applySettings(bool save)
-{
-	if (save)
-	{
-		_task->setBaristaPort(ui->txtWorkerPort->value());
-		_task->setLibraryPath(ui->txtLibraryPath->text());
-
-		_task->setNetwork(ui->lstNetworks->getSelectedNetwork());
-		_task->setOutputDirectory(ui->txtOutputDir->text());
-		_task->setRemoteDirectory(ui->txtRemoteDir->text());
-
-		_task->setMaxIterations(ui->txtMaxIterations->value());
-		_task->setDisplayInterval(ui->txtDisplayInterval->value());
-		_task->setSnapshotInterval(ui->txtSnapshotInterval->value());
-	}
-	else
-	{
-		ui->txtWorkerPort->setValue(_task->getBaristaPort());
-		ui->txtLibraryPath->setText(_task->getLibraryPath());		
-
-		ui->lstNetworks->setSelectedNetwork(_task->getNetwork());
-		ui->txtOutputDir->setText(_task->getOutputDirectory());
-		ui->txtRemoteDir->setText(_task->getRemoteDirectory());
-
-		ui->txtMaxIterations->setValue(_task->getMaxIterations());
-		ui->txtDisplayInterval->setValue(_task->getDisplayInterval());
-		ui->txtSnapshotInterval->setValue(_task->getSnapshotInterval());
-	}
-}
-
-void BaristaTrainingTaskWidget::setupUi()
-{
-	ui->setupUi(this);
-
-	ui->lstNetworks->populate();
-}
diff --git a/Grinder/ui/barista/tasks/BaristaTrainingTaskWidget.h b/Grinder/ui/barista/tasks/BaristaTrainingTaskWidget.h
deleted file mode 100644
index 818b80e..0000000
--- a/Grinder/ui/barista/tasks/BaristaTrainingTaskWidget.h
+++ /dev/null
@@ -1,43 +0,0 @@
-/******************************************************************************
- * File: BaristaTrainingTaskWidget.h
- * Date: 20.11.2018
- *****************************************************************************/
-
-#ifndef BARISTATRAININGTASKWIDGET_H
-#define BARISTATRAININGTASKWIDGET_H
-
-#include "ui/task/ConfigureTaskWidget.h"
-
-namespace Ui
-{
-	class BaristaTrainingTaskWidget;
-}
-
-namespace grndr
-{
-	class BaristaTrainingTask;
-	class LabelVector;
-	class Label;
-	class ImageReferenceVector;
-	class ImageReference;
-	class Block;
-
-	class BaristaTrainingTaskWidget : public ConfigureTaskWidget<BaristaTrainingTask>
-	{
-		Q_OBJECT
-
-	public:
-		BaristaTrainingTaskWidget(BaristaTrainingTask* task, bool newTask, QWidget* parent = nullptr);
-		virtual ~BaristaTrainingTaskWidget();
-
-	public:
-		virtual void verifySettings() override;
-		virtual void applySettings(bool save) override;
-
-	private:
-		Ui::BaristaTrainingTaskWidget *ui;
-		void setupUi();
-	};
-}
-
-#endif
diff --git a/Grinder/ui/barista/tasks/BaristaTrainingTaskWidget.ui b/Grinder/ui/barista/tasks/BaristaTrainingTaskWidget.ui
deleted file mode 100644
index 9845ce8..0000000
--- a/Grinder/ui/barista/tasks/BaristaTrainingTaskWidget.ui
+++ /dev/null
@@ -1,263 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>BaristaTrainingTaskWidget</class>
- <widget class="QWidget" name="BaristaTrainingTaskWidget">
-  <property name="geometry">
-   <rect>
-    <x>0</x>
-    <y>0</y>
-    <width>386</width>
-    <height>320</height>
-   </rect>
-  </property>
-  <property name="windowTitle">
-   <string>Form</string>
-  </property>
-  <layout class="QGridLayout" name="gridLayout_4">
-   <property name="leftMargin">
-    <number>0</number>
-   </property>
-   <property name="topMargin">
-    <number>0</number>
-   </property>
-   <property name="rightMargin">
-    <number>0</number>
-   </property>
-   <property name="bottomMargin">
-    <number>0</number>
-   </property>
-   <item row="0" column="0">
-    <widget class="QGroupBox" name="groupBox">
-     <property name="title">
-      <string>Barista settings</string>
-     </property>
-     <layout class="QGridLayout" name="gridLayout_2">
-      <item row="0" column="0">
-       <widget class="QLabel" name="label">
-        <property name="text">
-         <string>&amp;Worker port:</string>
-        </property>
-        <property name="buddy">
-         <cstring>txtWorkerPort</cstring>
-        </property>
-       </widget>
-      </item>
-      <item row="1" column="0">
-       <widget class="QLabel" name="label_2">
-        <property name="text">
-         <string>Library &amp;path:</string>
-        </property>
-        <property name="buddy">
-         <cstring>txtLibraryPath</cstring>
-        </property>
-       </widget>
-      </item>
-      <item row="0" column="1">
-       <widget class="QSpinBox" name="txtWorkerPort">
-        <property name="minimum">
-         <number>1024</number>
-        </property>
-        <property name="maximum">
-         <number>65536</number>
-        </property>
-       </widget>
-      </item>
-      <item row="0" column="2">
-       <spacer name="horizontalSpacer">
-        <property name="orientation">
-         <enum>Qt::Horizontal</enum>
-        </property>
-        <property name="sizeHint" stdset="0">
-         <size>
-          <width>205</width>
-          <height>20</height>
-         </size>
-        </property>
-       </spacer>
-      </item>
-      <item row="1" column="1" colspan="2">
-       <widget class="QLineEdit" name="txtLibraryPath"/>
-      </item>
-     </layout>
-    </widget>
-   </item>
-   <item row="1" column="0">
-    <widget class="QGroupBox" name="groupBox_2">
-     <property name="title">
-      <string>Network settings</string>
-     </property>
-     <layout class="QGridLayout" name="gridLayout">
-      <item row="2" column="0">
-       <widget class="QLabel" name="label_9">
-        <property name="text">
-         <string>&amp;Remote directory:</string>
-        </property>
-        <property name="buddy">
-         <cstring>txtRemoteDir</cstring>
-        </property>
-       </widget>
-      </item>
-      <item row="2" column="1">
-       <widget class="QLineEdit" name="txtRemoteDir"/>
-      </item>
-      <item row="0" column="1">
-       <widget class="BaristaNetworksComboBox" name="lstNetworks"/>
-      </item>
-      <item row="3" column="1">
-       <widget class="QLabel" name="label_10">
-        <property name="enabled">
-         <bool>false</bool>
-        </property>
-        <property name="text">
-         <string>Leave empty to use the network directory</string>
-        </property>
-       </widget>
-      </item>
-      <item row="1" column="0">
-       <widget class="QLabel" name="label_8">
-        <property name="text">
-         <string>&amp;Output directory:</string>
-        </property>
-        <property name="buddy">
-         <cstring>txtOutputDir</cstring>
-        </property>
-       </widget>
-      </item>
-      <item row="1" column="1">
-       <widget class="QLineEdit" name="txtOutputDir"/>
-      </item>
-      <item row="0" column="0">
-       <widget class="QLabel" name="label_7">
-        <property name="text">
-         <string>&amp;Network:</string>
-        </property>
-        <property name="buddy">
-         <cstring>lstNetworks</cstring>
-        </property>
-       </widget>
-      </item>
-     </layout>
-    </widget>
-   </item>
-   <item row="2" column="0">
-    <widget class="QGroupBox" name="groupBox_4">
-     <property name="title">
-      <string>Training settings</string>
-     </property>
-     <layout class="QGridLayout" name="gridLayout_5">
-      <item row="0" column="0">
-       <widget class="QLabel" name="label_11">
-        <property name="text">
-         <string>Max. &amp;iterations:</string>
-        </property>
-        <property name="buddy">
-         <cstring>txtMaxIterations</cstring>
-        </property>
-       </widget>
-      </item>
-      <item row="0" column="1">
-       <widget class="QSpinBox" name="txtMaxIterations">
-        <property name="minimum">
-         <number>1</number>
-        </property>
-        <property name="maximum">
-         <number>1000000000</number>
-        </property>
-        <property name="singleStep">
-         <number>100</number>
-        </property>
-        <property name="value">
-         <number>5000</number>
-        </property>
-       </widget>
-      </item>
-      <item row="1" column="0">
-       <widget class="QLabel" name="label_3">
-        <property name="text">
-         <string>&amp;Display interval:</string>
-        </property>
-        <property name="buddy">
-         <cstring>txtDisplayInterval</cstring>
-        </property>
-       </widget>
-      </item>
-      <item row="0" column="2">
-       <spacer name="horizontalSpacer_2">
-        <property name="orientation">
-         <enum>Qt::Horizontal</enum>
-        </property>
-        <property name="sizeHint" stdset="0">
-         <size>
-          <width>40</width>
-          <height>20</height>
-         </size>
-        </property>
-       </spacer>
-      </item>
-      <item row="2" column="0">
-       <widget class="QLabel" name="label_4">
-        <property name="text">
-         <string>&amp;Snapshot interval:</string>
-        </property>
-        <property name="buddy">
-         <cstring>txtSnapshotInterval</cstring>
-        </property>
-       </widget>
-      </item>
-      <item row="1" column="1">
-       <widget class="QSpinBox" name="txtDisplayInterval">
-        <property name="minimum">
-         <number>1</number>
-        </property>
-        <property name="maximum">
-         <number>1000000000</number>
-        </property>
-        <property name="singleStep">
-         <number>10</number>
-        </property>
-        <property name="value">
-         <number>100</number>
-        </property>
-       </widget>
-      </item>
-      <item row="2" column="1">
-       <widget class="QSpinBox" name="txtSnapshotInterval">
-        <property name="minimum">
-         <number>1</number>
-        </property>
-        <property name="maximum">
-         <number>1000000000</number>
-        </property>
-        <property name="singleStep">
-         <number>100</number>
-        </property>
-        <property name="value">
-         <number>1000</number>
-        </property>
-       </widget>
-      </item>
-     </layout>
-    </widget>
-   </item>
-  </layout>
- </widget>
- <customwidgets>
-  <customwidget>
-   <class>BaristaNetworksComboBox</class>
-   <extends>QComboBox</extends>
-   <header>ui/barista/widgets/BaristaNetworksComboBox.h</header>
-  </customwidget>
- </customwidgets>
- <tabstops>
-  <tabstop>txtWorkerPort</tabstop>
-  <tabstop>txtLibraryPath</tabstop>
-  <tabstop>lstNetworks</tabstop>
-  <tabstop>txtOutputDir</tabstop>
-  <tabstop>txtRemoteDir</tabstop>
-  <tabstop>txtMaxIterations</tabstop>
-  <tabstop>txtDisplayInterval</tabstop>
-  <tabstop>txtSnapshotInterval</tabstop>
- </tabstops>
- <resources/>
- <connections/>
-</ui>
diff --git a/Grinder/ui/task/TaskPoolWidget.cpp b/Grinder/ui/task/TaskPoolWidget.cpp
index 0d4f9f5..0ba85ef 100644
--- a/Grinder/ui/task/TaskPoolWidget.cpp
+++ b/Grinder/ui/task/TaskPoolWidget.cpp
@@ -1,114 +1,117 @@
-/******************************************************************************
- * File: TaskPoolWidget.cpp
- * Date: 01.11.2018
- *****************************************************************************/
-
-#include "Grinder.h"
-#include "TaskPoolWidget.h"
-#include "TaskWidget.h"
-#include "core/GrinderApplication.h"
-#include "task/TaskCatalog.h"
-#include "ui/UIUtils.h"
-#include "res/Resources.h"
-
-TaskPoolWidget::TaskPoolWidget(QWidget* parent) : MetaWidget(parent),
-	_layout{new QVBoxLayout{this}}
-{
-	// Create tasks actions
-	_newTaskAction = UIUtils::createAction(this, "&New task", FILE_ICON_ADD, nullptr, "Add a new task", "", Qt::WidgetShortcut, nullptr, true);
-	_removeAllTasksAction = UIUtils::createAction(this, "Remove all tasks", "", SLOT(removeAllTasks()), "Remove all tasks");
-
-	// Set up the new task menu
-	for (auto type : TaskCatalog::getTypes())
-	{
-		auto action = _newTaskMenu.addAction(type + " task");
-		action->setData(type);
-
-		connect(action, SIGNAL(triggered(bool)), this, SLOT(newTask()));
-	}
-
-	_newTaskAction->setMenu(&_newTaskMenu);
-
-	// Add a spacer item to align all tasks
-	_layout->setContentsMargins(0, 0, 0, 0);
-	_layout->setSpacing(0);
-	_layout->addItem(new QSpacerItem{0, 0, QSizePolicy::Expanding, QSizePolicy::Expanding});
-
-	updateActions();
-}
-
-void TaskPoolWidget::addTask(const std::shared_ptr<Task>& task)
-{
-	if (!findTaskWidget(task.get()))
-	{
-		// Create a new task widget
-		auto widget = new TaskWidget{task, this};
-		_layout->insertWidget(_taskWidgets.size(), widget);
-
-		_taskWidgets.push_back(widget);
-
-		updateActions();
-	}
-}
-
-void TaskPoolWidget::removeTask(const std::shared_ptr<Task>& task)
-{
-	// Remove the task widget
-	if (auto widget = findTaskWidget(task.get()))
-	{
-		_taskWidgets.erase(std::remove(_taskWidgets.begin(), _taskWidgets.end(), widget), _taskWidgets.end());
-
-		widget->hide();
-		delete widget;
-
-		updateActions();
-	}
-}
-
-std::vector<QAction*> TaskPoolWidget::getActions(MetaWidget::AddActionsMode mode) const
-{
-	std::vector<QAction*> actions;
-
-	actions.push_back(_newTaskAction);
-
-	if (mode == AddActionsMode::ContextMenu)
-	{
-		actions.push_back(nullptr);
-		actions.push_back(_removeAllTasksAction);
-	}
-
-	return actions;
-}
-
-void TaskPoolWidget::newTask()
-{
-	if (auto action = dynamic_cast<QAction*>(sender()))
-	{
-		TaskType taskType = action->data().toString();
-
-		if (taskType != TaskType::Undefined)
-			grinder()->taskController().newTask(taskType);
-	}
-}
-
-void TaskPoolWidget::removeAllTasks()
-{
-	grinder()->taskController().removeAllTasks();
-	updateActions();
-}
-
-void TaskPoolWidget::updateActions()
-{
-	_removeAllTasksAction->setEnabled(!grinder()->project().taskPool().tasks().empty());
-}
-
-TaskWidget* TaskPoolWidget::findTaskWidget(const Task* task) const
-{
-	for (auto widget : _taskWidgets)
-	{
-		if (widget->task() == task)
-			return widget;
-	}
-
-	return nullptr;
-}
+/******************************************************************************
+ * File: TaskPoolWidget.cpp
+ * Date: 01.11.2018
+ *****************************************************************************/
+
+#include "Grinder.h"
+#include "TaskPoolWidget.h"
+#include "TaskWidget.h"
+#include "core/GrinderApplication.h"
+#include "task/TaskCatalog.h"
+#include "ui/UIUtils.h"
+#include "res/Resources.h"
+
+TaskPoolWidget::TaskPoolWidget(QWidget* parent) : MetaWidget(parent),
+	_layout{new QVBoxLayout{this}}
+{
+	// Create tasks actions
+	_newTaskAction = UIUtils::createAction(this, "&New task", FILE_ICON_ADD, nullptr, "Add a new task", "", Qt::WidgetShortcut, nullptr, true);
+	_removeAllTasksAction = UIUtils::createAction(this, "Remove all tasks", "", SLOT(removeAllTasks()), "Remove all tasks");
+
+	// Set up the new task menu
+	for (auto type : TaskCatalog::getTypes())
+	{
+		if (!TaskCatalog::isUserCreatable(type))
+			continue;
+
+		auto action = _newTaskMenu.addAction(type + " task");
+		action->setData(type);
+
+		connect(action, SIGNAL(triggered(bool)), this, SLOT(newTask()));
+	}
+
+	_newTaskAction->setMenu(&_newTaskMenu);
+
+	// Add a spacer item to align all tasks
+	_layout->setContentsMargins(0, 0, 0, 0);
+	_layout->setSpacing(0);
+	_layout->addItem(new QSpacerItem{0, 0, QSizePolicy::Expanding, QSizePolicy::Expanding});
+
+	updateActions();
+}
+
+void TaskPoolWidget::addTask(const std::shared_ptr<Task>& task)
+{
+	if (!findTaskWidget(task.get()))
+	{
+		// Create a new task widget
+		auto widget = new TaskWidget{task, this};
+		_layout->insertWidget(_taskWidgets.size(), widget);
+
+		_taskWidgets.push_back(widget);
+
+		updateActions();
+	}
+}
+
+void TaskPoolWidget::removeTask(const std::shared_ptr<Task>& task)
+{
+	// Remove the task widget
+	if (auto widget = findTaskWidget(task.get()))
+	{
+		_taskWidgets.erase(std::remove(_taskWidgets.begin(), _taskWidgets.end(), widget), _taskWidgets.end());
+
+		widget->hide();
+		delete widget;
+
+		updateActions();
+	}
+}
+
+std::vector<QAction*> TaskPoolWidget::getActions(MetaWidget::AddActionsMode mode) const
+{
+	std::vector<QAction*> actions;
+
+	actions.push_back(_newTaskAction);
+
+	if (mode == AddActionsMode::ContextMenu)
+	{
+		actions.push_back(nullptr);
+		actions.push_back(_removeAllTasksAction);
+	}
+
+	return actions;
+}
+
+void TaskPoolWidget::newTask()
+{
+	if (auto action = dynamic_cast<QAction*>(sender()))
+	{
+		TaskType taskType = action->data().toString();
+
+		if (taskType != TaskType::Undefined)
+			grinder()->taskController().newTask(taskType);
+	}
+}
+
+void TaskPoolWidget::removeAllTasks()
+{
+	grinder()->taskController().removeAllTasks();
+	updateActions();
+}
+
+void TaskPoolWidget::updateActions()
+{
+	_removeAllTasksAction->setEnabled(!grinder()->project().taskPool().tasks().empty());
+}
+
+TaskWidget* TaskPoolWidget::findTaskWidget(const Task* task) const
+{
+	for (auto widget : _taskWidgets)
+	{
+		if (widget->task() == task)
+			return widget;
+	}
+
+	return nullptr;
+}
diff --git a/Grinder/ui/task/TaskWidget.cpp b/Grinder/ui/task/TaskWidget.cpp
index f486aa0..2756366 100644
--- a/Grinder/ui/task/TaskWidget.cpp
+++ b/Grinder/ui/task/TaskWidget.cpp
@@ -1,177 +1,188 @@
-/******************************************************************************
- * File: TaskWidget.cpp
- * Date: 03.11.2018
- *****************************************************************************/
-
-#include "Grinder.h"
-#include "TaskWidget.h"
-#include "ui_TaskWidget.h"
-#include "core/GrinderApplication.h"
-#include "task/Task.h"
-#include "ui/dlg/TextViewerDialog.h"
-#include "res/Resources.h"
-
-TaskWidget::TaskWidget(const std::shared_ptr<Task>& task, QWidget *parent) : QWidget(parent),
-	ui{new Ui::TaskWidget}, _task{task}
-{
-	if (!task)
-		throw std::invalid_argument{_EXCPT("task may not be null")};
-
-	setupUi();
-
-	// Update the UI whenever the task has changed
-	if (auto task = _task.lock())	// Make sure that the underlying task still exists
-		connect(task.get(), &Task::taskUpdated, this, &TaskWidget::updateUi);
-}
-
-TaskWidget::~TaskWidget()
-{
-	delete ui;
-}
-
-void TaskWidget::showEvent(QShowEvent* event)
-{
-	QWidget::showEvent(event);
-	updateUi();
-}
-
-void TaskWidget::resizeEvent(QResizeEvent* event)
-{
-	QWidget::resizeEvent(event);
-	updateUi();
-}
-
-void TaskWidget::setupUi()
-{
-	ui->setupUi(this);
-
-	QColor clrBorder = QPalette{}.color(QPalette::Dark);
-	ui->line->setStyleSheet(QString{"color: %1; margin-bottom: -1px;"}.arg(clrBorder.name()));
-
-	updateUi();
-}
-
-void TaskWidget::updateUi()
-{
-	static auto setEllidedText = [](QLabel* label, QString text) {
-		text.replace("\t", "");
-
-		QFontMetrics metrics(label->font());
-		int width = label->width() - 2;
-		QString clippedText = metrics.elidedText(text, Qt::ElideRight, width);
-
-		label->setText(clippedText);
-		label->setToolTip(text);
-	};
-
-	if (auto task = _task.lock())	// Make sure that the underlying task still exists
-	{
-		setEllidedText(ui->lblTitle, task->getName());
-
-		auto getStatusStyleSheet = [](QColor color) { return QString{"background: %1; border: 1px solid %2; border-radius: 10px; margin-top: 2px; margin-bottom: 2px;"}.arg(color.name()).arg(color.darker(130).name()); };
-
-		if (task->isRunning())
-		{
-			setEllidedText(ui->lblMessage, task->getMessage());
-
-			if (task->isPaused())
-			{
-				ui->lblStatus->setText("Paused");
-				ui->lblStatus->setStyleSheet(getStatusStyleSheet(QColor{255, 200, 120}));
-			}
-			else
-			{
-				ui->lblStatus->setText("Running");
-				ui->lblStatus->setStyleSheet(getStatusStyleSheet(QColor{140, 220, 255}));
-			}			
-		}
-		else
-		{
-			switch (task->getResult())
-			{
-			case Task::Result::Succeeded:
-				setEllidedText(ui->lblMessage, "The task succeeded");
-				ui->lblStatus->setText("Succeeded");
-				ui->lblStatus->setStyleSheet(getStatusStyleSheet(QColor{120, 255, 120}));
-				break;
-
-			case Task::Result::Failed:
-				setEllidedText(ui->lblMessage, "The task failed");
-				ui->lblStatus->setText("Failed");
-				ui->lblStatus->setStyleSheet(getStatusStyleSheet(QColor{255, 120, 120}));
-				break;
-
-			default:
-				setEllidedText(ui->lblMessage, "");
-				ui->lblStatus->setText("Stopped");
-				ui->lblStatus->setStyleSheet(getStatusStyleSheet(QColor{190, 190, 190}));
-				break;
-			}
-		}
-
-		ui->progressBar->setEnabled(task->getCapabilities().testFlag(Task::Capability::HasProgress));
-		ui->progressBar->setValue(task->getProgress() * 100.0f);
-
-		ui->btnStart->setEnabled(!task->isRunning() || task->getCapabilities().testFlag(Task::Capability::CanBePaused));
-		ui->btnStart->setIcon(QIcon{task->isRunning() && !task->isPaused() ?  FILE_ICON_PAUSE : FILE_ICON_START});
-		ui->btnStart->setToolTip(task->isRunning() && !task->isPaused() ? "Pause task" : (task->isRunning() ? "Resume task" : "Start task"));
-		ui->btnStop->setEnabled(task->isRunning());
-		ui->btnRefresh->setEnabled(task->isRunning() && task->getCapabilities().testFlag(Task::Capability::CanBeRefreshed));
-
-		ui->btnConfigure->setEnabled(!task->isRunning());
-		ui->btnDelete->setEnabled(!task->isRunning());
-	}
-}
-
-void TaskWidget::on_btnStart_clicked()
-{
-	if (auto task = _task.lock())	// Make sure that the underlying task still exists
-	{
-		if (task->isRunning())
-			grinder()->taskController().pauseTask(task.get(), !task->isPaused());
-		else
-			grinder()->taskController().startTask(task.get());
-	}
-}
-
-void TaskWidget::on_btnStop_clicked()
-{
-	if (auto task = _task.lock())	// Make sure that the underlying task still exists
-	{
-		if (task->isRunning())
-			grinder()->taskController().stopTask(task.get());
-	}
-}
-
-void TaskWidget::on_btnRefresh_clicked()
-{
-	if (auto task = _task.lock())	// Make sure that the underlying task still exists
-	{
-		if (task->isRunning())
-			grinder()->taskController().refreshTask(task.get());
-	}
-}
-
-void TaskWidget::on_btnViewLog_clicked()
-{
-	if (auto task = _task.lock())	// Make sure that the underlying task still exists
-		TextViewerDialog::viewText(nullptr, "View task log", QString{"Log of task '%1':"}.arg(task->getName()), task->getMessageLog().join("\n"), "No log available");
-}
-
-void grndr::TaskWidget::on_btnConfigure_clicked()
-{
-	if (auto task = _task.lock())	// Make sure that the underlying task still exists
-	{
-		if (!task->isRunning())
-			grinder()->taskController().configureTask(task.get());
-	}
-}
-
-void TaskWidget::on_btnDelete_clicked()
-{
-	if (auto task = _task.lock())	// Make sure that the underlying task still exists
-	{
-		// Use a timer to give Qt a chance to finish handling the click event before removing this widget
-		QTimer::singleShot(0, [task] { grinder()->taskController().removeTask(task.get()); });
-	}
-}
+/******************************************************************************
+ * File: TaskWidget.cpp
+ * Date: 03.11.2018
+ *****************************************************************************/
+
+#include "Grinder.h"
+#include "TaskWidget.h"
+#include "ui_TaskWidget.h"
+#include "core/GrinderApplication.h"
+#include "task/Task.h"
+#include "ui/dlg/TextViewerDialog.h"
+#include "res/Resources.h"
+
+TaskWidget::TaskWidget(const std::shared_ptr<Task>& task, QWidget *parent) : QWidget(parent),
+	ui{new Ui::TaskWidget}, _task{task}
+{
+	if (!task)
+		throw std::invalid_argument{_EXCPT("task may not be null")};
+
+	setupUi();
+
+	// Update the UI whenever the task has changed
+	if (auto task = _task.lock())	// Make sure that the underlying task still exists
+		connect(task.get(), &Task::taskUpdated, this, &TaskWidget::updateUi);
+}
+
+TaskWidget::~TaskWidget()
+{
+	delete ui;
+}
+
+void TaskWidget::showEvent(QShowEvent* event)
+{
+	QWidget::showEvent(event);
+	updateUi();
+}
+
+void TaskWidget::resizeEvent(QResizeEvent* event)
+{
+	QWidget::resizeEvent(event);
+	updateUi();
+}
+
+void TaskWidget::setupUi()
+{
+	ui->setupUi(this);
+
+	QColor clrBorder = QPalette{}.color(QPalette::Dark);
+	ui->line->setStyleSheet(QString{"color: %1; margin-bottom: -1px;"}.arg(clrBorder.name()));
+
+	updateUi();
+}
+
+void TaskWidget::updateUi()
+{
+	static auto setEllidedText = [](QLabel* label, QString text) {
+		text.replace("\t", "");
+
+		QFontMetrics metrics(label->font());
+		int width = label->width() - 2;
+		QString clippedText = metrics.elidedText(text, Qt::ElideRight, width);
+
+		label->setText(clippedText);
+		label->setToolTip(text);
+	};
+
+	// Update texts, colors etc.
+	if (auto task = _task.lock())	// Make sure that the underlying task still exists
+	{
+		setEllidedText(ui->lblTitle, task->getName());
+
+		auto getStatusStyleSheet = [](QColor color) { return QString{"background: %1; border: 1px solid %2; border-radius: 10px; margin-top: 2px; margin-bottom: 2px;"}.arg(color.name()).arg(color.darker(130).name()); };
+
+		if (task->isRunning())
+		{
+			setEllidedText(ui->lblMessage, task->getMessage());
+
+			if (task->isPaused())
+			{
+				ui->lblStatus->setText("Paused");
+				ui->lblStatus->setStyleSheet(getStatusStyleSheet(QColor{255, 200, 120}));
+			}
+			else
+			{
+				ui->lblStatus->setText("Running");
+				ui->lblStatus->setStyleSheet(getStatusStyleSheet(QColor{140, 220, 255}));
+			}			
+		}
+		else
+		{
+			switch (task->getResult())
+			{
+			case Task::Result::Succeeded:
+				setEllidedText(ui->lblMessage, "The task succeeded");
+				ui->lblStatus->setText("Succeeded");
+				ui->lblStatus->setStyleSheet(getStatusStyleSheet(QColor{120, 255, 120}));
+				break;
+
+			case Task::Result::Failed:
+				setEllidedText(ui->lblMessage, "The task failed");
+				ui->lblStatus->setText("Failed");
+				ui->lblStatus->setStyleSheet(getStatusStyleSheet(QColor{255, 120, 120}));
+				break;
+
+			default:
+				setEllidedText(ui->lblMessage, "");
+				ui->lblStatus->setText("Stopped");
+				ui->lblStatus->setStyleSheet(getStatusStyleSheet(QColor{190, 190, 190}));
+				break;
+			}
+		}
+
+		ui->lblTaskInfo->setText(task->getInfo());
+
+		// Update control states
+		ui->progressBar->setEnabled(task->getCapabilities().testFlag(Task::Capability::HasProgress));
+		ui->progressBar->setValue(task->getProgress() * 100.0f);
+
+		ui->btnStart->setEnabled(!task->isRunning() || task->getCapabilities().testFlag(Task::Capability::CanBePaused));
+		ui->btnStart->setVisible(task->getCapabilities().testFlag(Task::Capability::UserControllable));
+		ui->btnStart->setIcon(QIcon{task->isRunning() && !task->isPaused() ?  FILE_ICON_PAUSE : FILE_ICON_START});
+		ui->btnStart->setToolTip(task->isRunning() && !task->isPaused() ? "Pause task" : (task->isRunning() ? "Resume task" : "Start task"));
+		ui->btnStop->setEnabled(task->isRunning());
+		ui->btnStop->setVisible(task->getCapabilities().testFlag(Task::Capability::UserControllable));
+		ui->btnRefresh->setEnabled(task->isRunning() && task->getCapabilities().testFlag(Task::Capability::CanBeRefreshed));
+		ui->btnRefresh->setVisible(task->getCapabilities().testFlag(Task::Capability::UserControllable));
+
+		ui->btnConfigure->setEnabled(!task->isRunning());
+		ui->btnConfigure->setVisible(task->getCapabilities().testFlag(Task::Capability::UserControllable));
+		ui->btnDelete->setEnabled(!task->isRunning());
+	}
+}
+
+void TaskWidget::on_btnStart_clicked()
+{
+	if (auto task = _task.lock())	// Make sure that the underlying task still exists
+	{
+		if (task->getCapabilities().testFlag(Task::Capability::UserControllable))
+		{
+			if (task->isRunning())
+				grinder()->taskController().pauseTask(task.get(), !task->isPaused());
+			else
+				grinder()->taskController().startTask(task.get());
+		}
+	}
+}
+
+void TaskWidget::on_btnStop_clicked()
+{
+	if (auto task = _task.lock())	// Make sure that the underlying task still exists
+	{
+		if (task->isRunning() && task->getCapabilities().testFlag(Task::Capability::UserControllable))
+			grinder()->taskController().stopTask(task.get());
+	}
+}
+
+void TaskWidget::on_btnRefresh_clicked()
+{
+	if (auto task = _task.lock())	// Make sure that the underlying task still exists
+	{
+		if (task->isRunning() && task->getCapabilities().testFlag(Task::Capability::UserControllable))
+			grinder()->taskController().refreshTask(task.get());
+	}
+}
+
+void TaskWidget::on_btnViewLog_clicked()
+{
+	if (auto task = _task.lock())	// Make sure that the underlying task still exists
+		TextViewerDialog::viewText(nullptr, "View task log", QString{"Log of task '%1':"}.arg(task->getName()), task->getMessageLog().join("\n"), "No log available");
+}
+
+void grndr::TaskWidget::on_btnConfigure_clicked()
+{
+	if (auto task = _task.lock())	// Make sure that the underlying task still exists
+	{
+		if (!task->isRunning() && task->getCapabilities().testFlag(Task::Capability::UserControllable))
+			grinder()->taskController().configureTask(task.get());
+	}
+}
+
+void TaskWidget::on_btnDelete_clicked()
+{
+	if (auto task = _task.lock())	// Make sure that the underlying task still exists
+	{
+		// Use a timer to give Qt a chance to finish handling the click event before removing this widget
+		QTimer::singleShot(0, [task] { grinder()->taskController().removeTask(task.get()); });
+	}
+}
diff --git a/Grinder/ui/task/TaskWidget.ui b/Grinder/ui/task/TaskWidget.ui
index d208c6e..419f597 100644
--- a/Grinder/ui/task/TaskWidget.ui
+++ b/Grinder/ui/task/TaskWidget.ui
@@ -1,315 +1,325 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>TaskWidget</class>
- <widget class="QWidget" name="TaskWidget">
-  <property name="geometry">
-   <rect>
-    <x>0</x>
-    <y>0</y>
-    <width>418</width>
-    <height>126</height>
-   </rect>
-  </property>
-  <property name="windowTitle">
-   <string>Form</string>
-  </property>
-  <layout class="QVBoxLayout" name="verticalLayout_2">
-   <property name="spacing">
-    <number>0</number>
-   </property>
-   <property name="leftMargin">
-    <number>0</number>
-   </property>
-   <property name="topMargin">
-    <number>0</number>
-   </property>
-   <property name="rightMargin">
-    <number>0</number>
-   </property>
-   <property name="bottomMargin">
-    <number>0</number>
-   </property>
-   <item>
-    <widget class="QWidget" name="widget" native="true">
-     <layout class="QVBoxLayout" name="verticalLayout">
-      <item>
-       <widget class="QWidget" name="widget_3" native="true">
-        <layout class="QGridLayout" name="gridLayout">
-         <property name="leftMargin">
-          <number>0</number>
-         </property>
-         <property name="topMargin">
-          <number>0</number>
-         </property>
-         <property name="rightMargin">
-          <number>0</number>
-         </property>
-         <property name="bottomMargin">
-          <number>3</number>
-         </property>
-         <item row="0" column="0" rowspan="2" colspan="2">
-          <widget class="QLabel" name="lblTitle">
-           <property name="sizePolicy">
-            <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
-             <horstretch>0</horstretch>
-             <verstretch>0</verstretch>
-            </sizepolicy>
-           </property>
-           <property name="font">
-            <font>
-             <weight>75</weight>
-             <bold>true</bold>
-            </font>
-           </property>
-           <property name="text">
-            <string>TextLabel</string>
-           </property>
-          </widget>
-         </item>
-         <item row="0" column="2" rowspan="3">
-          <widget class="QLabel" name="lblStatus">
-           <property name="sizePolicy">
-            <sizepolicy hsizetype="Fixed" vsizetype="Minimum">
-             <horstretch>0</horstretch>
-             <verstretch>0</verstretch>
-            </sizepolicy>
-           </property>
-           <property name="minimumSize">
-            <size>
-             <width>90</width>
-             <height>0</height>
-            </size>
-           </property>
-           <property name="font">
-            <font>
-             <weight>75</weight>
-             <bold>true</bold>
-            </font>
-           </property>
-           <property name="styleSheet">
-            <string notr="true">border: 1px solid black;
-border-radius: 5px;</string>
-           </property>
-           <property name="text">
-            <string>TextLabel</string>
-           </property>
-           <property name="alignment">
-            <set>Qt::AlignCenter</set>
-           </property>
-          </widget>
-         </item>
-         <item row="2" column="0" colspan="2">
-          <widget class="QLabel" name="lblMessage">
-           <property name="sizePolicy">
-            <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
-             <horstretch>0</horstretch>
-             <verstretch>0</verstretch>
-            </sizepolicy>
-           </property>
-           <property name="text">
-            <string>TextLabel</string>
-           </property>
-          </widget>
-         </item>
-        </layout>
-       </widget>
-      </item>
-      <item>
-       <widget class="QProgressBar" name="progressBar">
-        <property name="value">
-         <number>24</number>
-        </property>
-       </widget>
-      </item>
-      <item>
-       <widget class="QWidget" name="widget_2" native="true">
-        <layout class="QHBoxLayout" name="horizontalLayout">
-         <property name="leftMargin">
-          <number>0</number>
-         </property>
-         <property name="topMargin">
-          <number>6</number>
-         </property>
-         <property name="rightMargin">
-          <number>0</number>
-         </property>
-         <property name="bottomMargin">
-          <number>0</number>
-         </property>
-         <item>
-          <widget class="QToolButton" name="btnViewLog">
-           <property name="toolTip">
-            <string>View the task log</string>
-           </property>
-           <property name="statusTip">
-            <string>View the task log</string>
-           </property>
-           <property name="styleSheet">
-            <string notr="true">QToolButton {
-	padding: 5px
-}
-</string>
-           </property>
-           <property name="text">
-            <string>View log</string>
-           </property>
-           <property name="icon">
-            <iconset resource="../../res/Grinder.qrc">
-             <normaloff>:/icons/icons/basic-text-format.png</normaloff>:/icons/icons/basic-text-format.png</iconset>
-           </property>
-           <property name="toolButtonStyle">
-            <enum>Qt::ToolButtonIconOnly</enum>
-           </property>
-          </widget>
-         </item>
-         <item>
-          <widget class="QToolButton" name="btnConfigure">
-           <property name="toolTip">
-            <string>Configure this task</string>
-           </property>
-           <property name="statusTip">
-            <string>Configure this task</string>
-           </property>
-           <property name="styleSheet">
-            <string notr="true">QToolButton {
-	padding: 5px
-}
-</string>
-           </property>
-           <property name="text">
-            <string>...</string>
-           </property>
-           <property name="icon">
-            <iconset resource="../../res/Grinder.qrc">
-             <normaloff>:/icons/icons/gear.png</normaloff>:/icons/icons/gear.png</iconset>
-           </property>
-          </widget>
-         </item>
-         <item>
-          <widget class="QToolButton" name="btnDelete">
-           <property name="toolTip">
-            <string>Delete this task</string>
-           </property>
-           <property name="statusTip">
-            <string>Delete this task</string>
-           </property>
-           <property name="styleSheet">
-            <string notr="true">QToolButton {
-	padding: 5px
-}
-</string>
-           </property>
-           <property name="text">
-            <string>...</string>
-           </property>
-           <property name="icon">
-            <iconset resource="../../res/Grinder.qrc">
-             <normaloff>:/icons/icons/delete.png</normaloff>:/icons/icons/delete.png</iconset>
-           </property>
-          </widget>
-         </item>
-         <item>
-          <spacer name="horizontalSpacer">
-           <property name="orientation">
-            <enum>Qt::Horizontal</enum>
-           </property>
-           <property name="sizeHint" stdset="0">
-            <size>
-             <width>239</width>
-             <height>20</height>
-            </size>
-           </property>
-          </spacer>
-         </item>
-         <item>
-          <widget class="QToolButton" name="btnRefresh">
-           <property name="toolTip">
-            <string>Refresh this task</string>
-           </property>
-           <property name="statusTip">
-            <string>Refresh this task</string>
-           </property>
-           <property name="styleSheet">
-            <string notr="true">QToolButton {
-	padding: 5px
-}
-</string>
-           </property>
-           <property name="text">
-            <string>...</string>
-           </property>
-           <property name="icon">
-            <iconset resource="../../res/Grinder.qrc">
-             <normaloff>:/icons/icons/arrows-circle.png</normaloff>:/icons/icons/arrows-circle.png</iconset>
-           </property>
-          </widget>
-         </item>
-         <item>
-          <widget class="QToolButton" name="btnStart">
-           <property name="toolTip">
-            <string>Start/pause this task</string>
-           </property>
-           <property name="statusTip">
-            <string>Start/pause this task</string>
-           </property>
-           <property name="styleSheet">
-            <string notr="true">QToolButton {
-	padding: 5px
-}
-</string>
-           </property>
-           <property name="text">
-            <string>...</string>
-           </property>
-           <property name="icon">
-            <iconset resource="../../res/Grinder.qrc">
-             <normaloff>:/icons/icons/start.png</normaloff>:/icons/icons/start.png</iconset>
-           </property>
-          </widget>
-         </item>
-         <item>
-          <widget class="QToolButton" name="btnStop">
-           <property name="toolTip">
-            <string>Stop this task</string>
-           </property>
-           <property name="statusTip">
-            <string>Stop this task</string>
-           </property>
-           <property name="styleSheet">
-            <string notr="true">QToolButton {
-	padding: 5px
-}
-</string>
-           </property>
-           <property name="text">
-            <string>...</string>
-           </property>
-           <property name="icon">
-            <iconset resource="../../res/Grinder.qrc">
-             <normaloff>:/icons/icons/stop.png</normaloff>:/icons/icons/stop.png</iconset>
-           </property>
-          </widget>
-         </item>
-        </layout>
-       </widget>
-      </item>
-     </layout>
-    </widget>
-   </item>
-   <item>
-    <widget class="Line" name="line">
-     <property name="frameShadow">
-      <enum>QFrame::Plain</enum>
-     </property>
-     <property name="orientation">
-      <enum>Qt::Horizontal</enum>
-     </property>
-    </widget>
-   </item>
-  </layout>
- </widget>
- <resources>
-  <include location="../../res/Grinder.qrc"/>
- </resources>
- <connections/>
-</ui>
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>TaskWidget</class>
+ <widget class="QWidget" name="TaskWidget">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>418</width>
+    <height>126</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Form</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout_2">
+   <property name="spacing">
+    <number>0</number>
+   </property>
+   <property name="leftMargin">
+    <number>0</number>
+   </property>
+   <property name="topMargin">
+    <number>0</number>
+   </property>
+   <property name="rightMargin">
+    <number>0</number>
+   </property>
+   <property name="bottomMargin">
+    <number>0</number>
+   </property>
+   <item>
+    <widget class="QWidget" name="widget" native="true">
+     <layout class="QVBoxLayout" name="verticalLayout">
+      <item>
+       <widget class="QWidget" name="widget_3" native="true">
+        <layout class="QGridLayout" name="gridLayout">
+         <property name="leftMargin">
+          <number>0</number>
+         </property>
+         <property name="topMargin">
+          <number>0</number>
+         </property>
+         <property name="rightMargin">
+          <number>0</number>
+         </property>
+         <property name="bottomMargin">
+          <number>3</number>
+         </property>
+         <item row="0" column="0" rowspan="2" colspan="2">
+          <widget class="QLabel" name="lblTitle">
+           <property name="sizePolicy">
+            <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+             <horstretch>0</horstretch>
+             <verstretch>0</verstretch>
+            </sizepolicy>
+           </property>
+           <property name="font">
+            <font>
+             <weight>75</weight>
+             <bold>true</bold>
+            </font>
+           </property>
+           <property name="text">
+            <string>TextLabel</string>
+           </property>
+          </widget>
+         </item>
+         <item row="0" column="2" rowspan="3">
+          <widget class="QLabel" name="lblStatus">
+           <property name="sizePolicy">
+            <sizepolicy hsizetype="Fixed" vsizetype="Minimum">
+             <horstretch>0</horstretch>
+             <verstretch>0</verstretch>
+            </sizepolicy>
+           </property>
+           <property name="minimumSize">
+            <size>
+             <width>90</width>
+             <height>0</height>
+            </size>
+           </property>
+           <property name="font">
+            <font>
+             <weight>75</weight>
+             <bold>true</bold>
+            </font>
+           </property>
+           <property name="styleSheet">
+            <string notr="true">border: 1px solid black;
+border-radius: 5px;</string>
+           </property>
+           <property name="text">
+            <string>TextLabel</string>
+           </property>
+           <property name="alignment">
+            <set>Qt::AlignCenter</set>
+           </property>
+          </widget>
+         </item>
+         <item row="2" column="0" colspan="2">
+          <widget class="QLabel" name="lblMessage">
+           <property name="sizePolicy">
+            <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+             <horstretch>0</horstretch>
+             <verstretch>0</verstretch>
+            </sizepolicy>
+           </property>
+           <property name="text">
+            <string>TextLabel</string>
+           </property>
+          </widget>
+         </item>
+        </layout>
+       </widget>
+      </item>
+      <item>
+       <widget class="QProgressBar" name="progressBar">
+        <property name="value">
+         <number>24</number>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QWidget" name="widget_2" native="true">
+        <layout class="QHBoxLayout" name="horizontalLayout">
+         <property name="leftMargin">
+          <number>0</number>
+         </property>
+         <property name="topMargin">
+          <number>6</number>
+         </property>
+         <property name="rightMargin">
+          <number>0</number>
+         </property>
+         <property name="bottomMargin">
+          <number>0</number>
+         </property>
+         <item>
+          <widget class="QToolButton" name="btnViewLog">
+           <property name="toolTip">
+            <string>View the task log</string>
+           </property>
+           <property name="statusTip">
+            <string>View the task log</string>
+           </property>
+           <property name="styleSheet">
+            <string notr="true">QToolButton {
+	padding: 5px
+}
+</string>
+           </property>
+           <property name="text">
+            <string>View log</string>
+           </property>
+           <property name="icon">
+            <iconset resource="../../res/Grinder.qrc">
+             <normaloff>:/icons/icons/basic-text-format.png</normaloff>:/icons/icons/basic-text-format.png</iconset>
+           </property>
+           <property name="toolButtonStyle">
+            <enum>Qt::ToolButtonIconOnly</enum>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QToolButton" name="btnConfigure">
+           <property name="toolTip">
+            <string>Configure this task</string>
+           </property>
+           <property name="statusTip">
+            <string>Configure this task</string>
+           </property>
+           <property name="styleSheet">
+            <string notr="true">QToolButton {
+	padding: 5px
+}
+</string>
+           </property>
+           <property name="text">
+            <string>...</string>
+           </property>
+           <property name="icon">
+            <iconset resource="../../res/Grinder.qrc">
+             <normaloff>:/icons/icons/gear.png</normaloff>:/icons/icons/gear.png</iconset>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QToolButton" name="btnDelete">
+           <property name="toolTip">
+            <string>Delete this task</string>
+           </property>
+           <property name="statusTip">
+            <string>Delete this task</string>
+           </property>
+           <property name="styleSheet">
+            <string notr="true">QToolButton {
+	padding: 5px
+}
+</string>
+           </property>
+           <property name="text">
+            <string>...</string>
+           </property>
+           <property name="icon">
+            <iconset resource="../../res/Grinder.qrc">
+             <normaloff>:/icons/icons/delete.png</normaloff>:/icons/icons/delete.png</iconset>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <spacer name="horizontalSpacer">
+           <property name="orientation">
+            <enum>Qt::Horizontal</enum>
+           </property>
+           <property name="sizeHint" stdset="0">
+            <size>
+             <width>239</width>
+             <height>20</height>
+            </size>
+           </property>
+          </spacer>
+         </item>
+         <item>
+          <widget class="QLabel" name="lblTaskInfo">
+           <property name="enabled">
+            <bool>false</bool>
+           </property>
+           <property name="text">
+            <string>TaskInfo</string>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QToolButton" name="btnRefresh">
+           <property name="toolTip">
+            <string>Refresh this task</string>
+           </property>
+           <property name="statusTip">
+            <string>Refresh this task</string>
+           </property>
+           <property name="styleSheet">
+            <string notr="true">QToolButton {
+	padding: 5px
+}
+</string>
+           </property>
+           <property name="text">
+            <string>...</string>
+           </property>
+           <property name="icon">
+            <iconset resource="../../res/Grinder.qrc">
+             <normaloff>:/icons/icons/arrows-circle.png</normaloff>:/icons/icons/arrows-circle.png</iconset>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QToolButton" name="btnStart">
+           <property name="toolTip">
+            <string>Start/pause this task</string>
+           </property>
+           <property name="statusTip">
+            <string>Start/pause this task</string>
+           </property>
+           <property name="styleSheet">
+            <string notr="true">QToolButton {
+	padding: 5px
+}
+</string>
+           </property>
+           <property name="text">
+            <string>...</string>
+           </property>
+           <property name="icon">
+            <iconset resource="../../res/Grinder.qrc">
+             <normaloff>:/icons/icons/start.png</normaloff>:/icons/icons/start.png</iconset>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QToolButton" name="btnStop">
+           <property name="toolTip">
+            <string>Stop this task</string>
+           </property>
+           <property name="statusTip">
+            <string>Stop this task</string>
+           </property>
+           <property name="styleSheet">
+            <string notr="true">QToolButton {
+	padding: 5px
+}
+</string>
+           </property>
+           <property name="text">
+            <string>...</string>
+           </property>
+           <property name="icon">
+            <iconset resource="../../res/Grinder.qrc">
+             <normaloff>:/icons/icons/stop.png</normaloff>:/icons/icons/stop.png</iconset>
+           </property>
+          </widget>
+         </item>
+        </layout>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
+    <widget class="Line" name="line">
+     <property name="frameShadow">
+      <enum>QFrame::Plain</enum>
+     </property>
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources>
+  <include location="../../res/Grinder.qrc"/>
+ </resources>
+ <connections/>
+</ui>
-- 
GitLab