diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..3ab8784078c0af970f261f7dcb46450e62e8cebe --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/bin-Debug/ +/bin-Release/ +/build-Debug/ +/build-Release/ diff --git a/CodeBackup_d.lst b/CodeBackup_d.lst new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/CodeBackup_r+.lst b/CodeBackup_r+.lst new file mode 100644 index 0000000000000000000000000000000000000000..ca62e07b574857d67113ff6927aa5bf04786de7a --- /dev/null +++ b/CodeBackup_r+.lst @@ -0,0 +1 @@ +Grinder\*.* diff --git a/CodeBackup_r-.lst b/CodeBackup_r-.lst new file mode 100644 index 0000000000000000000000000000000000000000..7019cabd842c903a8bb9b139e3ddfd0f66243102 --- /dev/null +++ b/CodeBackup_r-.lst @@ -0,0 +1 @@ +.\*.* diff --git a/CreateCodeBackup.cmd b/CreateCodeBackup.cmd new file mode 100644 index 0000000000000000000000000000000000000000..05afea587951d7364ce29207dd42a16d8864b3d3 --- /dev/null +++ b/CreateCodeBackup.cmd @@ -0,0 +1,4 @@ +del Q:\Downloaded\GrinderXXXX.rar + +E:\WinRar\WinRar.exe A -y -m5 -s -x@CodeBackup_d.lst Q:\Downloaded\GrinderXXXX.rar @CodeBackup_r-.lst +E:\WinRar\WinRar.exe A -y -r -m5 -s -x@CodeBackup_d.lst Q:\Downloaded\GrinderXXXX.rar @CodeBackup_r+.lst diff --git a/Grinder.smproj b/Grinder.smproj new file mode 100644 index 0000000000000000000000000000000000000000..2db2df38f2f89894252cb564f2aa7d1ce2b08a05 Binary files /dev/null and b/Grinder.smproj differ diff --git a/Grinder.tdl b/Grinder.tdl new file mode 100644 index 0000000000000000000000000000000000000000..688c21872e8db99d4ff912f1ca7ce24e096a66e1 Binary files /dev/null and b/Grinder.tdl differ diff --git a/Grinder/.gitignore b/Grinder/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..75c107bcc90038bcdd0d643b20eb4d33a13b4245 --- /dev/null +++ b/Grinder/.gitignore @@ -0,0 +1 @@ +*.pro.user diff --git a/Grinder/Grinder.h b/Grinder/Grinder.h new file mode 100644 index 0000000000000000000000000000000000000000..f5f04777c53e0b2f67e0918e0faa5850e990b3a7 --- /dev/null +++ b/Grinder/Grinder.h @@ -0,0 +1,23 @@ +/****************************************************************************** + * File: Grinder.h + * Date: 11.1.2018 + *****************************************************************************/ + +#ifndef GRINDER_H +#define GRINDER_H + +// Global includes frequently needed +#include <QtCore> +#include <QtGui> +#include <QtWidgets> + +#include <QDebug> +#include <iostream> + +#include "core/GrinderExceptions.h" + +// Always use the application's namespace +namespace grndr {} +using namespace grndr; + +#endif diff --git a/Grinder/Grinder.pro b/Grinder/Grinder.pro new file mode 100644 index 0000000000000000000000000000000000000000..4841788786e77f0640974dc569ee559e8afeda8a --- /dev/null +++ b/Grinder/Grinder.pro @@ -0,0 +1,417 @@ +## Project settings + +VERSION = 1.0.0 + +QT += core gui widgets + +TARGET = Grinder +TEMPLATE = app + +CONFIG += c++14 +DEFINES += QT_DEPRECATED_WARNINGS + +## Output directories + +Debug:DESTDIR = ../bin-Debug +Release:DESTDIR = ../bin-Release + +## Libraries + +include(Libraries.pro) + +## Platform specifics + +win32 { + RC_FILE = res/Grinder.rc +} + +## Project files + +SOURCES += \ + main.cpp \ + ui/mainwnd/GrinderWindow.cpp \ + core/GrinderApplication.cpp \ + Version.cpp \ + util/StringUtils.cpp \ + util/StringConv.cpp \ + pipeline/PipelineItem.cpp \ + pipeline/PipelineExceptions.cpp \ + pipeline/Block.cpp \ + pipeline/BlockType.cpp \ + pipeline/Port.cpp \ + pipeline/PortType.cpp \ + pipeline/Connection.cpp \ + pipeline/ConnectionVector.cpp \ + pipeline/PortVector.cpp \ + pipeline/BlockVector.cpp \ + pipeline/Pipeline.cpp \ + pipeline/PipelineManager.cpp \ + pipeline/PipelineVector.cpp \ + ui/graph/GraphView.cpp \ + ui/graph/GraphScene.cpp \ + ui/graph/GraphStyle.cpp \ + ui/graph/GraphNode.cpp \ + ui/graph/GraphBlockNode.cpp \ + ui/graph/GraphNodeFactory.cpp \ + ui/graph/GraphPortNode.cpp \ + ui/graph/GraphConnectionNode.cpp \ + ui/graph/GraphConnectionBase.cpp \ + ui/graph/GraphConnectionBlueprint.cpp \ + ui/graph/GraphConnectionMessage.cpp \ + pipeline/BlockCatalog.cpp \ + pipeline/BlockCategory.cpp \ + util/UIUtils.cpp \ + ui/StyleSheet.cpp \ + controller/PipelineController.cpp \ + ui/graph/GraphBlockList.cpp \ + ui/graph/GraphWidget.cpp \ + project/Label.cpp \ + project/LabelVector.cpp \ + project/Project.cpp \ + project/ProjectExceptions.cpp \ + controller/ProjectController.cpp \ + ui/widget/ControlBar.cpp \ + ui/mainwnd/LabelsListItem.cpp \ + ui/mainwnd/LabelsListWidget.cpp \ + controller/GenericController.cpp \ + project/ImageReferenceVector.cpp \ + project/ImageReference.cpp \ + project/ProjectItem.cpp \ + ui/mainwnd/ImageReferencesListItem.cpp \ + ui/mainwnd/ImageReferencesListWidget.cpp \ + util/ImageUtils.cpp \ + util/FileUtils.cpp \ + util/TemporaryStatusMessage.cpp \ + engine/data/DataDescriptor.cpp \ + util/DataUtils.cpp \ + core/GrinderSettings.cpp \ + engine/data/DataBlob.cpp \ + util/CVUtils.cpp \ + engine/data/DataExceptions.cpp \ + pipeline/BlockHierarchy.cpp \ + pipeline/blocks/BinaryThresholdBlock.cpp \ + engine/Engine.cpp \ + engine/EngineExceptions.cpp \ + controller/EngineController.cpp \ + engine/EngineExecutionContext.cpp \ + engine/processors/BinaryThresholdProcessor.cpp \ + engine/ProcessorBase.cpp \ + pipeline/blocks/ConvertToGrayscaleBlock.cpp \ + engine/processors/ConvertToGrayscaleProcessor.cpp \ + ui/graph/GraphLayout.cpp \ + core/GrinderExceptions.cpp \ + project/serialization/SettingsContainer.cpp \ + project/serialization/SerializationExceptions.cpp \ + project/serialization/SettingsCodec.cpp \ + project/serialization/JsonSettingsCodec.cpp \ + project/serialization/ProjectSerializer.cpp \ + project/serialization/SerializationContext.cpp \ + project/serialization/DeserializationContext.cpp \ + util/SerializationUtils.cpp \ + common/MRUStringList.cpp \ + ui/mainwnd/RecentProjectsMenu.cpp \ + ui/dlg/OptionsDialog.cpp \ + ui/property/PropertyTreeWidget.cpp \ + ui/property/PropertyTreeItem.cpp \ + ui/property/BlockPropertyTreeItem.cpp \ + ui/property/PropertyTreeItemDelegate.cpp \ + ui/property/ValuePropertyTreeItem.cpp \ + ui/property/editors/BoolPropertyEditor.cpp \ + ui/property/editors/TextPropertyEditor.cpp \ + pipeline/blocks/OutputBlock.cpp \ + pipeline/blocks/InputBlock.cpp \ + image/ImageBuild.cpp \ + image/ImageBuildVector.cpp \ + image/ImageBuildPool.cpp \ + image/ImageExceptions.cpp \ + engine/processors/InputProcessor.cpp \ + engine/processors/OutputProcessor.cpp \ + ui/image/ImageEditorManager.cpp \ + ui/image/ImageEditorDockWidget.cpp \ + ui/visscene/VisualSceneView.cpp \ + ui/visscene/VisualSceneStyle.cpp \ + ui/image/ImageEditorView.cpp \ + ui/image/ImageEditorScene.cpp \ + controller/ImageEditorController.cpp \ + ui/image/ImageEditorStyle.cpp \ + image/ImageBuildItem.cpp \ + image/Layer.cpp \ + image/LayerVector.cpp \ + ui/image/LayersListWidget.cpp \ + ui/image/LayersListItem.cpp \ + common/properties/BoolProperty.cpp \ + common/properties/IntProperty.cpp \ + common/properties/RealProperty.cpp \ + common/properties/StringProperty.cpp \ + common/properties/UIntProperty.cpp \ + common/PropertyBase.cpp \ + common/PropertyExceptions.cpp \ + common/PropertyID.cpp \ + common/PropertyVector.cpp \ + image/DraftItemType.cpp \ + image/DraftItem.cpp \ + common/properties/PointProperty.cpp \ + common/properties/ColorProperty.cpp \ + image/DraftItemVector.cpp \ + image/DraftItemCatalog.cpp \ + image/draftitems/BoxDraftItem.cpp \ + image/draftitems/LineDraftItem.cpp \ + image/draftitems/BoxDraftItemRenderer.cpp \ + image/draftitems/LineDraftItemRenderer.cpp \ + image/DraftItemRendererBase.cpp \ + common/properties/SizeProperty.cpp \ + ui/visscene/VisualNode.cpp \ + ui/image/ImageEditorNode.cpp \ + ui/image/DraftItemNode.cpp \ + ui/image/DraftItemNodeFactory.cpp \ + ui/image/ImageEditorTool.cpp \ + ui/image/tools/DraftItemTool.cpp \ + ui/image/tools/DefaultImageEditorTool.cpp \ + ui/image/tools/BoxDraftItemTool.cpp \ + ui/image/tools/LineDraftItemTool.cpp \ + ui/image/ImageEditorToolList.cpp \ + ui/image/draftitems/BoxDraftItemNode.cpp \ + ui/image/draftitems/LineDraftItemNode.cpp \ + ui/image/ImageEditorPropertyWidget.cpp \ + ui/property/editors/PointPropertyEditor.cpp \ + ui/property/editors/SizePropertyEditor.cpp \ + ui/widget/AutoFocusLineEdit.cpp \ + ui/widget/ColorWidget.cpp \ + ui/image/tools/ColorPickerTool.cpp \ + ui/widget/GrinderDockWidget.cpp \ + common/properties/AngleProperty.cpp \ + ui/property/editors/AnglePropertyEditor.cpp \ + ui/image/ImageEditorEnvironment.cpp \ + util/MathUtils.cpp \ + ui/image/ImageEditorWidget.cpp \ + ui/image/ImageEditor.cpp \ + ui/image/ImageEditorComponent.cpp \ + ui/image/InPlaceEditorDragHandle.cpp \ + ui/image/InPlaceEditor.cpp \ + ui/image/editors/LinearInPlaceEditor.cpp \ + common/PropertyObject.cpp \ + ui/image/editors/RectangularInPlaceEditor.cpp \ + ui/visscene/VisualSceneInputHandler.cpp \ + ui/image/ColorPresetsWidget.cpp + +HEADERS += \ + ui/mainwnd/GrinderWindow.h \ + core/GrinderApplication.h \ + Grinder.h \ + Version.h \ + util/StringUtils.h \ + util/StringConv.h \ + pipeline/PipelineItem.h \ + pipeline/PipelineExceptions.h \ + pipeline/Block.h \ + pipeline/BlockType.h \ + pipeline/Port.h \ + pipeline/PortType.h \ + pipeline/Connection.h \ + pipeline/ConnectionVector.h \ + pipeline/PortVector.h \ + pipeline/BlockVector.h \ + pipeline/Pipeline.h \ + pipeline/PipelineManager.h \ + pipeline/PipelineVector.h \ + ui/graph/GraphView.h \ + ui/graph/GraphScene.h \ + ui/graph/GraphStyle.h \ + ui/graph/GraphNode.h \ + ui/graph/GraphBlockNode.h \ + ui/graph/GraphNodeFactory.h \ + ui/graph/GraphPortNode.h \ + ui/graph/GraphConnectionNode.h \ + ui/graph/GraphConnectionBase.h \ + ui/graph/GraphConnectionBlueprint.h \ + ui/graph/GraphConnectionMessage.h \ + pipeline/BlockCatalog.h \ + pipeline/BlockCategory.h \ + util/UIUtils.h \ + ui/StyleSheet.h \ + controller/PipelineController.h \ + ui/graph/GraphBlockList.h \ + ui/graph/GraphWidget.h \ + common/ObjectVector.h \ + common/ObjectVector.impl.h \ + project/Label.h \ + project/LabelVector.h \ + project/Project.h \ + project/ProjectExceptions.h \ + controller/ProjectController.h \ + ui/widget/ControlBar.h \ + ui/mainwnd/LabelsListItem.h \ + ui/mainwnd/LabelsListWidget.h \ + controller/GenericController.h \ + controller/GenericController.impl.h \ + project/ImageReference.h \ + project/ImageReferenceVector.h \ + project/ProjectItem.h \ + ui/widget/ObjectListItem.h \ + ui/widget/ObjectListItem.impl.h \ + ui/widget/ObjectListWidget.h \ + ui/widget/ObjectListWidget.impl.h \ + ui/mainwnd/ImageReferencesListItem.h \ + ui/mainwnd/ImageReferencesListWidget.h \ + util/ImageUtils.h \ + util/FileUtils.h \ + util/TemporaryStatusMessage.h \ + engine/data/DataDescriptor.h \ + util/DataUtils.h \ + core/GrinderSettings.h \ + engine/data/DataBlob.h \ + engine/data/DataBlob.impl.h \ + util/CVUtils.h \ + util/CVUtils.impl.h \ + engine/data/DataExceptions.h \ + engine/data/DataDescriptor.impl.h \ + pipeline/BlockHierarchy.h \ + pipeline/blocks/BinaryThresholdBlock.h \ + engine/Engine.h \ + engine/EngineExceptions.h \ + controller/EngineController.h \ + engine/EngineExecutionContext.h \ + engine/Processor.h \ + engine/processors/BinaryThresholdProcessor.h \ + engine/Processor.impl.h \ + engine/ProcessorBase.h \ + pipeline/blocks/ConvertToGrayscaleBlock.h \ + engine/processors/ConvertToGrayscaleProcessor.h \ + ui/graph/GraphLayout.h \ + core/GrinderExceptions.h \ + project/serialization/SettingsContainer.h \ + project/serialization/SerializationExceptions.h \ + project/serialization/SettingsContainer.impl.h \ + project/serialization/SettingsCodec.h \ + project/serialization/JsonSettingsCodec.h \ + project/serialization/ProjectSerializer.h \ + project/serialization/SerializationContext.h \ + project/serialization/DeserializationContext.h \ + util/SerializationUtils.h \ + util/SerializationUtils.impl.h \ + common/MRUStringList.h \ + common/MRUStringList.impl.h \ + ui/mainwnd/RecentProjectsMenu.h \ + ui/dlg/OptionsDialog.h \ + util/StringUtils.impl.h \ + ui/property/PropertyTreeWidget.h \ + ui/property/PropertyTreeItem.h \ + ui/property/BlockPropertyTreeItem.h \ + ui/property/PropertyTreeItemDelegate.h \ + ui/property/ValuePropertyTreeItem.h \ + ui/property/PropertyEditor.h \ + ui/property/PropertyEditor.impl.h \ + ui/property/editors/BoolPropertyEditor.h \ + ui/property/editors/TextPropertyEditor.h \ + res/Resources.h \ + pipeline/blocks/OutputBlock.h \ + pipeline/blocks/InputBlock.h \ + image/ImageBuild.h \ + project/serialization/SerializationContext.impl.h \ + project/serialization/DeserializationContext.impl.h \ + image/ImageBuildVector.h \ + image/ImageBuildPool.h \ + image/ImageExceptions.h \ + engine/processors/InputProcessor.h \ + engine/processors/OutputProcessor.h \ + ui/image/ImageEditorManager.h \ + ui/image/ImageEditorDockWidget.h \ + common/AdjacentRange.h \ + ui/visscene/VisualScene.h \ + ui/visscene/VisualScene.impl.h \ + ui/visscene/VisualSceneView.h \ + ui/visscene/VisualSceneStyle.h \ + ui/widget/MetaWidget.h \ + ui/widget/MetaWidget.impl.h \ + ui/image/ImageEditorView.h \ + ui/image/ImageEditorScene.h \ + controller/ImageEditorController.h \ + ui/image/ImageEditorStyle.h \ + image/ImageBuildItem.h \ + image/Layer.h \ + image/LayerVector.h \ + ui/image/LayersListWidget.h \ + ui/image/LayersListItem.h \ + common/properties/BoolProperty.h \ + common/properties/IntProperty.h \ + common/properties/RangeConstraint.h \ + common/properties/RangeConstraint.impl.h \ + common/properties/RealProperty.h \ + common/properties/StandardProperties.h \ + common/properties/StringProperty.h \ + common/properties/UIntProperty.h \ + common/Property.h \ + common/Property.impl.h \ + common/PropertyBase.h \ + common/PropertyConstraint.h \ + common/PropertyConstraint.impl.h \ + common/PropertyExceptions.h \ + common/PropertyID.h \ + common/PropertyVector.h \ + common/PropertyVector.impl.h \ + image/DraftItemType.h \ + image/DraftItem.h \ + common/properties/PointProperty.h \ + common/properties/ColorProperty.h \ + image/DraftItemVector.h \ + image/DraftItemCatalog.h \ + image/draftitems/BoxDraftItem.h \ + image/draftitems/LineDraftItem.h \ + image/DraftItemRenderer.h \ + image/draftitems/BoxDraftItemRenderer.h \ + image/draftitems/LineDraftItemRenderer.h \ + image/DraftItemRenderer.impl.h \ + image/DraftItemRendererBase.h \ + common/properties/SizeProperty.h \ + ui/visscene/VisualNode.h \ + ui/image/ImageEditorNode.h \ + ui/visscene/VisualNodeFactory.h \ + ui/visscene/VisualNodeFactory.impl.h \ + ui/image/DraftItemNode.h \ + ui/image/DraftItemNodeFactory.h \ + ui/image/ImageEditorScene.impl.h \ + ui/image/ImageEditorTool.h \ + ui/image/tools/DraftItemTool.h \ + ui/image/tools/DefaultImageEditorTool.h \ + ui/image/tools/BoxDraftItemTool.h \ + ui/image/tools/LineDraftItemTool.h \ + ui/image/ImageEditorToolList.h \ + ui/image/ImageEditorToolList.impl.h \ + ui/image/draftitems/BoxDraftItemNode.h \ + ui/image/draftitems/LineDraftItemNode.h \ + ui/image/ImageEditorPropertyWidget.h \ + ui/property/editors/DualTextPropertyEditor.h \ + ui/property/editors/PointPropertyEditor.h \ + ui/property/editors/DualTextPropertyEditor.impl.h \ + ui/property/editors/SizePropertyEditor.h \ + ui/widget/AutoFocusLineEdit.h \ + ui/widget/ColorWidget.h \ + ui/image/tools/ColorPickerTool.h \ + ui/widget/GrinderDockWidget.h \ + common/properties/AngleProperty.h \ + ui/property/editors/AnglePropertyEditor.h \ + ui/image/ImageEditorEnvironment.h \ + util/MathUtils.h \ + ui/image/ImageEditorWidget.h \ + ui/image/ImageEditor.h \ + ui/image/ImageEditorComponent.h \ + ui/image/InPlaceEditorDragHandle.h \ + ui/image/InPlaceEditor.h \ + ui/image/editors/LinearInPlaceEditor.h \ + common/PropertyObject.h \ + common/PropertyObject.impl.h \ + ui/image/editors/RectangularInPlaceEditor.h \ + ui/visscene/VisualSceneInputHandler.h \ + ui/visscene/VisualSceneInputHandler.impl.h \ + ui/image/ColorPresetsWidget.h + +FORMS += \ + ui/mainwnd/GrinderWindow.ui \ + ui/dlg/OptionsDialog.ui \ + ui/image/ImageEditorWidget.ui + +RESOURCES += \ + res/Grinder.qrc + +OTHER_FILES += \ + res/Grinder.rc diff --git a/Grinder/Libraries.pro b/Grinder/Libraries.pro new file mode 100644 index 0000000000000000000000000000000000000000..5bd9facc4913bdc2a53194bce421ee440387b181 --- /dev/null +++ b/Grinder/Libraries.pro @@ -0,0 +1,15 @@ +# Paths + +win32 { + INCLUDEPATH += F:\Lib\OpenCV\build-Release\install\include + DEPENDPATH += F:\Lib\OpenCV\build-Release\install\x86\mingw\lib + LIBS += -LF:\Lib\OpenCV\build-Release\install\x86\mingw\lib +} + +# OpenCV + +LIBS += libopencv_core340 +LIBS += libopencv_imgcodecs340 +LIBS += libopencv_imgproc340 +LIBS += libopencv_features2d340 +LIBS += libopencv_highgui340 diff --git a/Grinder/Version.cpp b/Grinder/Version.cpp new file mode 100644 index 0000000000000000000000000000000000000000..a05849f9c49b6e4c16527e94caf034a3bd9b3f43 --- /dev/null +++ b/Grinder/Version.cpp @@ -0,0 +1,15 @@ +/****************************************************************************** + * File: Version.cpp + * Date: 11.1.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "Version.h" + +QString grndr::GetVersionString(bool includeBuild) +{ + if (includeBuild) + return QString{"%1.%2.%3.%4"}.arg(GRNDR_VERSION_MAJOR).arg(GRNDR_VERSION_MINOR).arg(GRNDR_VERSION_REVISION).arg(GRNDR_VERSION_BUILD); + else + return QString{"%1.%2.%3"}.arg(GRNDR_VERSION_MAJOR).arg(GRNDR_VERSION_MINOR).arg(GRNDR_VERSION_REVISION); +} diff --git a/Grinder/Version.h b/Grinder/Version.h new file mode 100644 index 0000000000000000000000000000000000000000..1ebf1c94cd8dd20b8e8c5f0228691d4b3bab65cf --- /dev/null +++ b/Grinder/Version.h @@ -0,0 +1,27 @@ +/****************************************************************************** + * File: Version.cpp + * Date: 11.1.2018 + *****************************************************************************/ + +#ifndef VERSION_H +#define VERSION_H + +#include <QString> + +#define GRNDR_INFO_TITLE "Grinder" +#define GRNDR_INFO_COPYRIGHT "Copyright (c) WWU Muenster" +#define GRNDR_INFO_DATE "03.04.2018" +#define GRNDR_INFO_COMPANY "WWU Muenster" +#define GRNDR_INFO_WEBSITE "http://www.uni-muenster.de" + +#define GRNDR_VERSION_MAJOR 0 +#define GRNDR_VERSION_MINOR 2 +#define GRNDR_VERSION_REVISION 0 +#define GRNDR_VERSION_BUILD 125 + +namespace grndr +{ + QString GetVersionString(bool includeBuild = false); +} + +#endif diff --git a/Grinder/common/AdjacentRange.h b/Grinder/common/AdjacentRange.h new file mode 100644 index 0000000000000000000000000000000000000000..0a80c21de3926c03d40d66d2956b09c533ed38dc --- /dev/null +++ b/Grinder/common/AdjacentRange.h @@ -0,0 +1,73 @@ +/****************************************************************************** + * File: AdjacentRange.h + * Date: 13.3.2018 + *****************************************************************************/ + +#ifndef ADJACENTRANGE_H +#define ADJACENTRANGE_H + +#include <iterator> +#include <utility> + +namespace grndr +{ + template <typename FwdIt> + class adjacent_iterator + { + public: + adjacent_iterator(FwdIt first, FwdIt last) : m_first(first), m_next(first == last ? first : std::next(first)) { } + + bool operator !=(const adjacent_iterator& other) const + { + return m_next != other.m_next; + } + + adjacent_iterator& operator++() + { + ++m_first; + ++m_next; + return *this; + } + + using Ref = typename std::iterator_traits<FwdIt>::reference; + using Pair = std::pair<Ref, Ref>; + + Pair operator*() const + { + return Pair(*m_first, *m_next); + } + + private: + FwdIt m_first; + FwdIt m_next; + }; + + template <typename FwdIt> + class adjacent_range + { + public: + adjacent_range(FwdIt first, FwdIt last) : m_first(first), m_last(last) { } + + adjacent_iterator<FwdIt> begin() const + { + return adjacent_iterator<FwdIt>(m_first, m_last); + } + + adjacent_iterator<FwdIt> end() const + { + return adjacent_iterator<FwdIt>(m_last, m_last); + } + + private: + FwdIt m_first; + FwdIt m_last; + }; + + template <typename C> + auto make_adjacent_range(C& c) -> adjacent_range<decltype(c.begin())> + { + return adjacent_range<decltype(c.begin())>(c.begin(), c.end()); + } +} + +#endif diff --git a/Grinder/common/MRUStringList.cpp b/Grinder/common/MRUStringList.cpp new file mode 100644 index 0000000000000000000000000000000000000000..e2d2d2c9b85cef281e2aff78c2c644db369bfe48 --- /dev/null +++ b/Grinder/common/MRUStringList.cpp @@ -0,0 +1,50 @@ +/****************************************************************************** + * File: MRUStringList.cpp + * Date: 01.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "MRUStringList.h" + +MRUStringList::MRUStringList(bool caseSensitive, bool pathList, unsigned int entryLimit) : + _isCaseSensitive{caseSensitive}, _isPathList{pathList}, _entryLimit{entryLimit} +{ + +} + +void MRUStringList::add(QString entry) +{ + auto it = find(entry); + + if (it != _entries.begin() || empty()) // The entry is not at the front of the list + { + // Remove the item if it already exists + if (it != _entries.end()) + _entries.erase(it); + + _entries.push_front(entry); + purgeList(); + } +} + +void MRUStringList::remove(QString entry) +{ + std::remove(_entries.begin(), _entries.end(), entry); +} + +QStringList::const_iterator MRUStringList::find(QString entry) const +{ + // Remove constness to be able to call find; safe since we will return a const_iterator + MRUStringList* list = const_cast<MRUStringList*>(this); + return list->find(entry, _entries.cbegin(), _entries.cend()); +} + +void MRUStringList::purgeList() +{ + // Remove excessive entries + if (_entryLimit > 0) + { + while (_entries.size() > static_cast<int>(_entryLimit)) + _entries.pop_back(); + } +} diff --git a/Grinder/common/MRUStringList.h b/Grinder/common/MRUStringList.h new file mode 100644 index 0000000000000000000000000000000000000000..ac472aed03da7787d63629e058c4c6d89baee605 --- /dev/null +++ b/Grinder/common/MRUStringList.h @@ -0,0 +1,73 @@ +/****************************************************************************** + * File: MRUStringList.h + * Date: 01.3.2018 + *****************************************************************************/ + +#ifndef MRUSTRINGLIST_H +#define MRUSTRINGLIST_H + +#include <QStringList> + +namespace grndr +{ + class MRUStringList + { + public: + MRUStringList(bool caseSensitive = true, bool pathList = false, unsigned int entryLimit = 0); + MRUStringList(const MRUStringList& otherList) = default; + MRUStringList(MRUStringList&& otherList) = default; + MRUStringList(const QStringList& otherList) { _entries = otherList; purgeList(); } + MRUStringList(QStringList&& otherList) { _entries = std::move(otherList); purgeList(); } + + MRUStringList& operator =(const MRUStringList& otherList) = default; + MRUStringList& operator =(MRUStringList&& otherList) = default; + MRUStringList& operator =(const QStringList& otherList) { _entries = otherList; purgeList(); return *this; } + MRUStringList& operator =(QStringList&& otherList) { _entries = std::move(otherList); purgeList(); return *this; } + + operator QStringList() const { return _entries; } + + public: + void add(QString entry); + void remove(QString entry); + + void operator +=(QString entry) { add(entry); } + void operator -=(QString entry) { remove(entry); } + + public: + auto at(int index) { return _entries.at(index); } + bool contains(QString entry) const { return find(entry) != _entries.cend(); } + + auto size() { return _entries.size(); } + auto empty() { return _entries.isEmpty(); } + + auto begin() { return _entries.begin(); } + auto cbegin() const { return _entries.cbegin(); } + auto end() { return _entries.end(); } + auto cend() { return _entries.cend(); } + + public: + void setCaseSensitive(bool caseSensitive = true) { _isCaseSensitive = caseSensitive; } + void setPathList(bool pathList = true) { _isPathList = pathList; } + void setEntryLimit(unsigned int limit) { _entryLimit = limit; purgeList(); } + + private: + QStringList::iterator find(QString entry) { return find(entry, _entries.begin(), _entries.end()); } + QStringList::const_iterator find(QString entry) const; + template<typename ContainerType> + ContainerType find(QString entry, ContainerType begin, ContainerType end); + + void purgeList(); + + private: + QStringList _entries; + + bool _isCaseSensitive{true}; + bool _isPathList{false}; + + unsigned int _entryLimit{0}; + }; +} + +#include "MRUStringList.impl.h" + +#endif diff --git a/Grinder/common/MRUStringList.impl.h b/Grinder/common/MRUStringList.impl.h new file mode 100644 index 0000000000000000000000000000000000000000..c43c66411cb810b1932a86b8b17ca827b1696820 --- /dev/null +++ b/Grinder/common/MRUStringList.impl.h @@ -0,0 +1,28 @@ +/****************************************************************************** + * File: MRUStringList.impl.h + * Date: 01.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "MRUStringList.h" + +template<typename ContainerType> +ContainerType MRUStringList::find(QString entry, ContainerType begin, ContainerType end) +{ + // When comparing paths, convert backslashes to slashes first + if (_isPathList) + entry.replace('\\', '/'); + + for (auto it = begin; it != end; ++it) + { + QString listEntry = *it; + + if (_isPathList) + listEntry.replace('\\', '/'); + + if (entry.compare(listEntry, _isCaseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive) == 0) + return it; + } + + return end; +} diff --git a/Grinder/common/ObjectVector.h b/Grinder/common/ObjectVector.h new file mode 100644 index 0000000000000000000000000000000000000000..339f31bb8ece302fd4fcab0f863cd94c758bfe06 --- /dev/null +++ b/Grinder/common/ObjectVector.h @@ -0,0 +1,61 @@ +/****************************************************************************** + * File: ObjectVector.h + * Date: 14.1.2018 + *****************************************************************************/ + +#ifndef OBJECTVECTOR_H +#define OBJECTVECTOR_H + +#include <vector> +#include <memory> + +#include "project/serialization/SerializationContext.h" +#include "project/serialization/DeserializationContext.h" + +namespace grndr +{ + template<typename ObjType> + class ObjectVector : public std::vector<std::shared_ptr<ObjType>> + { + public: + using vector_type = ObjectVector<ObjType>; + using object_type = ObjType; + using object_vec_type = std::vector<ObjType>; + using pointer_type = std::shared_ptr<ObjType>; + using pointer_vec_type = std::vector<pointer_type>; + using size_type = typename std::vector<std::shared_ptr<ObjType>>::size_type; + + public: + ObjectVector() { } + ObjectVector(const vector_type& src) = default; + ObjectVector(vector_type&& src) = default; + + vector_type& operator =(const vector_type& src) = default; + vector_type& operator =(vector_type&& src) = default; + + template<typename T = vector_type> + void deepCopy(const std::enable_if_t<std::is_copy_constructible<ObjType>::value, T>& src); + + public: + pointer_type selectFirst(std::function<bool(const pointer_type&)> pred) const; + pointer_vec_type select(std::function<bool(const pointer_type&)> pred) const; + + auto find(const pointer_type& obj) const; + auto find(const object_type* obj) const; + auto find(std::function<bool(const pointer_type&)> pred) const; + auto find(std::function<bool(const object_type*)> pred) const; + + auto indexOf(const pointer_type& obj) const; + auto indexOf(const ObjType* obj) const; + bool contains(const pointer_type& obj) const { return indexOf(obj) != -1; } + bool contains(const ObjType* obj) const { return indexOf(obj) != -1; } + + public: + void serialize(QString elemName, SerializationContext& ctx, std::function<bool(const pointer_type&)> predicate = nullptr) const; + void deserialize(QString elemName, DeserializationContext& ctx, std::function<pointer_type(const SettingsContainer&)> objCreator); + }; +} + +#include "ObjectVector.impl.h" + +#endif diff --git a/Grinder/common/ObjectVector.impl.h b/Grinder/common/ObjectVector.impl.h new file mode 100644 index 0000000000000000000000000000000000000000..a99334190043cd0524547b4098fef8fd59db3f19 --- /dev/null +++ b/Grinder/common/ObjectVector.impl.h @@ -0,0 +1,100 @@ +/****************************************************************************** + * File: ObjectVector.impl.h + * Date: 14.1.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ObjectVector.h" +#include "util/SerializationUtils.h" + +#include <algorithm> + +template<typename ObjType> +template<typename T> +void ObjectVector<ObjType>::deepCopy(const std::enable_if_t<std::is_copy_constructible<ObjType>::value, T>& src) +{ + this->clear(); + + for (const auto& elem : src) + { + if (elem.get()) + { + ObjType* obj = new ObjType{*elem.get()}; + this->emplace_back(obj); + } + else + this->emplace_back(nullptr); + } +} + +template<typename ObjType> +typename ObjectVector<ObjType>::pointer_type ObjectVector<ObjType>::selectFirst(std::function<bool(const pointer_type&)> pred) const +{ + auto it = std::find_if(this->cbegin(), this->cend(), pred); + + if (it != this->cend()) + return *it; + + return pointer_type{}; +} + +template<typename ObjType> +typename ObjectVector<ObjType>::pointer_vec_type ObjectVector<ObjType>::select(std::function<bool(const pointer_type&)> pred) const +{ + pointer_vec_type objects; + std::copy_if(this->cbegin(), this->cend(), std::back_inserter(objects), pred); + return objects; +} + +template<typename ObjType> +auto ObjectVector<ObjType>::find(const pointer_type& obj) const +{ + return std::find(this->cbegin(), this->cend(), obj); +} + +template<typename ObjType> +auto ObjectVector<ObjType>::find(const object_type* obj) const +{ + return std::find_if(this->cbegin(), this->cend(), [&obj](auto object) { return object.get() == obj; }); +} + +template<typename ObjType> +auto ObjectVector<ObjType>::find(std::function<bool(const pointer_type&)> pred) const +{ + return std::find_if(this->cbegin(), this->cend(), pred); +} + +template<typename ObjType> +auto ObjectVector<ObjType>::find(std::function<bool(const object_type*)> pred) const +{ + return std::find_if(this->cbegin(), this->cend(), [&pred](auto object) { return pred(object.get()); }); +} + +template<typename ObjType> +auto ObjectVector<ObjType>::indexOf(const ObjType* obj) const +{ + auto it = find(obj); + + if (it == this->cend()) + return -1; + else + return it - this->cbegin(); +} + +template<typename ObjType> +auto ObjectVector<ObjType>::indexOf(const pointer_type& obj) const +{ + return find(obj.get()); +} + +template<typename ObjType> +void ObjectVector<ObjType>::serialize(QString elemName, SerializationContext& ctx, std::function<bool(const pointer_type& object)> predicate) const +{ + SerializationUtils::serializeContainer(*this, elemName, ctx, predicate); +} + +template<typename ObjType> +void ObjectVector<ObjType>::deserialize(QString elemName, DeserializationContext& ctx, std::function<pointer_type(const SettingsContainer&)> objCreator) +{ + SerializationUtils::deserializeContainer<vector_type>(elemName, ctx, objCreator); +} diff --git a/Grinder/common/Property.h b/Grinder/common/Property.h new file mode 100644 index 0000000000000000000000000000000000000000..6d1460b37c68c0387d616a816f7179e209336a1a --- /dev/null +++ b/Grinder/common/Property.h @@ -0,0 +1,77 @@ +/****************************************************************************** + * File: Property.h + * Date: 12.1.2018 + *****************************************************************************/ + +#ifndef PROPERTY_H +#define PROPERTY_H + +#include <memory> + +#include "PropertyBase.h" +#include "PropertyConstraint.h" +#include "common/ObjectVector.h" + +namespace grndr +{ + template<typename ValType> + class Property : public PropertyBase + { + public: + using property_type = Property<ValType>; + using value_type = ValType; + + public: + Property(PropertyID id, QString name, ValType defValue = ValType(), Flags flags = Flag::None); + Property(const property_type& src) = default; + Property(property_type&& src) = default; + Property(const ValType& val); + Property(ValType&& val); + + Property& operator =(const property_type& src) = default; + Property& operator =(property_type&& src) = default; + Property& operator =(const ValType& val); + Property& operator =(ValType&& val); + + operator const ValType&() const { return _value; } + + bool operator ==(const property_type& other) const; + bool operator ==(const ValType& val) const; + + public: + const ValType& getValue() const { return _value; } + void setValue(const ValType& val); + void setValue(ValType&& val); + + public: + template<template<typename> class ConstraintType, typename... Args> + void createConstraint(Args... args); + void removeConstraint(const PropertyConstraints::value_type& constraint); + + const PropertyConstraints& constraints() { return _constraints; } + + public: + virtual void copyValue(const PropertyBase* property) override; + + virtual QString toString() const override; + virtual void fromString(const QString& data) override; + + virtual void serialize(SerializationContext& ctx) const override; + virtual void deserialize(DeserializationContext& ctx) override; + + private: + template<typename V, typename Setter> + void _setValue(V val, Setter setter); + + void applyConstraints(); + + protected: + value_type _value{}; + + PropertyConstraints _constraints; + }; +} + +#include "Property.impl.h" + +#endif diff --git a/Grinder/common/Property.impl.h b/Grinder/common/Property.impl.h new file mode 100644 index 0000000000000000000000000000000000000000..2ab9818bbd06862a94ce4bc1cdbfbfaad233e024 --- /dev/null +++ b/Grinder/common/Property.impl.h @@ -0,0 +1,155 @@ +/****************************************************************************** + * File: Property.impl.h + * Date: 12.1.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "Property.h" +#include "PropertyExceptions.h" +#include "util/StringUtils.h" +#include "util/StringConv.h" + +template<typename ValType> +Property<ValType>::Property(PropertyID id, QString name, ValType defValue, Flags flags) : PropertyBase(id, name, flags), + _value{defValue} +{ + +} + +template<typename ValType> +Property<ValType>::Property(const ValType& val) : PropertyBase(val), + _value{val} +{ + +} + +template<typename ValType> +Property<ValType>::Property(ValType&& val) : PropertyBase(val), + _value{std::move(val)} +{ + +} + +template<typename ValType> +Property<ValType>& Property<ValType>::operator =(const ValType& val) +{ + setValue(val); + return *this; +} + +template<typename ValType> +Property<ValType>& Property<ValType>::operator =(ValType&& val) +{ + setValue(val); + return *this; +} + +template<typename ValType> +bool Property<ValType>::operator ==(const property_type& other) const +{ + return _value == other._value; +} + +template<typename ValType> +bool Property<ValType>::operator ==(const ValType& val) const +{ + return _value == val; +} + +template<typename ValType> +void Property<ValType>::setValue(const ValType& val) +{ + _setValue(val, [&val, this]() { _value = val; }); +} + +template<typename ValType> +void Property<ValType>::setValue(ValType&& val) +{ + _setValue(val, [&val, this]() { _value = std::move(val); }); +} + +template<typename ValType> +template<template<typename> class ConstraintType, typename... Args> +void Property<ValType>::createConstraint(Args... args) +{ + _constraints.emplace_back(new ConstraintType<value_type>(this, std::forward<Args>(args)...)); +} + +template<typename ValType> +void Property<ValType>::removeConstraint(const PropertyConstraints::value_type& constraint) +{ + std::remove(_constraints.cbegin(), _constraints.cend(), constraint); +} + +template<typename ValType> +void Property<ValType>::copyValue(const PropertyBase* property) +{ + if (auto typedProperty = dynamic_cast<const property_type*>(property)) + setValue(typedProperty->_value); +} + +template<typename ValType> +QString Property<ValType>::toString() const +{ + return StringUtils::encodeEscapeCharacters(StringConv::convertValue(getValue())); +} + +template<typename ValType> +void Property<ValType>::fromString(const QString& data) +{ + bool ok = false; + setValue(StringConv::convertString<ValType>(StringUtils::decodeEscapeCharacters(data), &ok)); + + if (!ok) + throw PropertyException{this, _EXCPT(QString{"Invalid string data passed ('%1')"}.arg(data))}; +} + +template<typename ValType> +void Property<ValType>::serialize(SerializationContext& ctx) const +{ + PropertyBase::serialize(ctx); + + // Serialize values + if (!hasFlag(Flag::ReadOnly)) + ctx.settings()[Serialization_Value_Value] = toString(); +} + +template<typename ValType> +void Property<ValType>::deserialize(DeserializationContext& ctx) +{ + PropertyBase::deserialize(ctx); + + // Deserialize values + if (!hasFlag(Flag::ReadOnly)) + fromString(ctx.settings()[Serialization_Value_Value].toString()); +} + +template<typename ValType> +template<typename V, typename Setter> +void Property<ValType>::_setValue(V val, Setter setter) +{ + if (hasFlag(Flag::ReadOnly)) + throw PropertyException{this, _EXCPT("Trying to modify a read-only property")}; + + if (_value != val) + { + auto oldValue = _value; + + setter(); + applyConstraints(); + + if (_value != oldValue) + emit valueChanged(); + } +} + +template<typename ValType> +void Property<ValType>::applyConstraints() +{ + for (const auto& constraint : _constraints) + { + // Cast the generic constraint to a constraint of the same value type as this property + if (auto valuedConstraint = dynamic_cast<PropertyConstraint<value_type>*>(constraint.get())) + valuedConstraint->applyConstraint(_value); // Constraints will modify the passed value if needed + } +} diff --git a/Grinder/common/PropertyBase.cpp b/Grinder/common/PropertyBase.cpp new file mode 100644 index 0000000000000000000000000000000000000000..39294292582bb1c0f6be9fe0ec0c2a0a82018300 --- /dev/null +++ b/Grinder/common/PropertyBase.cpp @@ -0,0 +1,31 @@ +/****************************************************************************** + * File: PropertyBase.cpp + * Date: 13.1.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "PropertyBase.h" + +const char* PropertyBase::Serialization_Value_ID = "ID"; +const char* PropertyBase::Serialization_Value_Name = "Name"; +const char* PropertyBase::Serialization_Value_Value = "Value"; + +PropertyBase::PropertyBase(PropertyID id, QString name, Flags flags) : + _id{id}, _name{name}, _flags{flags} +{ + +} + +void PropertyBase::serialize(SerializationContext& ctx) const +{ + // Serialize values + ctx.settings()[Serialization_Value_ID] = _id; + ctx.settings()[Serialization_Value_Name] = _name; +} + +void PropertyBase::deserialize(DeserializationContext& ctx) +{ + // Deserialize values + _id = ctx.settings()[Serialization_Value_ID].toString(); + _name = ctx.settings()[Serialization_Value_Name].toString(); +} diff --git a/Grinder/common/PropertyBase.h b/Grinder/common/PropertyBase.h new file mode 100644 index 0000000000000000000000000000000000000000..a0de5a065869bdcac1e59bb5524a47520d4ac449 --- /dev/null +++ b/Grinder/common/PropertyBase.h @@ -0,0 +1,81 @@ +/****************************************************************************** + * File: PropertyBase.h + * Date: 13.1.2018 + *****************************************************************************/ + +#ifndef PROPERTYBASE_H +#define PROPERTYBASE_H + +#include "PropertyID.h" +#include "project/serialization/SerializationContext.h" +#include "project/serialization/DeserializationContext.h" + +namespace grndr +{ + class PropertyBase : public QObject + { + Q_OBJECT + + public: + static const char* Serialization_Value_ID; + static const char* Serialization_Value_Name; + static const char* Serialization_Value_Value; + + enum class Flag : unsigned int + { + None = 0x0000, + ReadOnly = 0x0001, + Hidden = 0x0002, + }; + + Q_DECLARE_FLAGS(Flags, Flag) + + public: + PropertyBase(PropertyID id, QString name, Flags flags = Flag::None); + PropertyBase(const PropertyBase& src) = default; + PropertyBase(PropertyBase&& src) = default; + + PropertyBase& operator =(const PropertyBase& src) = default; + PropertyBase& operator =(PropertyBase&& src) = default; + + public: + PropertyID getID() const { return _id; } + void setID(PropertyID id) { _id = id; } + + QString getName() const { return _name; } + void setName(QString name) { _name = name; } + QString getDescription() const { return _description; } + void setDescription(QString desc) { _description = desc; } + + bool hasFlag(Flag flag) const { return _flags.testFlag(flag); } + void setFlag(Flag flag) { _flags |= flag; } + Flags getFlags() const { return _flags; } + void setFlags(Flags flags) { _flags = flags; } + + public: + virtual QWidget* createEditor(QWidget* parent = nullptr) { Q_UNUSED(parent); return nullptr; } + + public: + virtual void copyValue(const PropertyBase* property) = 0; + + virtual QString toString() const = 0; + virtual void fromString(const QString& data) = 0; + + virtual void serialize(SerializationContext& ctx) const; + virtual void deserialize(DeserializationContext& ctx); + + signals: + void valueChanged(); + + protected: + PropertyID _id{PropertyID::None}; + QString _name{""}; + QString _description{""}; + + Flags _flags{Flag::None}; + }; +} + +Q_DECLARE_OPERATORS_FOR_FLAGS(grndr::PropertyBase::Flags) + +#endif diff --git a/Grinder/common/PropertyConstraint.h b/Grinder/common/PropertyConstraint.h new file mode 100644 index 0000000000000000000000000000000000000000..1f618f34464036d6d4b24f61ad3424063312725c --- /dev/null +++ b/Grinder/common/PropertyConstraint.h @@ -0,0 +1,43 @@ +/****************************************************************************** + * File: PropertyConstraint.h + * Date: 05.3.2018 + *****************************************************************************/ + +#ifndef PROPERTYCONSTRAINT_H +#define PROPERTYCONSTRAINT_H + +#include <vector> +#include <memory> + +namespace grndr +{ + template<typename> class Property; + + class PropertyConstraintBase + { + private: + virtual void _makePolymorphic() { } // Needed to make this class polymorphic + }; + + template<typename ValType> + class PropertyConstraint : public PropertyConstraintBase + { + public: + using value_type = ValType; + + public: + PropertyConstraint(Property<value_type>* property); + + public: + virtual void applyConstraint(value_type& value) = 0; + + protected: + Property<value_type>* _property{nullptr}; + }; + + using PropertyConstraints = std::vector<std::unique_ptr<PropertyConstraintBase>>; +} + +#include "PropertyConstraint.impl.h" + +#endif diff --git a/Grinder/common/PropertyConstraint.impl.h b/Grinder/common/PropertyConstraint.impl.h new file mode 100644 index 0000000000000000000000000000000000000000..30d04f4a96bdcbfbe455c5b3960b3c3d99f9d5d1 --- /dev/null +++ b/Grinder/common/PropertyConstraint.impl.h @@ -0,0 +1,15 @@ +/****************************************************************************** + * File: PropertyConstraint.impl.h + * Date: 05.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "PropertyConstraint.h" + +template<typename ValType> +PropertyConstraint<ValType>::PropertyConstraint(Property<value_type>* property) : + _property{property} +{ + if (!property) + throw std::invalid_argument{_EXCPT("property may not be null")}; +} diff --git a/Grinder/common/PropertyExceptions.cpp b/Grinder/common/PropertyExceptions.cpp new file mode 100644 index 0000000000000000000000000000000000000000..0477320dd96adad4cd090f29f8f691f1bc62a7e8 --- /dev/null +++ b/Grinder/common/PropertyExceptions.cpp @@ -0,0 +1,19 @@ +/****************************************************************************** + * File: PropertyExceptions.cpp + * Date: 14.1.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "PropertyExceptions.h" + +PropertyException::PropertyException(const PropertyBase* prop, QString what) : GrinderException(what), + _property{prop} +{ + +} + +PropertyItemException::PropertyItemException(const PropertyObject* item, QString what) : GrinderException(what), + _propertyItem{item} +{ + +} diff --git a/Grinder/common/PropertyExceptions.h b/Grinder/common/PropertyExceptions.h new file mode 100644 index 0000000000000000000000000000000000000000..09ccfba02e1624ffc9341731389b8cd43949596c --- /dev/null +++ b/Grinder/common/PropertyExceptions.h @@ -0,0 +1,43 @@ +/****************************************************************************** + * File: PropertyExceptions.h + * Date: 14.1.2018 + *****************************************************************************/ + +#ifndef PROPERTYEXCEPTIONS_H +#define PROPERTYEXCEPTIONS_H + +#include <QString> + +#include "core/GrinderExceptions.h" + +namespace grndr +{ + class PropertyBase; + class PropertyObject; + + class PropertyException : public GrinderException + { + public: + PropertyException(const PropertyBase* prop, QString what); + + public: + const PropertyBase* property() const { return _property; } + + protected: + const PropertyBase* _property{nullptr}; + }; + + class PropertyItemException : public GrinderException + { + public: + PropertyItemException(const PropertyObject* item, QString what); + + public: + const PropertyObject* propertyItem() const { return _propertyItem; } + + protected: + const PropertyObject* _propertyItem{nullptr}; + }; +} + +#endif diff --git a/Grinder/common/PropertyID.cpp b/Grinder/common/PropertyID.cpp new file mode 100644 index 0000000000000000000000000000000000000000..e46f54f6c684f595aa0effa5cc1da1100accefc8 --- /dev/null +++ b/Grinder/common/PropertyID.cpp @@ -0,0 +1,22 @@ +/****************************************************************************** + * File: PropertyID.cpp + * Date: 13.1.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "PropertyID.h" + +const char* PropertyID::None = ""; +const char* PropertyID::Default = "Default"; + +const char* PropertyID::PrimaryColor = "PrimaryColor"; +const char* PropertyID::Position = "Position"; +const char* PropertyID::EndPosition = "EndPosition"; +const char* PropertyID::Size = "Size"; +const char* PropertyID::LineWidth = "LineWidth"; +const char* PropertyID::HasDirection = "HasDirection"; +const char* PropertyID::Direction = "Direction"; + +const char* PropertyID::Threshold = "Threshold"; +const char* PropertyID::TargetValue = "TargetValue"; +const char* PropertyID::Invert = "Invert"; diff --git a/Grinder/common/PropertyID.h b/Grinder/common/PropertyID.h new file mode 100644 index 0000000000000000000000000000000000000000..71abe80ce41f043322c79fc5014131cc3ed0343b --- /dev/null +++ b/Grinder/common/PropertyID.h @@ -0,0 +1,45 @@ +/****************************************************************************** + * File: PropertyID.h + * Date: 12.1.2018 + *****************************************************************************/ + +#ifndef PROPERTYID_H +#define PROPERTYID_H + +#include <QString> + +namespace grndr +{ + class PropertyID final : public QString + { + public: + static const char* None; /* Invalid property */ + static const char* Default; + + static const char* PrimaryColor; + static const char* Position; + static const char* EndPosition; + static const char* Size; + static const char* LineWidth; + static const char* HasDirection; + static const char* Direction; + + static const char* Threshold; + static const char* TargetValue; + static const char* Invert; + + public: + using QString::QString; + + PropertyID() = default; + PropertyID(const PropertyID& other) = default; + PropertyID(PropertyID&& other) = default; + PropertyID(const QString& str) { *static_cast<QString*>(this) = str; } + + PropertyID& operator =(const PropertyID& other) = default; + PropertyID& operator =(PropertyID&& other) = default; + PropertyID& operator =(const QString& str) { *static_cast<QString*>(this) = str; return *this; } + }; +} + +#endif diff --git a/Grinder/common/PropertyObject.cpp b/Grinder/common/PropertyObject.cpp new file mode 100644 index 0000000000000000000000000000000000000000..c912f32d884d72205a7c6203b2fffd1ff9000077 --- /dev/null +++ b/Grinder/common/PropertyObject.cpp @@ -0,0 +1,63 @@ +/****************************************************************************** + * File: PropertyObject.cpp + * Date: 20.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "PropertyObject.h" +#include "PropertyExceptions.h" + +void PropertyObject::serialize(SerializationContext& ctx) const +{ + // Serialize all properties + ctx.beginGroup(PropertyVector::Serialization_Group, true); + _properties.serialize(PropertyVector::Serialization_Element, ctx, [](const std::shared_ptr<PropertyBase> property) { + return !property->hasFlag(PropertyBase::Flag::ReadOnly); + }); + ctx.endGroup(); +} + +void PropertyObject::deserialize(DeserializationContext& ctx) +{ + // Deserialize all properties + if (ctx.beginGroup(PropertyVector::Serialization_Group)) + { + _properties.deserialize(PropertyVector::Serialization_Element, ctx, [this](const SettingsContainer& settings) -> std::shared_ptr<PropertyBase> { + PropertyID id = settings[PropertyBase::Serialization_Value_ID].toString(); + auto property = _properties.selectByID(id); + + if (property && !property->hasFlag(PropertyBase::Flag::ReadOnly)) + return property; + else + return nullptr; + }); + + ctx.endGroup(); + } +} + +void PropertyObject::removeProperty(PropertyID id) +{ + if (id != PropertyID::None) + { + auto prop = _properties.selectByID(id); + + if (prop) + removeProperty(prop.get()); + else + throw PropertyItemException{this, _EXCPT(QString{"Tried to remove a non-existing property (ID: %1)"}.arg(id))}; + } +} + +void PropertyObject::removeProperty(const PropertyBase* prop) +{ + if (prop) + { + auto it = _properties.find(prop); + + if (it != _properties.cend()) + _properties.erase(it); + else + throw PropertyItemException{this, _EXCPT("Tried to remove a property not belonging to this item")}; + } +} diff --git a/Grinder/common/PropertyObject.h b/Grinder/common/PropertyObject.h new file mode 100644 index 0000000000000000000000000000000000000000..bce1687c0936457fde56df15ffc197aa15feb30a --- /dev/null +++ b/Grinder/common/PropertyObject.h @@ -0,0 +1,43 @@ +/****************************************************************************** + * File: PropertyObject.h + * Date: 20.3.2018 + *****************************************************************************/ + +#ifndef PROPERTYOBJECT_H +#define PROPERTYOBJECT_H + +#include <QObject> + +#include "common/PropertyVector.h" +#include "common/properties/StandardProperties.h" + +namespace grndr +{ + class PropertyObject : public QObject + { + Q_OBJECT + + public: + PropertyVector& properties() { return _properties; } + const PropertyVector& properties() const { return _properties; } + + public: + virtual void serialize(SerializationContext& ctx) const; + virtual void deserialize(DeserializationContext& ctx); + + protected: + virtual void createProperties() { } + + template<typename PropType> + std::shared_ptr<PropertyBase> createProperty(PropertyID id, QString name, typename PropType::value_type defValue = typename PropType::value_type(), PropertyBase::Flags flags = PropertyBase::Flag::None); + void removeProperty(PropertyID id); + void removeProperty(const PropertyBase* prop); + + protected: + PropertyVector _properties; + }; +} + +#include "PropertyObject.impl.h" + +#endif diff --git a/Grinder/common/PropertyObject.impl.h b/Grinder/common/PropertyObject.impl.h new file mode 100644 index 0000000000000000000000000000000000000000..bfa920d82c08e9f60abf9309648919c56791a35e --- /dev/null +++ b/Grinder/common/PropertyObject.impl.h @@ -0,0 +1,22 @@ +/****************************************************************************** + * File: PropertyObject.impl.h + * Date: 20.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "PropertyObject.h" +#include "PropertyExceptions.h" + +template<typename PropType> +std::shared_ptr<PropertyBase> PropertyObject::createProperty(PropertyID id, QString name, typename PropType::value_type defValue, PropertyBase::Flags flags) +{ + if (id == PropertyID::None) + throw std::invalid_argument{_EXCPT("id may not be empty")}; + + if (_properties.selectByID(id)) + throw PropertyItemException{this, _EXCPT(QString{"A property with ID '%1' already exists"}.arg(id))}; + + auto prop = std::make_shared<PropType>(id, name, defValue, flags); + _properties.push_back(prop); + return prop; +} diff --git a/Grinder/common/PropertyVector.cpp b/Grinder/common/PropertyVector.cpp new file mode 100644 index 0000000000000000000000000000000000000000..6a61ceb36f168cd514f852d6456459a5dead0d26 --- /dev/null +++ b/Grinder/common/PropertyVector.cpp @@ -0,0 +1,25 @@ +/****************************************************************************** + * File: PropertyVector.cpp + * Date: 14.1.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "PropertyVector.h" + +const char* PropertyVector::Serialization_Group = "Properties"; +const char* PropertyVector::Serialization_Element = "Property"; + +void PropertyVector::copyValues(const PropertyVector& properties) +{ + // Copy values of properties which also exist in this vector + for (const auto& property : properties) + { + if (auto exProperty = selectByID(property->getID())) + exProperty->copyValue(property.get()); + } +} + +PropertyVector::pointer_type PropertyVector::selectByID(PropertyID id) const +{ + return selectFirst([id](auto prop) { return prop->getID() == id; }); +} diff --git a/Grinder/common/PropertyVector.h b/Grinder/common/PropertyVector.h new file mode 100644 index 0000000000000000000000000000000000000000..efd0e0ccdaa6cf6ce18af534e6ef79e6e4912842 --- /dev/null +++ b/Grinder/common/PropertyVector.h @@ -0,0 +1,34 @@ +/****************************************************************************** + * File: PropertyVector.h + * Date: 14.1.2018 + *****************************************************************************/ + +#ifndef PROPERTYVECTOR_H +#define PROPERTYVECTOR_H + +#include "common/ObjectVector.h" +#include "Property.h" + +namespace grndr +{ + class PropertyVector : public ObjectVector<PropertyBase> + { + public: + static const char* Serialization_Group; + static const char* Serialization_Element; + + public: + void copyValues(const PropertyVector& properties); + + template<typename PropType> + PropType* property(typename vector_type::size_type pos) const; + template<typename PropType> + PropType* property(PropertyID id) const; + + pointer_type selectByID(PropertyID id) const; + }; +} + +#include "PropertyVector.impl.h" + +#endif diff --git a/Grinder/common/PropertyVector.impl.h b/Grinder/common/PropertyVector.impl.h new file mode 100644 index 0000000000000000000000000000000000000000..0be2c0118c16e083c243e87e48cb045c8fa512d1 --- /dev/null +++ b/Grinder/common/PropertyVector.impl.h @@ -0,0 +1,19 @@ +/****************************************************************************** + * File: PropertyVector.impl.h + * Date: 14.1.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "PropertyVector.h" + +template<typename PropType> +PropType* PropertyVector::property(typename vector_type::size_type pos) const +{ + return dynamic_cast<PropType*>(at(pos).get()); +} + +template<typename PropType> +PropType* PropertyVector::property(PropertyID id) const +{ + return dynamic_cast<PropType*>(selectFirst([id](auto prop) { return prop->getID() == id; })); +} diff --git a/Grinder/common/properties/AngleProperty.cpp b/Grinder/common/properties/AngleProperty.cpp new file mode 100644 index 0000000000000000000000000000000000000000..a86aef29100d24e8ce7cb94ad4ab6c59ebde4f89 --- /dev/null +++ b/Grinder/common/properties/AngleProperty.cpp @@ -0,0 +1,14 @@ +/****************************************************************************** + * File: AngleProperty.cpp + * Date: 26.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "AngleProperty.h" +#include "RangeConstraint.h" +#include "ui/property/editors/AnglePropertyEditor.h" + +QWidget* AngleProperty::createEditor(QWidget* parent) +{ + return new AnglePropertyEditor{this, parent}; +} diff --git a/Grinder/common/properties/AngleProperty.h b/Grinder/common/properties/AngleProperty.h new file mode 100644 index 0000000000000000000000000000000000000000..698d2aad8f319d53b309635355d66699e3445579 --- /dev/null +++ b/Grinder/common/properties/AngleProperty.h @@ -0,0 +1,23 @@ +/****************************************************************************** + * File: AngleProperty.h + * Date: 26.3.2018 + *****************************************************************************/ + +#ifndef ANGLEPROPERTY_H +#define ANGLEPROPERTY_H + +#include "common/Property.h" + +namespace grndr +{ + class AngleProperty : public Property<double> + { + public: + using Property<value_type>::Property; + + public: + virtual QWidget* createEditor(QWidget* parent) override; + }; +} + +#endif diff --git a/Grinder/common/properties/BoolProperty.cpp b/Grinder/common/properties/BoolProperty.cpp new file mode 100644 index 0000000000000000000000000000000000000000..447e2e6656f89ca7cfe6b54378785453f2773102 --- /dev/null +++ b/Grinder/common/properties/BoolProperty.cpp @@ -0,0 +1,13 @@ +/****************************************************************************** + * File: BoolProperty.cpp + * Date: 05.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "BoolProperty.h" +#include "ui/property/editors/BoolPropertyEditor.h" + +QWidget* BoolProperty::createEditor(QWidget* parent) +{ + return new BoolPropertyEditor{this, parent}; +} diff --git a/Grinder/common/properties/BoolProperty.h b/Grinder/common/properties/BoolProperty.h new file mode 100644 index 0000000000000000000000000000000000000000..c7597f5d63d24d652bf90ec932ead5f4010b913a --- /dev/null +++ b/Grinder/common/properties/BoolProperty.h @@ -0,0 +1,23 @@ +/****************************************************************************** + * File: BoolProperty.h + * Date: 05.3.2018 + *****************************************************************************/ + +#ifndef BOOLPROPERTY_H +#define BOOLPROPERTY_H + +#include "common/Property.h" + +namespace grndr +{ + class BoolProperty : public Property<bool> + { + public: + using Property<value_type>::Property; + + public: + virtual QWidget* createEditor(QWidget* parent) override; + }; +} + +#endif diff --git a/Grinder/common/properties/ColorProperty.cpp b/Grinder/common/properties/ColorProperty.cpp new file mode 100644 index 0000000000000000000000000000000000000000..afae6920791be85a3f70326ac5b324ff5c4b10ab --- /dev/null +++ b/Grinder/common/properties/ColorProperty.cpp @@ -0,0 +1,13 @@ +/****************************************************************************** + * File: ColorProperty.cpp + * Date: 20.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ColorProperty.h" + +QWidget* ColorProperty::createEditor(QWidget* parent) +{ + // TODO: + return nullptr; +} diff --git a/Grinder/common/properties/ColorProperty.h b/Grinder/common/properties/ColorProperty.h new file mode 100644 index 0000000000000000000000000000000000000000..270533d0b6a63d96063b1a047dfefca190dadb04 --- /dev/null +++ b/Grinder/common/properties/ColorProperty.h @@ -0,0 +1,23 @@ +/****************************************************************************** + * File: ColorProperty.h + * Date: 20.3.2018 + *****************************************************************************/ + +#ifndef COLORPROPERTY_H +#define COLORPROPERTY_H + +#include "common/Property.h" + +namespace grndr +{ + class ColorProperty : public Property<QColor> + { + public: + using Property<value_type>::Property; + + public: + virtual QWidget* createEditor(QWidget* parent) override; + }; +} + +#endif diff --git a/Grinder/common/properties/IntProperty.cpp b/Grinder/common/properties/IntProperty.cpp new file mode 100644 index 0000000000000000000000000000000000000000..b55576de56c49bfae358caf686b010d9d0b9f7d1 --- /dev/null +++ b/Grinder/common/properties/IntProperty.cpp @@ -0,0 +1,13 @@ +/****************************************************************************** + * File: IntProperty.cpp + * Date: 05.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "IntProperty.h" +#include "ui/property/editors/TextPropertyEditor.h" + +QWidget* IntProperty::createEditor(QWidget* parent) +{ + return new TextPropertyEditor{this, "-?\\d+", parent}; +} diff --git a/Grinder/common/properties/IntProperty.h b/Grinder/common/properties/IntProperty.h new file mode 100644 index 0000000000000000000000000000000000000000..b3c1b393d7bf77f25ff44ba876fffa467e8c1c58 --- /dev/null +++ b/Grinder/common/properties/IntProperty.h @@ -0,0 +1,23 @@ +/****************************************************************************** + * File: IntProperty.h + * Date: 05.3.2018 + *****************************************************************************/ + +#ifndef INTPROPERTY_H +#define INTPROPERTY_H + +#include "common/Property.h" + +namespace grndr +{ + class IntProperty : public Property<int> + { + public: + using Property<value_type>::Property; + + public: + virtual QWidget* createEditor(QWidget* parent) override; + }; +} + +#endif diff --git a/Grinder/common/properties/PointProperty.cpp b/Grinder/common/properties/PointProperty.cpp new file mode 100644 index 0000000000000000000000000000000000000000..2e8eabe53407440d152b5803090412c45f8e3167 --- /dev/null +++ b/Grinder/common/properties/PointProperty.cpp @@ -0,0 +1,13 @@ +/****************************************************************************** + * File: PointProperty.cpp + * Date: 20.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "PointProperty.h" +#include "ui/property/editors/PointPropertyEditor.h" + +QWidget* PointProperty::createEditor(QWidget* parent) +{ + return new PointPropertyEditor{this, parent}; +} diff --git a/Grinder/common/properties/PointProperty.h b/Grinder/common/properties/PointProperty.h new file mode 100644 index 0000000000000000000000000000000000000000..58eba874b6f79d023332d37cfb8bd642d7cb7879 --- /dev/null +++ b/Grinder/common/properties/PointProperty.h @@ -0,0 +1,23 @@ +/****************************************************************************** + * File: PointProperty.h + * Date: 20.3.2018 + *****************************************************************************/ + +#ifndef POINTPROPERTY_H +#define POINTPROPERTY_H + +#include "common/Property.h" + +namespace grndr +{ + class PointProperty : public Property<QPoint> + { + public: + using Property<value_type>::Property; + + public: + virtual QWidget* createEditor(QWidget* parent) override; + }; +} + +#endif diff --git a/Grinder/common/properties/RangeConstraint.h b/Grinder/common/properties/RangeConstraint.h new file mode 100644 index 0000000000000000000000000000000000000000..9175bb880173b59954e3894e296cf8ccdd74d8a8 --- /dev/null +++ b/Grinder/common/properties/RangeConstraint.h @@ -0,0 +1,39 @@ +/****************************************************************************** + * File: RangeConstraint.h + * Date: 05.3.2018 + *****************************************************************************/ + +#ifndef RANGECONSTRAINT_H +#define RANGECONSTRAINT_H + +#include "common/PropertyConstraint.h" + +namespace grndr +{ + template<typename ValType> + class RangeConstraint : public PropertyConstraint<ValType> + { + public: + using value_type = typename PropertyConstraint<ValType>::value_type; + + public: + RangeConstraint(Property<value_type>* property, const value_type& minValue, const value_type& maxValue); + + public: + virtual void applyConstraint(value_type& value); + + public: + value_type getMinValue() const { return _minValue; } + void setMinValue(value_type value) { _minValue = value; } + value_type getMaxValue() const { return _maxValue; } + void setMaxValue(value_type value) { _maxValue = value; } + + private: + value_type _minValue{}; + value_type _maxValue{}; + }; +} + +#include "RangeConstraint.impl.h" + +#endif diff --git a/Grinder/common/properties/RangeConstraint.impl.h b/Grinder/common/properties/RangeConstraint.impl.h new file mode 100644 index 0000000000000000000000000000000000000000..9c5f3eb6c6c3d57a274172dc276717306509a05c --- /dev/null +++ b/Grinder/common/properties/RangeConstraint.impl.h @@ -0,0 +1,24 @@ +/****************************************************************************** + * File: RangeConstraint.impl.h + * Date: 05.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "RangeConstraint.h" + +template<typename ValType> +RangeConstraint<ValType>::RangeConstraint(Property<value_type>* property, const value_type& minValue, const value_type& maxValue) : PropertyConstraint<ValType>(property), + _minValue{minValue}, _maxValue{maxValue} +{ + +} + +template<typename ValType> +void RangeConstraint<ValType>::applyConstraint(value_type& value) +{ + if (value < _minValue) + value = _minValue; + + if (value > _maxValue) + value = _maxValue; +} diff --git a/Grinder/common/properties/RealProperty.cpp b/Grinder/common/properties/RealProperty.cpp new file mode 100644 index 0000000000000000000000000000000000000000..f8f49fa96f1ccbba65f87c1d3cbdfb577eb1086e --- /dev/null +++ b/Grinder/common/properties/RealProperty.cpp @@ -0,0 +1,13 @@ +/****************************************************************************** + * File: RealProperty.cpp + * Date: 05.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "RealProperty.h" +#include "ui/property/editors/TextPropertyEditor.h" + +QWidget* RealProperty::createEditor(QWidget* parent) +{ + return new TextPropertyEditor{this, "-?\\d+(.?\\d+)?", parent}; +} diff --git a/Grinder/common/properties/RealProperty.h b/Grinder/common/properties/RealProperty.h new file mode 100644 index 0000000000000000000000000000000000000000..749581bd72085dbbe51d2887b92e7ded41e8a65f --- /dev/null +++ b/Grinder/common/properties/RealProperty.h @@ -0,0 +1,23 @@ +/****************************************************************************** + * File: RealProperty.h + * Date: 05.3.2018 + *****************************************************************************/ + +#ifndef REALPROPERTY_H +#define REALPROPERTY_H + +#include "common/Property.h" + +namespace grndr +{ + class RealProperty : public Property<double> + { + public: + using Property<value_type>::Property; + + public: + virtual QWidget* createEditor(QWidget* parent) override; + }; +} + +#endif diff --git a/Grinder/common/properties/SizeProperty.cpp b/Grinder/common/properties/SizeProperty.cpp new file mode 100644 index 0000000000000000000000000000000000000000..c535cdb92327d6f313f40f8d5bc77876547b379e --- /dev/null +++ b/Grinder/common/properties/SizeProperty.cpp @@ -0,0 +1,13 @@ +/****************************************************************************** + * File: SizeProperty.cpp + * Date: 21.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "SizeProperty.h" +#include "ui/property/editors/SizePropertyEditor.h" + +QWidget* SizeProperty::createEditor(QWidget* parent) +{ + return new SizePropertyEditor{this, parent}; +} diff --git a/Grinder/common/properties/SizeProperty.h b/Grinder/common/properties/SizeProperty.h new file mode 100644 index 0000000000000000000000000000000000000000..27604fd3a9f6b4e6cf20a560b805359694272a28 --- /dev/null +++ b/Grinder/common/properties/SizeProperty.h @@ -0,0 +1,23 @@ +/****************************************************************************** + * File: SizeProperty.h + * Date: 21.3.2018 + *****************************************************************************/ + +#ifndef SIZEPROPERTY_H +#define SIZEPROPERTY_H + +#include "common/Property.h" + +namespace grndr +{ + class SizeProperty : public Property<QSize> + { + public: + using Property<value_type>::Property; + + public: + virtual QWidget* createEditor(QWidget* parent) override; + }; +} + +#endif diff --git a/Grinder/common/properties/StandardProperties.h b/Grinder/common/properties/StandardProperties.h new file mode 100644 index 0000000000000000000000000000000000000000..4dd7d4078fc7b9e0149810fa86895506898fac38 --- /dev/null +++ b/Grinder/common/properties/StandardProperties.h @@ -0,0 +1,19 @@ +/****************************************************************************** + * File: StandardProperties.h + * Date: 05.3.2018 + *****************************************************************************/ + +#ifndef STANDARDPROPERTIES_H +#define STANDARDPROPERTIES_H + +#include "BoolProperty.h" +#include "IntProperty.h" +#include "RealProperty.h" +#include "AngleProperty.h" +#include "StringProperty.h" +#include "UIntProperty.h" +#include "PointProperty.h" +#include "SizeProperty.h" +#include "ColorProperty.h" + +#endif diff --git a/Grinder/common/properties/StringProperty.cpp b/Grinder/common/properties/StringProperty.cpp new file mode 100644 index 0000000000000000000000000000000000000000..7c320659f1e94be3fccaafef9a83b82e8a39dd5b --- /dev/null +++ b/Grinder/common/properties/StringProperty.cpp @@ -0,0 +1,13 @@ +/****************************************************************************** + * File: StringProperty.cpp + * Date: 05.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "StringProperty.h" +#include "ui/property/editors/TextPropertyEditor.h" + +QWidget* StringProperty::createEditor(QWidget* parent) +{ + return new TextPropertyEditor{this, "", parent}; +} diff --git a/Grinder/common/properties/StringProperty.h b/Grinder/common/properties/StringProperty.h new file mode 100644 index 0000000000000000000000000000000000000000..510d840a22da8bca1b3a6df706c46958cace3b03 --- /dev/null +++ b/Grinder/common/properties/StringProperty.h @@ -0,0 +1,23 @@ +/****************************************************************************** + * File: StringProperty.h + * Date: 05.3.2018 + *****************************************************************************/ + +#ifndef STRINGPROPERTY_H +#define STRINGPROPERTY_H + +#include "common/Property.h" + +namespace grndr +{ + class StringProperty : public Property<QString> + { + public: + using Property<value_type>::Property; + + public: + virtual QWidget* createEditor(QWidget* parent) override; + }; +} + +#endif diff --git a/Grinder/common/properties/UIntProperty.cpp b/Grinder/common/properties/UIntProperty.cpp new file mode 100644 index 0000000000000000000000000000000000000000..1395c1fc8920c08984bfd878ae7c76f4dea8236d --- /dev/null +++ b/Grinder/common/properties/UIntProperty.cpp @@ -0,0 +1,13 @@ +/****************************************************************************** + * File: UIntProperty.cpp + * Date: 05.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "UIntProperty.h" +#include "ui/property/editors/TextPropertyEditor.h" + +QWidget* UIntProperty::createEditor(QWidget* parent) +{ + return new TextPropertyEditor{this, "\\d+", parent}; +} diff --git a/Grinder/common/properties/UIntProperty.h b/Grinder/common/properties/UIntProperty.h new file mode 100644 index 0000000000000000000000000000000000000000..e7ad8fb08ca6befe1fa9052c5fd0ba7a89dcb3e3 --- /dev/null +++ b/Grinder/common/properties/UIntProperty.h @@ -0,0 +1,23 @@ +/****************************************************************************** + * File: UIntProperty.h + * Date: 05.3.2018 + *****************************************************************************/ + +#ifndef UINTPROPERTY_H +#define UINTPROPERTY_H + +#include "common/Property.h" + +namespace grndr +{ + class UIntProperty : public Property<unsigned int> + { + public: + using Property<value_type>::Property; + + public: + virtual QWidget* createEditor(QWidget* parent) override; + }; +} + +#endif diff --git a/Grinder/controller/EngineController.cpp b/Grinder/controller/EngineController.cpp new file mode 100644 index 0000000000000000000000000000000000000000..56d16fb2161d15f66cfe4eeb70f9a557521c797c --- /dev/null +++ b/Grinder/controller/EngineController.cpp @@ -0,0 +1,57 @@ +/****************************************************************************** + * File: EngineController.cpp + * Date: 21.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "EngineController.h" +#include "project/Label.h" +#include "engine/Engine.h" +#include "engine/EngineExceptions.h" +#include "util/TemporaryStatusMessage.h" +#include "util/UIUtils.h" + +EngineController::EngineController(Engine* engine) : GenericController("Engine"), + _engine{engine} +{ + if (!engine) + throw std::invalid_argument{_EXCPT("engine may not be null")}; +} + +void EngineController::executeLabel(Label* label, Engine::ExecutionMode mode) const +{ + if (label) + { + TemporaryStatusMessage tmpStatusMessage{QString{"Executing label '%1'..."}.arg(label->getName())}; + + callControllerFunction(QString{"Executing label '%1'"}.arg(label->getName()), [this](Label* label, Engine::ExecutionMode mode) { + _engine->executeLabel(label, mode); + return true; + }, label, mode); + } +} + +cv::Mat EngineController::executeLabelEx(Label* label, const Port* port, Engine::ExecutionMode mode) const +{ + if (label && port) + { + TemporaryStatusMessage tmpStatusMessage{QString{"Executing label '%1'..."}.arg(label->getName())}; + + return callControllerFunction(QString{"Executing label '%1'"}.arg(label->getName()), [this](Label* label, const Port* port, Engine::ExecutionMode mode) { + // If the port is an in-port, get data from the connected out-port + if (port->isIn()) + { + auto cons = port->getConnections(Port::Direction::In); + + if (cons.size() == 1) + port = cons.front()->sourcePort(); + else if (cons.size() > 1) + throw EngineException{_engine, _EXCPT(QString{"Port '%1' has multiple inputs"}.arg(port->getFormattedName()))}; + } + + return _engine->executeLabelEx(label, port, mode); + }, label, port, mode); + } + + return cv::Mat{}; +} diff --git a/Grinder/controller/EngineController.h b/Grinder/controller/EngineController.h new file mode 100644 index 0000000000000000000000000000000000000000..8fffcd78ee519e34f526571b7471a9529c10919a --- /dev/null +++ b/Grinder/controller/EngineController.h @@ -0,0 +1,37 @@ +/****************************************************************************** + * File: EngineController.h + * Date: 21.2.2018 + *****************************************************************************/ + +#ifndef ENGINECONTROLLER_H +#define ENGINECONTROLLER_H + +#include "GenericController.h" +#include "engine/EngineExecutionContext.h" + +#include <opencv2/core.hpp> + +namespace grndr +{ + class Engine; + class Label; + class Block; + class Port; + + class EngineController : public GenericController + { + Q_OBJECT + + public: + EngineController(Engine* engine); + + public: + void executeLabel(Label* label, Engine::ExecutionMode mode) const; + cv::Mat executeLabelEx(Label* label, const Port* port, Engine::ExecutionMode mode) const; + + private: + Engine* _engine{nullptr}; + }; +} + +#endif diff --git a/Grinder/controller/GenericController.cpp b/Grinder/controller/GenericController.cpp new file mode 100644 index 0000000000000000000000000000000000000000..8b70dd397de534561691ac8275742a6f80fffcad --- /dev/null +++ b/Grinder/controller/GenericController.cpp @@ -0,0 +1,23 @@ +/****************************************************************************** + * File: GenericController.cpp + * Date: 08.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "GenericController.h" + +GenericController::GenericController(QString typeName) : + _controllerTypeName{typeName} +{ + if (typeName.isEmpty()) + throw std::invalid_argument{_EXCPT("typeName may not be empty")}; +} + +void GenericController::showControllerError(QString action, QString error) const +{ + if (!_suppressErrors) + { + QString str = !error.isEmpty() ? GetExceptionMessage(error) : "Unknown exception"; + QMessageBox::warning(nullptr, _controllerTypeName + " error", QString{"<b>%2 failed:</b><br>%1"}.arg(str).arg(action)); + } +} diff --git a/Grinder/controller/GenericController.h b/Grinder/controller/GenericController.h new file mode 100644 index 0000000000000000000000000000000000000000..6d3349e44d638b5df3284ee1d7c3cb03f2f35e56 --- /dev/null +++ b/Grinder/controller/GenericController.h @@ -0,0 +1,39 @@ +/****************************************************************************** + * File: GenericController.h + * Date: 08.2.2018 + *****************************************************************************/ + +#ifndef GENERICCONTROLLER_H +#define GENERICCONTROLLER_H + +#include <QObject> + +namespace grndr +{ + class GenericController : public QObject + { + Q_OBJECT + + public: + GenericController(QString typeName); + + protected: + template<typename Functor, typename... Args> + auto callControllerFunction(QString action, Functor fnc, Args... args) const -> decltype(fnc(args...)); + + virtual bool canCallControllerFunction() const { return true; } + virtual void controllerFunctionCalled() const { } + + void showControllerError(QString action, QString error) const; + + protected: + bool _suppressErrors{false}; + + private: + QString _controllerTypeName{""}; + }; +} + +#include "GenericController.impl.h" + +#endif diff --git a/Grinder/controller/GenericController.impl.h b/Grinder/controller/GenericController.impl.h new file mode 100644 index 0000000000000000000000000000000000000000..9fe6e6a88213c7c008abc4970748625d1877cd4e --- /dev/null +++ b/Grinder/controller/GenericController.impl.h @@ -0,0 +1,25 @@ +/****************************************************************************** + * File: GenericController.impl.h + * Date: 08.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "GenericController.h" + +template<typename Functor, typename... Args> +auto GenericController::callControllerFunction(QString action, Functor fnc, Args... args) const -> decltype(fnc(args...)) +{ + if (canCallControllerFunction()) + { + try { + auto result = fnc(std::forward<Args>(args)...); + controllerFunctionCalled(); + return result; + } catch (std::exception& e) { + showControllerError(action, e.what()); + } + } + + using retType = decltype(fnc(args...)); + return retType{}; +} diff --git a/Grinder/controller/ImageEditorController.cpp b/Grinder/controller/ImageEditorController.cpp new file mode 100644 index 0000000000000000000000000000000000000000..ce5c25d19818ac6614431b3b70f951a01ac27ae5 --- /dev/null +++ b/Grinder/controller/ImageEditorController.cpp @@ -0,0 +1,316 @@ +/****************************************************************************** + * File: ImageEditorController.cpp + * Date: 15.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ImageEditorController.h" +#include "core/GrinderApplication.h" +#include "image/ImageExceptions.h" +#include "ui/image/ImageEditor.h" +#include "ui/image/LayersListWidget.h" + +ImageEditorController::ImageEditorController(ImageEditor* imageEditor) : GenericController("Image editor"), ImageEditorComponent(imageEditor) +{ + if (!imageEditor) + throw std::invalid_argument{_EXCPT("imageEditor may not be null")}; +} + +void ImageEditorController::assignUiComponents(ImageEditorView* view, LayersListWidget* layersList) +{ + if (!view) + throw std::invalid_argument{_EXCPT("view may not be null")}; + + if (!layersList) + throw std::invalid_argument{_EXCPT("layersList may not be null")}; + + _view = view; + _layersList = layersList; +} + +void ImageEditorController::showImageBuild(ImageBuild* build) +{ + if (build == _activeImageBuild) + return; + + if (!_view) + throw std::runtime_error{_EXCPT("No view has been set")}; + + if (_activeImageBuild) // No longer listen for events from the previous image build + { + disconnect(_activeImageBuild, nullptr, this, nullptr); + connectLayerSignals(_activeImageBuild, false); + } + + if (_activeImageBuild) + emit imageBuildSwitching(_activeImageBuild); + + _activeImageBuild = build; + + // Unset any currently active scene + _view->setEditorScene(nullptr); + _activeScene.reset(nullptr); + + // Unset any currently active layer + switchLayer(nullptr); + + if (build) // Create a new scene for the given image build and show it + { + _activeScene = std::make_unique<ImageEditorScene>(_imageEditor, _activeImageBuild, _view); + _activeScene->buildScene(); + + _view->setEditorScene(_activeScene.get()); + + // Listen for image build events + connect(_activeImageBuild, &ImageBuild::imageDataChanged, this, &ImageEditorController::refreshEditorImage); + connect(_activeImageBuild, &ImageBuild::layerCreated, this, &ImageEditorController::layerCreated); + connect(_activeImageBuild, &ImageBuild::layerRemoved, this, &ImageEditorController::layerRemoved); + connect(_activeImageBuild, &ImageBuild::layerMoved, this, &ImageEditorController::layerMoved); + + // Listen for signals from all existing layers of the image build + connectLayerSignals(_activeImageBuild); + + // Show the new editor image and fit it to its window if the corresponding option is turned on + refreshEditorImage(); + + if (grinder()->settings().imageEditorOptions().autoFitToWindow) + _activeScene->fitViewToImage(); + } + + emit imageBuildSwitched(_activeImageBuild); +} + +void ImageEditorController::switchLayer(Layer* layer) +{ + if (layer == _activeLayer) + return; + + if (_activeLayer) + emit layerSwitching(_activeLayer); + + _activeLayer = layer; + emit layerSwitched(_activeLayer); +} + +std::shared_ptr<Layer> ImageEditorController::createLayer(QString name) const +{ + return callControllerFunction("Creating a new layer", [this](QString name) { + return _activeImageBuild->createLayer(name); + }, name); +} + +void ImageEditorController::removeLayer(const Layer* layer) +{ + if (layer) + { + // If removing the active layer, unset it first + if (_activeLayer == layer) + switchLayer(nullptr); + + callControllerFunction("Removing a layer", [this](const Layer* layer) { + _activeImageBuild->removeLayer(layer); + return true; + }, layer); + } +} + +void ImageEditorController::removeAllLayers() +{ + if (_activeImageBuild) + { + auto layers = _activeImageBuild->layers(); + + for (const auto& layer : layers) + removeLayer(layer.get()); + } +} + +std::shared_ptr<DraftItem> ImageEditorController::createDraftItem(DraftItemType type, Layer* layer) const +{ + if (!layer) + layer = _activeLayer; + + if (layer) + { + return callControllerFunction("Creating a draft item", [this](DraftItemType type, Layer* layer) { + return layer->createDraftItem(type); + }, type, layer); + } + else + return nullptr; +} + +void ImageEditorController::removeDraftItem(const DraftItem* item) const +{ + auto layer = const_cast<Layer*>(item->layer()); // We don't modify the item itself but its parent layer, so remove constness + + if (item) + { + callControllerFunction("Removing a draft item", [this](const DraftItem* item, Layer* layer) { + layer->removeDraftItem(item); + return true; + }, item, layer); + } +} + +void ImageEditorController::setLayerVisibility(Layer* layer, bool visible) const +{ + layer->setVisible(visible); +} + +void ImageEditorController::moveLayer(Layer* layer, bool up) const +{ + callControllerFunction("Moving a layer", [this](Layer* layer, bool up) { + _activeImageBuild->moveLayer(layer, up); + return true; + }, layer, up); +} + +void ImageEditorController::renameLayer(Layer* layer, QString newName) const +{ + callControllerFunction("Renaming a layer", [this](Layer* layer, QString newName) { + validateLayerName(newName, layer); + renameImageBuildItem(layer, newName); + return true; + }, layer, newName); +} + +void ImageEditorController::renameImageBuildItem(ImageBuildItem* item, QString newName) const +{ + item->setName(newName); +} + +void ImageEditorController::setNodesPrimaryColor(QColor color, const std::vector<DraftItemNode*>& nodes) const +{ + if (_activeScene) + { + for (const auto& node : nodes) + { + if (auto item = node->draftItem().lock()) + item->primaryColor()->setValue(color); + } + } +} + +void ImageEditorController::removeSelectedNodes() const +{ + if (_activeScene) + { + // Remove all selected draft items + auto selItems = _activeScene->getNodes<DraftItemNode>(true); + + for (const auto& node : selItems) + { + if (auto item = node->draftItem().lock()) + removeDraftItem(item.get()); + } + } +} + +void ImageEditorController::controllerFunctionCalled() const +{ + // Redraw the scene after calling a controller function + if (_activeScene) + _activeScene->update(); +} + +void ImageEditorController::validateLayerName(QString name, const Layer* layer) const +{ + auto imageBuild = layer ? layer->imageBuild() : _activeImageBuild; + + // Name must be non-empty and unique + if (!name.isEmpty()) + { + auto existingLayer = imageBuild->layers().selectByName(name).get(); + + if (existingLayer && existingLayer != layer) + throw ImageBuildException{imageBuild, _EXCPT(QString{"A layer with the name '%1' already exists"}.arg(name))}; + } + else + throw ImageBuildException{imageBuild, _EXCPT("The layer name may not be empty")}; +} + +void ImageEditorController::connectLayerSignals(ImageBuild* imageBuild, bool connectSignals) const +{ + // Listen for/stop listening for events from all layers + for (auto& layer : imageBuild->layers()) + connectLayerSignals(layer.get(), connectSignals); +} + +void ImageEditorController::connectLayerSignals(Layer* layer, bool connectSignals) const +{ + if (connectSignals) + { + // Get notified about layer and draft item events + connect(layer, &Layer::draftItemCreated, this, &ImageEditorController::draftItemCreated); + connect(layer, &Layer::draftItemRemoved, this, &ImageEditorController::draftItemRemoved); + connect(layer, &Layer::layerShown, this, &ImageEditorController::layerVisibilityChanged); + connect(layer, &Layer::layerHidden, this, &ImageEditorController::layerVisibilityChanged); + } + else + disconnect(layer, nullptr, this, nullptr); +} + +void ImageEditorController::refreshEditorImage() +{ + callControllerFunction("Refreshing editor image", [this]() { + if (_activeScene) + _activeScene->refreshImage(true); + + return true; + }); +} + +void ImageEditorController::layerCreated(const std::shared_ptr<Layer>& layer) const +{ + connectLayerSignals(layer.get()); + + if (_layersList) + _layersList->addObject(layer.get(), true, true); +} + +void ImageEditorController::layerRemoved(const std::shared_ptr<Layer>& layer) const +{ + connectLayerSignals(layer.get(), false); + + // Remove all draft item nodes that belong to the removed layer + if (_activeScene) + { + for (const auto& item : layer->draftItems()) + _activeScene->removeDraftItemNode(item); + } + + if (_layersList) + _layersList->removeObject(layer.get()); +} + +void ImageEditorController::layerMoved(const std::shared_ptr<Layer>& layer, int indexOld, int indexNew) const +{ + Q_UNUSED(layer); + + // Reflect the new z-order in the scene + if (_activeScene) + _activeScene->updateDraftItemsOrder(); + + if (_layersList) + _layersList->swapLayers(indexOld, indexNew); +} + +void ImageEditorController::layerVisibilityChanged() const +{ + // Update the visibility of all draft items in the layer + if (_activeScene) + _activeScene->updateDraftItemsVisibility(dynamic_cast<Layer*>(sender())); +} + +void ImageEditorController::draftItemCreated(const std::shared_ptr<DraftItem>& item) const +{ + if (_activeScene) + _activeScene->createDraftItemNode(item); +} + +void ImageEditorController::draftItemRemoved(const std::shared_ptr<DraftItem>& item) const +{ + if (_activeScene) + _activeScene->removeDraftItemNode(item); +} diff --git a/Grinder/controller/ImageEditorController.h b/Grinder/controller/ImageEditorController.h new file mode 100644 index 0000000000000000000000000000000000000000..73ae110dacf7db95c84f5f557b7a6a455e8aea73 --- /dev/null +++ b/Grinder/controller/ImageEditorController.h @@ -0,0 +1,98 @@ +/****************************************************************************** + * File: ImageEditorController.h + * Date: 15.3.2018 + *****************************************************************************/ + +#ifndef IMAGEEDITORCONTROLLER_H +#define IMAGEEDITORCONTROLLER_H + +#include "GenericController.h" +#include "ui/image/ImageEditorComponent.h" +#include "ui/image/ImageEditorScene.h" + +namespace grndr +{ + class ImageEditor; + class ImageEditorView; + class ImageBuild; + class Layer; + class LayersListWidget; + + class ImageEditorController : public GenericController, public ImageEditorComponent + { + Q_OBJECT + + public: + ImageEditorController(ImageEditor* imageEditor); + + public: + void assignUiComponents(ImageEditorView* view, LayersListWidget* layersList); + + public: + void showImageBuild(ImageBuild* build); + void hideImageBuild() { showImageBuild(nullptr); } + ImageBuild* activeImageBuild() { return _activeImageBuild; } + const ImageBuild* activeImageBuild() const { return _activeImageBuild; } + + ImageEditorScene* activeScene() { return _activeScene.get(); } + const ImageEditorScene* activeScene() const { return _activeScene.get(); } + + void switchLayer(Layer* layer); + Layer* activeLayer() { return _activeLayer; } + const Layer* activeLayer() const { return _activeLayer; } + + public: + std::shared_ptr<Layer> createLayer(QString name = "") const; + void removeLayer(const Layer* layer); + void removeAllLayers(); + + std::shared_ptr<DraftItem> createDraftItem(DraftItemType type, Layer* layer = nullptr) const; + void removeDraftItem(const DraftItem* item) const; + + void setLayerVisibility(Layer* layer, bool visible) const; + void moveLayer(Layer* layer, bool up) const; + void renameLayer(Layer* layer, QString newName) const; + void renameImageBuildItem(ImageBuildItem* item, QString newName) const; + + void setNodesPrimaryColor(QColor color, const std::vector<DraftItemNode*>& nodes) const; + + void removeSelectedNodes() const; + + signals: + void imageBuildSwitching(ImageBuild*); + void imageBuildSwitched(ImageBuild*); + void layerSwitching(Layer*); + void layerSwitched(Layer*); + + protected: + virtual bool canCallControllerFunction() const override { return _activeImageBuild != nullptr; } + virtual void controllerFunctionCalled() const override; + + private: + void validateLayerName(QString name, const Layer* layer = nullptr) const; + + void connectLayerSignals(ImageBuild* imageBuild, bool connectSignals = true) const; + void connectLayerSignals(Layer* layer, bool connectSignals = true) const; + + private slots: + void refreshEditorImage(); + + void layerCreated(const std::shared_ptr<Layer>& layer) const; + void layerRemoved(const std::shared_ptr<Layer>& layer) const; + void layerMoved(const std::shared_ptr<Layer>& layer, int indexOld, int indexNew) const; + void layerVisibilityChanged() const; + + void draftItemCreated(const std::shared_ptr<DraftItem>& item) const; + void draftItemRemoved(const std::shared_ptr<DraftItem>& item) const; + + private: + ImageEditorView* _view{nullptr}; + LayersListWidget* _layersList{nullptr}; + + ImageBuild* _activeImageBuild{nullptr}; + std::unique_ptr<ImageEditorScene> _activeScene; + Layer* _activeLayer{nullptr}; + }; +} + +#endif diff --git a/Grinder/controller/PipelineController.cpp b/Grinder/controller/PipelineController.cpp new file mode 100644 index 0000000000000000000000000000000000000000..f45999e90bf00110e48cc5c0e70e4eb8c16c8cb0 --- /dev/null +++ b/Grinder/controller/PipelineController.cpp @@ -0,0 +1,221 @@ +/****************************************************************************** + * File: PipelineController.cpp + * Date: 18.1.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "PipelineController.h" +#include "ui/graph/GraphView.h" +#include "ui/graph/GraphBlockNode.h" +#include "ui/graph/GraphConnectionNode.h" +#include "ui/graph/GraphStyle.h" +#include "pipeline/Pipeline.h" +#include "pipeline/PipelineItem.h" +#include "pipeline/PipelineExceptions.h" +#include "util/UIUtils.h" + +PipelineController::PipelineController() : GenericController("Pipeline") +{ + +} + +void PipelineController::assignUiComponents(GraphView* view) +{ + if (!view) + throw std::invalid_argument{_EXCPT("view may not be null")}; + + _view = view; +} + +void PipelineController::showPipeline(Pipeline* pipeline) +{ + if (pipeline == _activePipeline) + return; + + if (!_view) + throw std::runtime_error{_EXCPT("No view has been set")}; + + if (_activePipeline) // No longer listen for events from the previous pipeline + disconnect(_activePipeline, nullptr, this, nullptr); + + if (_activePipeline) + emit pipelineSwitching(_activePipeline); + + _activePipeline = pipeline; + + // Unset any currently active scene + _view->setGraphScene(nullptr); + _activeScene.reset(nullptr); + + if (pipeline) // Create a new scene for the given pipeline and show it + { + _activeScene = std::make_unique<GraphScene>(_activePipeline, _view, _view->sceneStyle().getViewStyle().defaultSceneSize); + _activeScene->buildScene(); + + _view->setGraphScene(_activeScene.get()); + + // Listen for pipeline events + connect(_activePipeline, &Pipeline::blockCreated, this, &PipelineController::blockCreated); + connect(_activePipeline, &Pipeline::blockRemoved, this, &PipelineController::blockRemoved); + connect(_activePipeline, &Pipeline::connectionCreated, this, &PipelineController::connectionCreated); + connect(_activePipeline, &Pipeline::connectionRemoved, this, &PipelineController::connectionRemoved); + } + + emit pipelineSwitched(_activePipeline); +} + +std::shared_ptr<Block> PipelineController::createBlock(BlockType type, QString name) const +{ + return callControllerFunction("Creating a new block", [this](BlockType type, QString name) { + return _activePipeline->createBlock(type, name); + }, type, name); +} + +void PipelineController::removeBlock(const Block* block) const +{ + if (block) + { + callControllerFunction("Removing a block", [this](const Block* block) { + _activePipeline->removeBlock(block); + return true; + }, block); + } +} + +void PipelineController::disconnectPortFromAll(Port* port) const +{ + if (!port) + throw std::invalid_argument{_EXCPT("port may not be null")}; + + callControllerFunction("Disconnecting a port", [](Port* port) { + port->disconnectFromAll(); + return true; + }, port); +} + +void PipelineController::validateConnection(const Port* sourcePort, const Port* destPort) const +{ + if (sourcePort && destPort) // Connection is made between an outgoing and an incoming port at least + sourcePort->verifyConnection(destPort); // Check if the connection is valid + else + throw std::invalid_argument{_EXCPT("Must connect an outgoing to an incoming port")}; +} + +std::shared_ptr<Connection> PipelineController::createConnection(Port* sourcePort, Port* destPort) const +{ + if (!sourcePort || !destPort) + throw std::invalid_argument{_EXCPT("sourcePort and destPort may not be null")}; + + return callControllerFunction("Creating a new connection", [](Port* sourcePort, Port* destPort) { + return sourcePort->connectTo(destPort); + }, sourcePort, destPort); +} + +void PipelineController::removeConnection(const Connection* connection) const +{ + if (connection) + { + callControllerFunction("Removing a connection", [](const Connection* connection) { + auto sourcePort = const_cast<Port*>(connection->sourcePort()); // Need to be able to modify the source port of the connection + sourcePort->removeConnection(connection); + return true; + }, connection); + } +} + +void PipelineController::renameBlock(Block* block, QString newName) const +{ + callControllerFunction("Renaming a block", [this](Block* block, QString newName) { + validateBlockName(newName, block); + renamePipelineItem(block, newName); + return true; + }, block, newName); +} + +void PipelineController::renamePipelineItem(PipelineItem* item, QString newName) const +{ + item->setName(newName); +} + +void PipelineController::removeSelectedNodes() const +{ + if (_activeScene) + { + // Remove all selected connections first + auto selConnections = _activeScene->getNodes<GraphConnectionNode>(true); + + for (const auto& node : selConnections) + { + if (auto connection = node->connection().lock()) + removeConnection(connection.get()); + } + + // Remove all selected blocks next + auto selBlocks = _activeScene->getNodes<GraphBlockNode>(true); + + for (const auto& node : selBlocks) + { + if (auto block = node->block().lock()) + removeBlock(block.get()); + } + } +} + +BlockHierarchy PipelineController::createBlockHierarchy() const +{ + if (_activePipeline) + { + return callControllerFunction("Calculating the graph layout", [this]() { + return BlockHierarchy{_activePipeline}; + }); + } + else + return BlockHierarchy{}; +} + +void PipelineController::controllerFunctionCalled() const +{ + // Redraw the scene after calling a controller function + if (_activeScene) + _activeScene->update(); +} + +void PipelineController::validateBlockName(QString name, const Block* block) const +{ + auto pipeline = block ? block->pipeline() : _activePipeline; + + // Name must be non-empty and unique + if (!name.isEmpty()) + { + auto existingBlock = pipeline->blocks().selectByName(name).get(); + + if (existingBlock && existingBlock != block) + throw PipelineException{pipeline, _EXCPT(QString{"A block with the name '%1' already exists"}.arg(name))}; + } + else + throw PipelineException{pipeline, _EXCPT("The block name may not be empty")}; +} + +void PipelineController::blockCreated(const std::shared_ptr<Block>& block) const +{ + if (_activeScene) + _activeScene->createBlockNode(block); +} + +void PipelineController::blockRemoved(const std::shared_ptr<Block>& block) const +{ + if (_activeScene) + _activeScene->removeBlockNode(block); +} + +void PipelineController::connectionCreated(const std::shared_ptr<Connection>& connection) const +{ + if (_activeScene) + _activeScene->createConnectionNode(connection); +} + +void PipelineController::connectionRemoved(const std::shared_ptr<Connection>& connection) const +{ + if (_activeScene) + _activeScene->removeConnectionNode(connection); +} diff --git a/Grinder/controller/PipelineController.h b/Grinder/controller/PipelineController.h new file mode 100644 index 0000000000000000000000000000000000000000..98c36ffa0b69be4a4bb6eca4eb72d58e00145127 --- /dev/null +++ b/Grinder/controller/PipelineController.h @@ -0,0 +1,88 @@ +/****************************************************************************** + * File: PipelineController.h + * Date: 18.1.2018 + *****************************************************************************/ + +#ifndef PIPELINECONTROLLER_H +#define PIPELINECONTROLLER_H + +#include <memory> + +#include "GenericController.h" +#include "pipeline/BlockType.h" +#include "pipeline/BlockHierarchy.h" +#include "ui/graph/GraphScene.h" + +namespace grndr +{ + class Pipeline; + class PipelineItem; + class Block; + class Connection; + class Port; + class GraphView; + class GraphScene; + + class PipelineController : public GenericController + { + Q_OBJECT + + public: + PipelineController(); + + public: + void assignUiComponents(GraphView* view); + + public: + void showPipeline(Pipeline* pipeline); + void hidePipeline() { showPipeline(nullptr); } + Pipeline* activePipeline() { return _activePipeline; } + const Pipeline* activePipeline() const { return _activePipeline; } + + GraphScene* activeScene() { return _activeScene.get(); } + const GraphScene* activeScene() const { return _activeScene.get(); } + + public: + std::shared_ptr<Block> createBlock(BlockType type, QString name = "") const; + void removeBlock(const Block* block) const; + + void disconnectPortFromAll(Port* port) const; + + void validateConnection(const Port* sourcePort, const Port* destPort) const; + std::shared_ptr<Connection> createConnection(Port* sourcePort, Port* destPort) const; + void removeConnection(const Connection* connection) const; + + void renameBlock(Block* block, QString newName) const; + void renamePipelineItem(PipelineItem* item, QString newName) const; + + void removeSelectedNodes() const; + + public: + BlockHierarchy createBlockHierarchy() const; + + signals: + void pipelineSwitching(Pipeline*); + void pipelineSwitched(Pipeline*); + + protected: + virtual bool canCallControllerFunction() const override { return _activePipeline != nullptr; } + virtual void controllerFunctionCalled() const override; + + private: + void validateBlockName(QString name, const Block* block = nullptr) const; + + private slots: + void blockCreated(const std::shared_ptr<Block>& block) const; + void blockRemoved(const std::shared_ptr<Block>& block) const; + void connectionCreated(const std::shared_ptr<Connection>& connection) const; + void connectionRemoved(const std::shared_ptr<Connection>& connection) const; + + private: + GraphView* _view{nullptr}; + + Pipeline* _activePipeline{nullptr}; + std::unique_ptr<GraphScene> _activeScene; + }; +} + +#endif diff --git a/Grinder/controller/ProjectController.cpp b/Grinder/controller/ProjectController.cpp new file mode 100644 index 0000000000000000000000000000000000000000..f8efed3f663b65658a36819c0249cd608796b33d --- /dev/null +++ b/Grinder/controller/ProjectController.cpp @@ -0,0 +1,323 @@ +/****************************************************************************** + * File: ProjectController.cpp + * Date: 07.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ProjectController.h" +#include "PipelineController.h" +#include "core/GrinderApplication.h" +#include "project/Project.h" +#include "project/ProjectExceptions.h" +#include "project/serialization/JsonSettingsCodec.h" +#include "project/serialization/ProjectSerializer.h" +#include "ui/mainwnd/LabelsListWidget.h" +#include "ui/mainwnd/ImageReferencesListWidget.h" +#include "util/ImageUtils.h" +#include "util/TemporaryStatusMessage.h" +#include "util/UIUtils.h" + +ProjectController::ProjectController(Project* project) : GenericController("Project"), + _project{project} +{ + if (!project) + throw std::invalid_argument{_EXCPT("project may not be null")}; + + // Listen for project events + connect(_project, &Project::labelCreated, this, &ProjectController::labelCreated); + connect(_project, &Project::labelRemoved, this, &ProjectController::labelRemoved); + connect(_project, &Project::imageReferenceCreated, this, &ProjectController::imageReferenceCreated); + connect(_project, &Project::imageReferenceRemoved, this, &ProjectController::imageReferenceRemoved); +} + +void ProjectController::assignUiComponents(LabelsListWidget* labelsList, ImageReferencesListWidget* imageRefsList) +{ + if (!labelsList) + throw std::invalid_argument{_EXCPT("labelsList may not be null")}; + + if (!imageRefsList) + throw std::invalid_argument{_EXCPT("imageRefsList may not be null")}; + + _labelsList = labelsList; + _imageReferencesList = imageRefsList; +} + +void ProjectController::clearProject(bool createDefaultLabel) +{ + emit projectClearing(); + + _suppressErrors = true; + + // Before clearing the project, make sure that no label or image reference is active + switchLabel(nullptr); + switchImageReference(nullptr); + + _project->clear(); + + if (createDefaultLabel) + { + if (auto label = grinder()->projectController().createLabel("Default label")) + switchLabel(label.get()); + } + + _suppressErrors = false; + + setCurrentProjectFile(""); + emit projectCleared(); +} + +void ProjectController::loadProject(QString fileName) +{ + emit projectLoading(fileName); + + bool result = callControllerFunction("Loading a project", [this](QString fileName) { + TemporaryStatusMessage tmpStatusMessage{QString{"Loading project '%1'..."}.arg(fileName)}; + + clearProject(); + + JsonSettingsCodec codec; + SettingsContainer projectSettings; + codec.loadContainer(projectSettings, fileName); + + ProjectSerializer serializer{_project}; + serializer.deserializeProject(projectSettings); + + return true; + }, fileName); + + if (result) + { + setCurrentProjectFile(fileName); + emit projectLoaded(fileName); + + // If no label is active, activate the first one after loading + if (!_activeLabel && !_project->labels().empty()) + switchLabel(_project->labels().front().get()); + + // If no image reference is active, activate the first one after loading + if (!_activeImageReference && !_project->imageReferences().empty()) + switchImageReference(_project->imageReferences().front().get()); + } + else + { + clearProject(); + throw ProjectException{_project, _EXCPT(QString{"Failed to load project '%1'"}.arg(fileName))}; + } +} + +void ProjectController::saveProject(QString fileName) +{ + emit projectSaving(fileName); + + bool result = callControllerFunction("Saving the current project", [this](QString fileName) { + TemporaryStatusMessage tmpStatusMessage{QString{"Saving project to '%1'..."}.arg(fileName)}; + + ProjectSerializer serializer{_project}; + auto projectSettings = serializer.serializeProject(); + + JsonSettingsCodec codec; + codec.saveContainer(projectSettings, fileName); + + return true; + }, fileName); + + if (result) + { + setCurrentProjectFile(fileName); + emit projectSaved(fileName); + } + else + throw ProjectException{_project, _EXCPT(QString{"Failed to save the current project to '%1'"}.arg(fileName))}; +} + +bool ProjectController::isProjectDirty() const +{ + // If there are no current settings, the application has just been started + if (_currentProjectSettings.isEmpty()) + return false; + + // Compare the current project data to the saved data; if they differ, the project has been changed + ProjectSerializer serializer{_project}; + auto projectSettings = serializer.serializeProject(); + + return projectSettings != _currentProjectSettings; +} + +void ProjectController::switchLabel(Label* label) +{ + if (label == _activeLabel) + return; + + if (_activeLabel) + emit labelSwitching(_activeLabel); + + _activeLabel = label; + + // Unset any currently active pipeline + grinder()->pipelineController().hidePipeline(); + + if (label) // Show the pipeline associated with the label + grinder()->pipelineController().showPipeline(label->pipeline()); + + emit labelSwitched(_activeLabel); +} + +void ProjectController::switchImageReference(ImageReference* imageRef) +{ + if (imageRef == _activeImageReference) + return; + + if (_activeImageReference) + emit imageReferenceSwitching(_activeImageReference); + + _activeImageReference = imageRef; + emit imageReferenceSwitched(_activeImageReference); +} + +std::shared_ptr<Label> ProjectController::createLabel(QString name) const +{ + return callControllerFunction("Creating a new label", [this](QString name) { + validateLabelName(name); + return _project->createLabel(name); + }, name); +} + +void ProjectController::removeLabel(const Label* label) +{ + if (label) + { + // If removing the active label, hide it first + if (_activeLabel == label) + switchLabel(nullptr); + + callControllerFunction("Removing a label", [this](const Label* label) { + _project->removeLabel(label); + return true; + }, label); + } +} + +void ProjectController::removeAllLabels() +{ + auto labels = _project->labels(); + + for (const auto& label : labels) + removeLabel(label.get()); +} + +void ProjectController::renameLabel(Label* label, QString newName) const +{ + callControllerFunction("Renaming a label", [this](Label* label, QString newName) { + validateLabelName(newName, label); + label->setName(newName); + return true; + }, label, newName); +} + +std::shared_ptr<ImageReference> ProjectController::createImageReference(QString imagePath) const +{ + TemporaryStatusMessage tmpStatusMessage; + + return callControllerFunction("Creating an image reference", [this, &tmpStatusMessage](QString imagePath) { + tmpStatusMessage.setMessage(QString{"Adding image '%1'..."}.arg(imagePath)); + return _project->createImageReference(imagePath); + }, imagePath); +} + +std::vector<std::shared_ptr<ImageReference>> ProjectController::createImageReferences(QStringList imagePaths, bool ignoreUnknownFiletypes) const +{ + static auto imageFormats = ImageUtils::getSupportedFormats(); + + std::vector<std::shared_ptr<ImageReference>> images; + + for (auto imagePath : imagePaths) + { + if (ignoreUnknownFiletypes) + { + QFileInfo fileInfo{imagePath}; + + if (!imageFormats.contains(fileInfo.suffix(), Qt::CaseInsensitive)) + continue; + } + + images.push_back(createImageReference(imagePath)); + } + + return images; +} + +void ProjectController::removeImageReference(const ImageReference* imageRef) +{ + if (imageRef) + { + // If removing the active image reference, unset it first + if (_activeImageReference == imageRef) + switchImageReference(nullptr); + + callControllerFunction("Removing an image reference", [this](const ImageReference* imageRef) { + _project->removeImageReference(imageRef); + return true; + }, imageRef); + } +} + +void ProjectController::removeAllImageReferences() +{ + auto imageRefs = _project->imageReferences(); + + for (const auto& imageRef : imageRefs) + removeImageReference(imageRef.get()); +} + +void ProjectController::validateLabelName(QString name, const Label* label) const +{ + // Name must be non-empty and unique + if (!name.isEmpty()) + { + auto existingLabel = _project->labels().selectByName(name).get(); + + if (existingLabel && existingLabel != label) + throw ProjectException{_project, _EXCPT(QString{"A label with the name '%1' already exists"}.arg(name))}; + } + else + throw ProjectException{_project, _EXCPT("The label name may not be empty")}; +} + +void ProjectController::updateCurrentProjectData() +{ + ProjectSerializer serializer{_project}; + _currentProjectSettings = serializer.serializeProject(); +} + +void ProjectController::labelCreated(const std::shared_ptr<Label>& label) const +{ + if (_labelsList) + _labelsList->addObject(label.get()); +} + +void ProjectController::labelRemoved(const std::shared_ptr<Label>& label) const +{ + if (_labelsList) + _labelsList->removeObject(label.get()); +} + +void ProjectController::imageReferenceCreated(const std::shared_ptr<ImageReference>& imageRef) const +{ + if (_imageReferencesList) + _imageReferencesList->addObject(imageRef.get()); +} + +void ProjectController::imageReferenceRemoved(const std::shared_ptr<ImageReference>& imageRef) const +{ + if (_imageReferencesList) + _imageReferencesList->removeObject(imageRef.get()); +} + +void ProjectController::setCurrentProjectFile(QString fileName) +{ + _currentProjectFile = fileName; + emit currentProjectFileChanged(fileName); + + // Whenever the project file has changed, the current project data needs to be updated, so do it here + updateCurrentProjectData(); +} diff --git a/Grinder/controller/ProjectController.h b/Grinder/controller/ProjectController.h new file mode 100644 index 0000000000000000000000000000000000000000..e3cad83cd2f01ed3c3e04150b648ddf8f1bc110e --- /dev/null +++ b/Grinder/controller/ProjectController.h @@ -0,0 +1,102 @@ +/****************************************************************************** + * File: ProjectController.h + * Date: 07.2.2018 + *****************************************************************************/ + +#ifndef PROJECTCONTROLLER_H +#define PROJECTCONTROLLER_H + +#include <memory> + +#include "GenericController.h" +#include "project/serialization/SettingsContainer.h" + +namespace grndr +{ + class Project; + class PipelineController; + class Label; + class LabelsListWidget; + class ImageReference; + class ImageReferencesListWidget; + + class ProjectController : public GenericController + { + Q_OBJECT + + public: + ProjectController(Project* project); + + public: + void assignUiComponents(LabelsListWidget* labelsList, ImageReferencesListWidget* imageRefsList); + + public: + void clearProject(bool createDefaultLabel = false); + void loadProject(QString fileName); + void saveProject(QString fileName); + + QString getCurrentProjectFile() const { return _currentProjectFile; } + bool isProjectDirty() const; + + public: + void switchLabel(Label* label); + Label* activeLabel() { return _activeLabel; } + const Label* activeLabel() const { return _activeLabel; } + + void switchImageReference(ImageReference* imageRef); + ImageReference* activeImageReference() { return _activeImageReference; } + const ImageReference* activeImageReference() const { return _activeImageReference; } + + public: + std::shared_ptr<Label> createLabel(QString name = "") const; + void removeLabel(const Label* label); + void removeAllLabels(); + void renameLabel(Label* label, QString newName) const; + + std::shared_ptr<ImageReference> createImageReference(QString imagePath) const; + std::vector<std::shared_ptr<ImageReference>> createImageReferences(QStringList imagePaths, bool ignoreUnknownFiletypes = true) const; + void removeImageReference(const ImageReference* imageRef); + void removeAllImageReferences(); + + signals: + void projectClearing(); + void projectCleared(); + void projectLoading(QString); + void projectLoaded(QString); + void projectSaving(QString); + void projectSaved(QString); + + void currentProjectFileChanged(QString fileName); + + void labelSwitching(Label*); + void labelSwitched(Label*); + void imageReferenceSwitching(ImageReference*); + void imageReferenceSwitched(ImageReference*); + + private: + void validateLabelName(QString name, const Label* label = nullptr) const; + + void setCurrentProjectFile(QString fileName); + void updateCurrentProjectData(); + + private slots: + void labelCreated(const std::shared_ptr<Label>& label) const; + void labelRemoved(const std::shared_ptr<Label>& label) const; + + void imageReferenceCreated(const std::shared_ptr<ImageReference>& imageRef) const; + void imageReferenceRemoved(const std::shared_ptr<ImageReference>& imageRef) const; + + private: + Project* _project{nullptr}; + QString _currentProjectFile{""}; + SettingsContainer _currentProjectSettings; + + LabelsListWidget* _labelsList{nullptr}; + ImageReferencesListWidget* _imageReferencesList{nullptr}; + + Label* _activeLabel{nullptr}; + ImageReference* _activeImageReference{nullptr}; + }; +} + +#endif diff --git a/Grinder/core/GrinderApplication.cpp b/Grinder/core/GrinderApplication.cpp new file mode 100644 index 0000000000000000000000000000000000000000..9099ea5a4422239f344576406ed7b67c5e66725f --- /dev/null +++ b/Grinder/core/GrinderApplication.cpp @@ -0,0 +1,80 @@ +/****************************************************************************** + * File: GrinderApplication.cpp + * Date: 11.1.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "GrinderApplication.h" +#include "Version.h" +#include "pipeline/BlockCatalog.h" +#include "image/DraftItemCatalog.h" +#include "ui/StyleSheet.h" +#include "res/Resources.h" + +GrinderApplication* GrinderApplication::s_appInstance = nullptr; + +QFont GrinderApplication::boldFont(QWidget* widget) +{ + QFont font; + + if (widget) + font = widget->font(); + + font.setBold(true); + return font; +} + +GrinderApplication::GrinderApplication(int& argc, char** argv, int flags) : QApplication(argc, argv, flags), + _settings{GRNDR_INFO_COMPANY, GRNDR_INFO_TITLE}, _projectController{&_project}, _engineController{&_engine}, _imageEditorManager{&_pipelineManager} +{ + s_appInstance = this; + + // Basic app information + setApplicationName(GRNDR_INFO_TITLE); + setApplicationVersion(GetVersionString(true)); + setOrganizationName(GRNDR_INFO_COMPANY); + setOrganizationDomain(GRNDR_INFO_WEBSITE); + + setWindowIcon(QIcon{FILE_ICON_MAIN}); + + // Apply font quality fixes for some OSes +#if defined(Q_OS_LINUX) + QFont fnt = font(); + + fnt.setStyleStrategy(QFont::StyleStrategy(QFont::PreferAntialias|QFont::PreferQuality)); + setFont(fnt); +#else + fixFontQuality("QMessageBox"); + fixFontQuality("QMenuBar"); + fixFontQuality("QMenu"); + fixFontQuality("QStatusBar"); +#endif + + // Apply global stylesheet + setStyleSheet(StyleSheet::loadStyleSheet(FILE_STYLESHEET_GLOBAL)); + + // Register standard types in all catalogs + BlockCatalog::registerStandardBlocks(); + DraftItemCatalog::registerStandardDraftItems(); +} + +GrinderApplication::~GrinderApplication() +{ + s_appInstance = nullptr; +} + +int GrinderApplication::run() +{ + _mainWindow = std::make_unique<GrinderWindow>(); + _mainWindow->show(); + + return exec(); +} + +void GrinderApplication::fixFontQuality(const char* ctrlClass) +{ + QFont fnt = font(ctrlClass); + + fnt.setStyleStrategy(QFont::StyleStrategy(QFont::PreferAntialias|QFont::PreferQuality)); + setFont(fnt, ctrlClass); +} diff --git a/Grinder/core/GrinderApplication.h b/Grinder/core/GrinderApplication.h new file mode 100644 index 0000000000000000000000000000000000000000..0ecb9519cfadfb0e966baf0bfa21f3e683be240d --- /dev/null +++ b/Grinder/core/GrinderApplication.h @@ -0,0 +1,89 @@ +/****************************************************************************** + * File: GrinderApplication.h + * Date: 11.1.2018 + *****************************************************************************/ + +#ifndef GRINDERAPPLICATION_H +#define GRINDERAPPLICATION_H + +#include <QApplication> + +#include "GrinderSettings.h" +#include "pipeline/PipelineManager.h" +#include "project/Project.h" +#include "engine/Engine.h" +#include "controller/PipelineController.h" +#include "controller/ProjectController.h" +#include "controller/EngineController.h" +#include "ui/mainwnd/GrinderWindow.h" +#include "ui/image/ImageEditorManager.h" + +namespace grndr +{ + class PipelineManager; + + class GrinderApplication : public QApplication + { + static GrinderApplication* s_appInstance; + + public: + static GrinderApplication* instance() { Q_ASSERT(s_appInstance != nullptr); return s_appInstance; } + + static QFont boldFont(QWidget* widget = nullptr); + + public: + GrinderApplication(int &argc, char **argv, int flags = ApplicationFlags); + virtual ~GrinderApplication(); + + public: + int run(); + + public: + GrinderSettings& settings() { return _settings; } + const GrinderSettings& settings() const { return _settings; } + + PipelineManager& pipelineManager() { return _pipelineManager; } + const PipelineManager& pipelineManager() const { return _pipelineManager; } + PipelineController& pipelineController() { return _pipelineController; } + const PipelineController& pipelineController() const { return _pipelineController; } + + Project& project() { return _project; } + const Project& project() const { return _project; } + ProjectController& projectController() { return _projectController; } + const ProjectController& projectController() const { return _projectController; } + + Engine& engine() { return _engine; } + const Engine& engine() const { return _engine; } + EngineController& engineController() { return _engineController; } + const EngineController& engineController() const { return _engineController; } + + GrinderWindow* mainWindow() { return _mainWindow.get(); } + const GrinderWindow* mainWindow() const { return _mainWindow.get(); } + + ImageEditorManager& imageEditorManager() { return _imageEditorManager; } + const ImageEditorManager& imageEditorManager() const { return _imageEditorManager; } + + private: + void fixFontQuality(const char* ctrlClass); + + private: + GrinderSettings _settings; + + PipelineManager _pipelineManager; + PipelineController _pipelineController; + + Project _project; + ProjectController _projectController; + + Engine _engine; + EngineController _engineController; + + std::unique_ptr<GrinderWindow> _mainWindow; + + ImageEditorManager _imageEditorManager; + }; + + inline GrinderApplication* grinder() { return GrinderApplication::instance(); } +} + +#endif diff --git a/Grinder/core/GrinderExceptions.cpp b/Grinder/core/GrinderExceptions.cpp new file mode 100644 index 0000000000000000000000000000000000000000..1037a3ad767044aeb7a4faa27bb0a4759cb6f276 --- /dev/null +++ b/Grinder/core/GrinderExceptions.cpp @@ -0,0 +1,39 @@ +/****************************************************************************** + * File: ExceptionHandling.cpp + * Date: 17.1.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "GrinderExceptions.h" + +QString grndr::GetExceptionMessage(QString what, QString* func, QString* file, QString* line) +{ + QStringList tokens = what.split("|"); + + if (func && tokens.size() >= 1) + *func = tokens[0]; + + if (file && tokens.size() >= 2) + *file = tokens[1]; + + if (line && tokens.size() >= 3) + *line = tokens[2]; + + return tokens.last(); +} + +void grndr::ShowExceptionMessage(QString what) +{ + QString str; + + if (!what.isEmpty()) + { + QString func, file, line; + what = GetExceptionMessage(what, &func, &file, &line); + str = QString{"%1<br><em>%2 (%3:%4)</em>"}.arg(what).arg(func).arg(file).arg(line); + } + else + str = "Unknown exception"; + + QMessageBox::critical(nullptr, "Uncaught exception", QString{"<b>An unhandled exception occurred:</b><br><br>%1<br><br>The application will now close."}.arg(str)); +} diff --git a/Grinder/core/GrinderExceptions.h b/Grinder/core/GrinderExceptions.h new file mode 100644 index 0000000000000000000000000000000000000000..a85bb8bee675c465b47749a3bbe95715e3da5ef1 --- /dev/null +++ b/Grinder/core/GrinderExceptions.h @@ -0,0 +1,26 @@ +/****************************************************************************** + * File: ExceptionHandling.h + * Date: 17.1.2018 + *****************************************************************************/ + +#ifndef EXCEPTIONHANDLING_H +#define EXCEPTIONHANDLING_H + +#include <QString> +#include <stdexcept> + +namespace grndr +{ + class GrinderException : public std::runtime_error + { + public: + GrinderException(QString what) : std::runtime_error{what.toStdString()} { } + }; + + QString GetExceptionMessage(QString what, QString* func = nullptr, QString* file = nullptr, QString* line = nullptr); + void ShowExceptionMessage(QString what); +} + +#define _EXCPT(what) QString{"%1|%2|%3|%4"}.arg(__func__).arg(__FILE__).arg(__LINE__).arg(what).toLatin1().data() + +#endif diff --git a/Grinder/core/GrinderSettings.cpp b/Grinder/core/GrinderSettings.cpp new file mode 100644 index 0000000000000000000000000000000000000000..a484dbee4cf0bd6ea27d90d6c6bc1a7d9d5535ad --- /dev/null +++ b/Grinder/core/GrinderSettings.cpp @@ -0,0 +1,145 @@ +/****************************************************************************** + * File: GrinderSettings.cpp + * Date: 16.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "GrinderSettings.h" + +GrinderSettings::GrinderSettings(QString organization, QString application) : QSettings(organization, application) +{ + loadSettings(); +} + +void GrinderSettings::loadSettings() +{ + loadGeneralOptions(); + loadStartupOptions(); + loadProjectFilesMRUList(); +} + +void GrinderSettings::saveSettings(bool notify) +{ + saveGeneralOptions(); + saveStartupOptions(); + saveProjectFilesMRUList(); + + if (notify) + emit settingsChanged(); +} + +void GrinderSettings::getWindowState(QString name, QMainWindow* window) +{ + beginGroup("WindowStates"); + beginGroup(name); + window->restoreGeometry(value("Geometry").toByteArray()); + window->restoreState(value("State").toByteArray()); + endGroup(); + endGroup(); +} + +void GrinderSettings::setWindowState(QString name, const QMainWindow* window) +{ + beginGroup("WindowStates"); + beginGroup(name); + setValue("Geometry", window->saveGeometry()); + setValue("State", window->saveState()); + endGroup(); + endGroup(); +} + +void GrinderSettings::getSplitterState(QString name, QSplitter* splitter) +{ + beginGroup("SplitterStates"); + beginGroup(name); + splitter->restoreState(value("State").toByteArray()); + endGroup(); + endGroup(); +} + +void GrinderSettings::setSplitterState(QString name, const QSplitter* splitter) +{ + beginGroup("SplitterStates"); + beginGroup(name); + setValue("State", splitter->saveState()); + endGroup(); + endGroup(); +} + +QString GrinderSettings::getFileDialogDir(QString dlgName) +{ + beginGroup("FileDialogDirs"); + QString dir = value(dlgName, "").toString(); + endGroup(); + + return dir; +} + +void GrinderSettings::setFileDialogDir(QString dlgName, QString dir) +{ + beginGroup("FileDialogDirs"); + setValue(dlgName, dir); + endGroup(); +} + +void GrinderSettings::loadGeneralOptions() +{ + beginGroup("GeneralOptions"); + _generalOptions.askOnUnsavedChanges = value("AskOnUnsavedChanges", true).toBool(); + endGroup(); +} + +void GrinderSettings::saveGeneralOptions() +{ + beginGroup("GeneralOptions"); + setValue("AskOnUnsavedChanges", _generalOptions.askOnUnsavedChanges); + endGroup(); +} + +void GrinderSettings::loadStartupOptions() +{ + beginGroup("StartupOptions"); + _startupOptions.loadLastProject = value("LoadLastProject", false).toBool(); + _startupOptions.lastProjectFile = value("LastProjectFile", "").toString(); + endGroup(); +} + +void GrinderSettings::saveStartupOptions() +{ + beginGroup("StartupOptions"); + setValue("LoadLastProject", _startupOptions.loadLastProject); + setValue("LastProjectFile", _startupOptions.lastProjectFile); + endGroup(); +} + +void GrinderSettings::loadImageEditorOptions() +{ + beginGroup("StartupOptions"); + _imageEditorOptions.groupIntoTabs = value("GroupIntoTabs", true).toBool(); + _imageEditorOptions.startUndocked = value("StartUndocked", false).toBool(); + _imageEditorOptions.autoFitToWindow = value("AutoFitToWindow", false).toBool(); + endGroup(); +} + +void GrinderSettings::saveImageEditorOptions() +{ + beginGroup("StartupOptions"); + setValue("GroupIntoTabs", _imageEditorOptions.groupIntoTabs); + setValue("StartUndocked", _imageEditorOptions.startUndocked ); + setValue("AutoFitToWindow", _imageEditorOptions.autoFitToWindow); + endGroup(); +} + +void GrinderSettings::loadProjectFilesMRUList() +{ + beginGroup("MRULists"); + _recentProjects = value("ProjectFiles").toStringList(); + endGroup(); +} + +void GrinderSettings::saveProjectFilesMRUList() +{ + beginGroup("MRULists"); + setValue("ProjectFiles", static_cast<QStringList>(_recentProjects)); + endGroup(); +} diff --git a/Grinder/core/GrinderSettings.h b/Grinder/core/GrinderSettings.h new file mode 100644 index 0000000000000000000000000000000000000000..45b843b02e9f2498d669fd46353e8431e3bec29a --- /dev/null +++ b/Grinder/core/GrinderSettings.h @@ -0,0 +1,95 @@ +/****************************************************************************** + * File: GrinderSettings.h + * Date: 16.2.2018 + *****************************************************************************/ + +#ifndef GRINDERSETTINGS_H +#define GRINDERSETTINGS_H + +#include <QSettings> + +#include "common/MRUStringList.h" + +namespace grndr +{ + class GrinderSettings : public QSettings + { + Q_OBJECT + + public: + struct GeneralOptions + { + bool askOnUnsavedChanges{true}; + }; + + struct StartupOptions + { + bool loadLastProject{false}; + QString lastProjectFile{""}; + }; + + struct ImageEditorOptions + { + bool groupIntoTabs{true}; + bool startUndocked{false}; + + bool autoFitToWindow{false}; + }; + + public: + GrinderSettings(QString organization, QString application); + virtual ~GrinderSettings() { saveSettings(false); } + + public: + void loadSettings(); + void saveSettings(bool notify = true); + + public: + GeneralOptions& generalOptions() { return _generalOptions; } + const GeneralOptions& generalOptions() const { return _generalOptions; } + + StartupOptions& startupOptions() { return _startupOptions; } + const StartupOptions& startupOptions() const { return _startupOptions; } + + ImageEditorOptions& imageEditorOptions() { return _imageEditorOptions; } + const ImageEditorOptions& imageEditorOptions() const { return _imageEditorOptions; } + + MRUStringList& recentProjects() { return _recentProjects; } + const MRUStringList& recentProjects() const { return _recentProjects; } + + public: + void getWindowState(QString name, QMainWindow* window); + void setWindowState(QString name, const QMainWindow* window); + + void getSplitterState(QString name, QSplitter* splitter); + void setSplitterState(QString name, const QSplitter* splitter); + + QString getFileDialogDir(QString dlgName); + void setFileDialogDir(QString dlgName, QString dir); + + signals: + void settingsChanged(); + + private: + void loadGeneralOptions(); + void saveGeneralOptions(); + + void loadStartupOptions(); + void saveStartupOptions(); + + void loadImageEditorOptions(); + void saveImageEditorOptions(); + + void loadProjectFilesMRUList(); + void saveProjectFilesMRUList(); + + private: + GeneralOptions _generalOptions; + StartupOptions _startupOptions; + ImageEditorOptions _imageEditorOptions; + + MRUStringList _recentProjects{false, true, 8}; + }; +} + +#endif diff --git a/Grinder/engine/Engine.cpp b/Grinder/engine/Engine.cpp new file mode 100644 index 0000000000000000000000000000000000000000..b2578f4254703e747c76bf82a5d4c4ee62763f23 --- /dev/null +++ b/Grinder/engine/Engine.cpp @@ -0,0 +1,73 @@ +/****************************************************************************** + * File: Engine.cpp + * Date: 21.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "Engine.h" +#include "EngineExecutionContext.h" +#include "EngineExceptions.h" +#include "Processor.h" +#include "pipeline/Pipeline.h" +#include "project/Label.h" + +void Engine::executeLabel(Label* label, ExecutionMode mode) const +{ + executeLabelEx(label, nullptr, mode); +} + +cv::Mat Engine::executeLabelEx(Label* label, const Port* requestPort, ExecutionMode mode) const +{ + cv::Mat portData; + + try { + // We first need a block hierarchy to execute + BlockHierarchy blockHierarchy{label->pipeline()}; + + if (!blockHierarchy.empty()) + { + EngineExecutionContext ctx{this, label, mode}; + + for (unsigned int currentLevel = 0; currentLevel < blockHierarchy.size(); ++currentLevel) + { + executeHierarchyLevel(blockHierarchy.at(currentLevel), ctx, requestPort ? requestPort->block() : nullptr); + + // Clean up unused data in the context + ctx.purge(blockHierarchy, currentLevel); + + // If data at a specific port was requested, copy it to portData + if (requestPort) + { + if (auto dataBlob = ctx.contextEntry(requestPort)) + portData = dataBlob->getMatrix(); + } + + if (ctx.wasAborted()) + break; + } + } + } catch (std::exception& e) { + // Forward any exceptions as an EngineException + throw EngineException{this, _EXCPT(QString{"Executing '%2' failed: %1"}.arg(e.what()).arg(label->getName()))}; + } + + return portData; +} + +void Engine::executeHierarchyLevel(const BlockHierarchy::HierarchyLevel& level, EngineExecutionContext& ctx, const Block* finalBlock) const +{ + for (const auto& block : level) + { + // Create a processor for the block and execute it + if (auto processor = block->createProcessor()) + processor->execute(ctx); + else + throw EngineException{this, _EXCPT(QString{"No processor could be created for block '%1'"}.arg(block->getName()))}; + + if (block == finalBlock) + { + ctx.abortProcessing(); + break; + } + } +} diff --git a/Grinder/engine/Engine.h b/Grinder/engine/Engine.h new file mode 100644 index 0000000000000000000000000000000000000000..6d00af259296af8578cc6d8616ab07a908f2af71 --- /dev/null +++ b/Grinder/engine/Engine.h @@ -0,0 +1,42 @@ +/****************************************************************************** + * File: Engine.h + * Date: 21.2.2018 + *****************************************************************************/ + +#ifndef ENGINE_H +#define ENGINE_H + +#include <QObject> +#include <opencv2/core.hpp> + +#include "pipeline/BlockHierarchy.h" + +namespace grndr +{ + class EngineExecutionContext; + class Label; + class Block; + class Port; + + class Engine : public QObject + { + Q_OBJECT + + public: + enum class ExecutionMode + { + Execute, + View, + Write, + }; + + public: + void executeLabel(Label* label, ExecutionMode mode) const; + cv::Mat executeLabelEx(Label* label, const Port* requestPort, ExecutionMode mode) const; + + private: + void executeHierarchyLevel(const BlockHierarchy::HierarchyLevel& level, EngineExecutionContext& ctx, const Block* finalBlock) const; + }; +} + +#endif diff --git a/Grinder/engine/EngineExceptions.cpp b/Grinder/engine/EngineExceptions.cpp new file mode 100644 index 0000000000000000000000000000000000000000..7218f21d3353121dd57aaa314c59a2efa25f5941 --- /dev/null +++ b/Grinder/engine/EngineExceptions.cpp @@ -0,0 +1,20 @@ +/****************************************************************************** + * File: EngineExceptions.cpp + * Date: 21.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "EngineExceptions.h" +#include "Processor.h" + +EngineException::EngineException(const Engine* engine, QString what) : GrinderException(what), + _engine{engine} +{ + +} + +ProcessorException::ProcessorException(const ProcessorBase* processor, QString what) : EngineException(processor ? processor->engine() : nullptr, what), + _processor{processor} +{ + +} diff --git a/Grinder/engine/EngineExceptions.h b/Grinder/engine/EngineExceptions.h new file mode 100644 index 0000000000000000000000000000000000000000..eaeeb7548e2545d121bf3c18bfebe1b2fe01d978 --- /dev/null +++ b/Grinder/engine/EngineExceptions.h @@ -0,0 +1,43 @@ +/****************************************************************************** + * File: EngineExceptions.h + * Date: 21.2.2018 + *****************************************************************************/ + +#ifndef ENGINEEXCEPTIONS_H +#define ENGINEEXCEPTIONS_H + +#include <QString> + +#include "core/GrinderExceptions.h" + +namespace grndr +{ + class Engine; + class ProcessorBase; + + class EngineException : public GrinderException + { + public: + EngineException(const Engine* engine, QString what); + + public: + const Engine* engine() const { return _engine; } + + protected: + const Engine* _engine{nullptr}; + }; + + class ProcessorException : public EngineException + { + public: + ProcessorException(const ProcessorBase* processor, QString what); + + public: + const ProcessorBase* processor() const { return _processor; } + + protected: + const ProcessorBase* _processor{nullptr}; + }; +} + +#endif diff --git a/Grinder/engine/EngineExecutionContext.cpp b/Grinder/engine/EngineExecutionContext.cpp new file mode 100644 index 0000000000000000000000000000000000000000..d2545a40d79e84f7fa3f7c6ae3c9ba5b334be81c --- /dev/null +++ b/Grinder/engine/EngineExecutionContext.cpp @@ -0,0 +1,81 @@ +/****************************************************************************** + * File: EngineExecutionContext.cpp + * Date: 21.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "EngineExecutionContext.h" +#include "pipeline/Block.h" +#include "pipeline/Port.h" + +EngineExecutionContext::EngineExecutionContext(const Engine* engine, Label* label, Engine::ExecutionMode mode) : + _engine{engine}, _label{label}, _executionMode{mode} +{ + if (!engine) + throw std::invalid_argument{_EXCPT("engine may not be null")}; + + if (!label) + throw std::invalid_argument{_EXCPT("label may not be null")}; +} + +void EngineExecutionContext::purge(const BlockHierarchy& blockHierarchy, unsigned int reachedLevel) +{ + // Iterate over all levels lower than the reachedLevel and remove entries that are no longer needed + for (unsigned int level = 0; level < reachedLevel; ++level) + { + auto levelBlocks = blockHierarchy.at(level); + + for (const auto& block : levelBlocks) + { + for (const auto& port : block->ports().selectByDirection(Port::Direction::Out)) + { + auto it = _contextEntries.find(port.get()); + + if (it != _contextEntries.cend()) + { + if (canPurge(port.get(), blockHierarchy, reachedLevel)) + _contextEntries.erase(it); + } + } + } + } +} + +DataBlob* EngineExecutionContext::contextEntry(const Port* port) +{ + if (!port->isOut()) + throw std::invalid_argument{_EXCPT("Data can only be retrieved for out-ports")}; + + if (contains(port)) + return &_contextEntries.at(port); + else + return nullptr; +} + +void EngineExecutionContext::setContextEntry(const Port* port, const DataBlob& data) +{ + if (!port->isOut()) + throw std::invalid_argument{_EXCPT("Data can only be set for out-ports")}; + + _contextEntries.emplace(port, data); +} + +void EngineExecutionContext::setContextEntry(const Port* port, DataBlob&& data) +{ + if (!port->isOut()) + throw std::invalid_argument{_EXCPT("Data can only be set for out-ports")}; + + _contextEntries.emplace(port, std::move(data)); +} + +bool EngineExecutionContext::canPurge(const Port* port, const BlockHierarchy& blockHierarchy, unsigned int reachedLevel) const +{ + // Search for outgoing connections to blocks at a level higher than reachedLevel; if one exists, the port-data cannot be purged + for (const auto& con : port->connections()) + { + if (blockHierarchy.getBlockLevel(con->dest()) > static_cast<int>(reachedLevel)) + return false; + } + + return true; +} diff --git a/Grinder/engine/EngineExecutionContext.h b/Grinder/engine/EngineExecutionContext.h new file mode 100644 index 0000000000000000000000000000000000000000..0485434fa7b9ac6641aafcdd3ba7b0c4ff39e9d0 --- /dev/null +++ b/Grinder/engine/EngineExecutionContext.h @@ -0,0 +1,60 @@ +/****************************************************************************** + * File: EngineExecutionContext.h + * Date: 21.2.2018 + *****************************************************************************/ + +#ifndef ENGINEEXECUTIONCONTEXT_H +#define ENGINEEXECUTIONCONTEXT_H + +#include <map> + +#include "Engine.h" +#include "pipeline/BlockHierarchy.h" +#include "data/DataBlob.h" + +namespace grndr +{ + class Engine; + class Label; + class Port; + + class EngineExecutionContext + { + public: + EngineExecutionContext(const Engine* engine, Label* label, Engine::ExecutionMode mode); + + public: + void purge(const BlockHierarchy& blockHierarchy, unsigned int reachedLevel); + + public: + const Engine* engine() const { return _engine; } + Label* label() { return _label; } + const Label* label() const { return _label; } + + DataBlob* contextEntry(const Port* port); + void setContextEntry(const Port* port, const DataBlob& data); + void setContextEntry(const Port* port, DataBlob&& data); + + bool contains(const Port* port) const { return _contextEntries.find(port) != _contextEntries.end(); } + + Engine::ExecutionMode getExecutionMode() const { return _executionMode; } + void setExecutionMode(Engine::ExecutionMode mode) { _executionMode = mode; } + + bool wasAborted() const { return _abortProcessing; } + void abortProcessing() { _abortProcessing = true; } + + private: + bool canPurge(const Port* port, const BlockHierarchy& blockHierarchy, unsigned int reachedLevel) const; + + private: + const Engine* _engine{nullptr}; + Label* _label{nullptr}; + + std::map<const Port*, DataBlob> _contextEntries; + + Engine::ExecutionMode _executionMode{Engine::ExecutionMode::Execute}; + bool _abortProcessing{false}; + }; +} + +#endif diff --git a/Grinder/engine/Processor.h b/Grinder/engine/Processor.h new file mode 100644 index 0000000000000000000000000000000000000000..c44ba54c7fcdf8c8a5de7c1300f051b1a87e47f6 --- /dev/null +++ b/Grinder/engine/Processor.h @@ -0,0 +1,31 @@ +/****************************************************************************** + * File: Processor.h + * Date: 21.2.2018 + *****************************************************************************/ + +#ifndef PROCESSOR_H +#define PROCESSOR_H + +#include "ProcessorBase.h" + +namespace grndr +{ + class Block; + + template<typename BlockType> + class Processor : public ProcessorBase + { + public: + Processor(const Block* block); + + public: + const BlockType* block() const { return _block; } + + protected: + const BlockType* _block{nullptr}; + }; +} + +#include "Processor.impl.h" + +#endif diff --git a/Grinder/engine/Processor.impl.h b/Grinder/engine/Processor.impl.h new file mode 100644 index 0000000000000000000000000000000000000000..f5cf12618f45b832763515b1615e07892914532c --- /dev/null +++ b/Grinder/engine/Processor.impl.h @@ -0,0 +1,17 @@ +/****************************************************************************** + * File: Processor.impl.h + * Date: 21.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "Processor.h" +#include "EngineExceptions.h" +#include "pipeline/Block.h" + +template<typename BlockType> +Processor<BlockType>::Processor(const Block* block) : ProcessorBase(block), + _block{dynamic_cast<const BlockType*>(block)} +{ + if (!_block) + throw std::invalid_argument{_EXCPT("An invalid block type was requested")}; +} diff --git a/Grinder/engine/ProcessorBase.cpp b/Grinder/engine/ProcessorBase.cpp new file mode 100644 index 0000000000000000000000000000000000000000..6f03458ec46e75fa68297ca29663c4b8aa0751d8 --- /dev/null +++ b/Grinder/engine/ProcessorBase.cpp @@ -0,0 +1,85 @@ +/****************************************************************************** + * File: ProcessorBase.cpp + * Date: 21.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ProcessorBase.h" +#include "EngineExceptions.h" +#include "pipeline/Block.h" + +ProcessorBase::ProcessorBase(const Block* block) : + _block{block} +{ + if (!block) + throw std::invalid_argument{_EXCPT("block may not be null")}; +} + +void ProcessorBase::execute(EngineExecutionContext& ctx) +{ + _engine = ctx.engine(); + + if (!_engine) + throw std::invalid_argument{_EXCPT("Invalid execution context")}; +} + +DataDescriptor ProcessorBase::getPortDataDescriptor(const Port* port, unsigned int index) const +{ + return port->dataDescriptors().at(index); +} + +DataDescriptor ProcessorBase::getBestDataDescriptor(const DataDescriptor& dataDesc, const DataDescriptors& targetDescriptors) const +{ + for (const auto& targetDescriptor : targetDescriptors) + { + DataDescriptor targetDescAdjusted; + + if (dataDesc.match(targetDescriptor, &targetDescAdjusted)) // Found a perfect match <3 + return targetDescAdjusted; + else if (dataDesc.canConvertTo(targetDescriptor)) // Not perfect, but convertible + return targetDescriptor; + } + + // No valid descriptor found, throw an exception + throwProcessorException(_EXCPT("The input data is not compatible with the expected data type")); + return DataDescriptor{}; +} + +DataBlob* ProcessorBase::portData(EngineExecutionContext& ctx, const Port* port, bool convert, bool required) const +{ + auto dataPort = port; + + // If the given port is an in-port, find the out-port it is connected to + if (port->isIn()) + { + dataPort = nullptr; + + auto connections = port->getConnections(Port::Direction::In); + + if (connections.size() == 1) + dataPort = connections.at(0)->sourcePort(); + else if (connections.size() == 0 && required) + throwProcessorException(QString{"Port '%1' is not connected to a source"}.arg(port->getName())); + else if (connections.size() > 1) + throwProcessorException(QString{"Port '%1' is connected to more than one source"}.arg(port->getName())); // Should never happen + } + + DataBlob* data = dataPort ? ctx.contextEntry(dataPort) : nullptr; + + if (!data && required) + throwProcessorException(QString{"No data could be retrieved for port '%1'"}.arg(port->getName())); + + if (convert) + { + // Get the best matching target data descriptor; if none can be found, an exception will be thrown + auto targetDataDesc = getBestDataDescriptor(data->dataDescriptor(), port->dataDescriptors()); + data->convertTo(targetDataDesc); + } + + return data; +} + +void ProcessorBase::throwProcessorException(QString what) const +{ + throw ProcessorException{this, _EXCPT(QString{"%1: %2"}.arg(_block->getName()).arg(what))}; +} diff --git a/Grinder/engine/ProcessorBase.h b/Grinder/engine/ProcessorBase.h new file mode 100644 index 0000000000000000000000000000000000000000..65c2e40eeeee3dcd14733b7b6ae105a453c14f52 --- /dev/null +++ b/Grinder/engine/ProcessorBase.h @@ -0,0 +1,42 @@ +/****************************************************************************** + * File: ProcessorBase.h + * Date: 21.2.2018 + *****************************************************************************/ + +#ifndef PROCESSORBASE_H +#define PROCESSORBASE_H + +#include "engine/EngineExecutionContext.h" + +namespace grndr +{ + class Engine; + + class ProcessorBase + { + public: + ProcessorBase(const Block* block); + + public: + const Engine* engine() const { return _engine; } + const Block* block() const { return _block; } + + public: + virtual void execute(EngineExecutionContext& ctx); + + protected: + DataDescriptor getPortDataDescriptor(const Port* port, unsigned int index = 0) const; + DataDescriptor getBestDataDescriptor(const DataDescriptor& dataDesc, const DataDescriptors& targetDescriptors) const; + + DataBlob* portData(EngineExecutionContext& ctx, const Port* port, bool convert = true, bool required = true) const; + + protected: + void throwProcessorException(QString what) const; + + protected: + const Engine* _engine{nullptr}; + const Block* _block{nullptr}; + }; +} + +#endif diff --git a/Grinder/engine/data/DataBlob.cpp b/Grinder/engine/data/DataBlob.cpp new file mode 100644 index 0000000000000000000000000000000000000000..ad808f98ececcda72c921939667c5bc146567349 --- /dev/null +++ b/Grinder/engine/data/DataBlob.cpp @@ -0,0 +1,81 @@ +/****************************************************************************** + * File: DataBlob.cpp + * Date: 19.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "DataBlob.h" +#include "DataExceptions.h" + +#include <opencv2/imgproc.hpp> + +DataBlob::DataBlob(const DataDescriptor& dataDesc) : + _dataDescriptor{dataDesc} +{ + if (dataDesc.isArbitrary() || dataDesc.isAdaptive()) + throw DataException{_EXCPT("dataDesc may not be arbitrary or adaptive")}; +} + +void DataBlob::set(const cv::Mat& data) +{ + if (!data.empty()) + data.copyTo(_data); + else + clear(); +} + +void DataBlob::set(cv::Mat&& data) +{ + _data = std::move(data); +} + +void DataBlob::convertTo(const DataDescriptor& dataDesc, bool normalize, double minValue, double maxValue) +{ + if (!_dataDescriptor.canConvertTo(dataDesc) || dataDesc.isAdaptive()) + throw DataException{_EXCPT("Invalid conversion")}; + + DataDescriptor dataDescNew = _dataDescriptor; + + int channels, depth; + dataDesc.getCVMatrixType(&channels, &depth); + + try { + // First, convert the value type if necessary + if (dataDesc.getValueType() != DataDescriptor::ValueType::Any && depth != _data.depth()) + { + if (normalize && dataDesc.getValueType() < _dataDescriptor.getValueType()) // Normalize only if the new type is smaller than the current one + cv::normalize(_data, _data, maxValue, minValue, cv::NORM_MINMAX); + + _data.convertTo(_data, depth); + + // Update the new data descriptor to match the new value type + dataDescNew = DataDescriptor{dataDescNew.getName(), dataDescNew.getStructureType(), dataDescNew.getFieldType(), dataDesc.getValueType()}; + } + + // Next, convert image colors count if necessary + if (dataDesc.getFieldType() != DataDescriptor::FieldType::Any && channels != _data.channels()) + { + // Check if a color <-> grayscale conversion can be done + if (_dataDescriptor.canConvertToColor(dataDesc)) + { + cv::cvtColor(_data, _data, cv::COLOR_GRAY2BGR); + + // Update the new data descriptor to match the new field type + dataDescNew = DataDescriptor{dataDescNew.getName(), dataDescNew.getStructureType(), DataDescriptor::FieldType::Color, dataDescNew.getValueType()}; + } + else if (_dataDescriptor.canConvertToGrayscale(dataDesc)) + { + cv::cvtColor(_data, _data, cv::COLOR_BGR2GRAY); + + // Update the new data descriptor to match the new field type + dataDescNew = DataDescriptor{dataDescNew.getName(), dataDescNew.getStructureType(), DataDescriptor::FieldType::Basic, dataDescNew.getValueType()}; + } + } + + // Set the new data descriptor to match the converted type + _dataDescriptor = dataDescNew; + } catch (std::exception& e) { + // Forward exceptions from OpenCV as a DataException + throw DataException{_EXCPT(e.what())}; + } +} diff --git a/Grinder/engine/data/DataBlob.h b/Grinder/engine/data/DataBlob.h new file mode 100644 index 0000000000000000000000000000000000000000..173e64db502e8e64b897c4d9b3c6803a6339bac0 --- /dev/null +++ b/Grinder/engine/data/DataBlob.h @@ -0,0 +1,78 @@ +/****************************************************************************** + * File: DataBlob.h + * Date: 19.2.2018 + *****************************************************************************/ + +#ifndef DATABLOB_H +#define DATABLOB_H + +#include "DataDescriptor.h" + +#include <opencv2/core.hpp> + +namespace grndr +{ + class DataBlob + { + public: + DataBlob(const DataDescriptor& dataDesc); + template<typename DataType> + DataBlob(const DataDescriptor& dataDesc, const DataType& data); + template<typename DataType> + DataBlob(const DataDescriptor& dataDesc, DataType&& data); + DataBlob(const DataBlob& blob) = default; + DataBlob(DataBlob&& blob) = default; + + DataBlob& operator =(const DataBlob& blob) = default; + DataBlob& operator =(DataBlob&& blob) = default; + + DataBlob& operator =(const cv::Mat& data) { set(data); return *this; } + template<typename DataType> + DataBlob& operator =(const std::vector<DataType>& data) { set(data); return *this; } + template<typename DataType> + DataBlob& operator =(const DataType& data) { set(data); return *this; } + + operator cv::Mat() const { return getMatrix(); } + template<typename DataType> + operator std::vector<DataType>() const { return getVector<DataType>(); } + template<typename DataType> + operator DataType() const { return getScalar<DataType>(); } + + public: + void set(const cv::Mat& data); + void set(cv::Mat&& data); + template<typename DataType> + void set(const std::vector<DataType>& data); + template<typename DataType> + void set(const DataType& data); + + cv::Mat getMatrix() const { return _data; } + template<typename DataType> + std::vector<DataType> getVector() const; + template<typename DataType> + DataType getScalar() const; + + void convertTo(const DataDescriptor& dataDesc, bool normalize = false, double minValue = 0.0, double maxValue = 1.0); + template<typename TargetType> + void convertTo(const DataDescriptor& dataDesc); + + void clear() { _data.release(); } + + public: + const DataDescriptor& dataDescriptor() { return _dataDescriptor; } + + cv::Mat& data() { return _data; } + const cv::Mat& data() const { return _data; } + + bool empty() const { return _data.empty(); } + + private: + DataDescriptor _dataDescriptor; + + cv::Mat _data; + }; +} + +#include "DataBlob.impl.h" + +#endif diff --git a/Grinder/engine/data/DataBlob.impl.h b/Grinder/engine/data/DataBlob.impl.h new file mode 100644 index 0000000000000000000000000000000000000000..807cbe7b2ed023dc7509c72a40013b54d9d0144e --- /dev/null +++ b/Grinder/engine/data/DataBlob.impl.h @@ -0,0 +1,76 @@ +/****************************************************************************** + * File: DataBlob.impl.h + * Date: 19.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "DataBlob.h" + +#include <limits> + +template<typename DataType> +DataBlob::DataBlob(const DataDescriptor& dataDesc, const DataType& data) : DataBlob(dataDesc) +{ + set(data); +} + +template<typename DataType> +DataBlob::DataBlob(const DataDescriptor& dataDesc, DataType&& data) : DataBlob(dataDesc) +{ + set(std::move(data)); +} + +template<typename DataType> +void DataBlob::set(const std::vector<DataType>& data) +{ + if (data.size() > 0) + { + // Create a matrix with a single row that holds the vector data + _data.create(1, data.size(), _dataDescriptor.getCVMatrixType()); + + // Perform a direct memory copy of the data + DataType* matrixData = _data.ptr<DataType>(0); + std::memcpy(matrixData, data.data(), data.size() * sizeof(DataType)); + } + else + clear(); +} + +template<typename DataType> +void DataBlob::set(const DataType& data) +{ + // Create a matrix with a single row and a single column that holds the value + _data.create(1, 1, _dataDescriptor.getCVMatrixType()); + _data.at<DataType>(0, 0) = data; +} +template<typename DataType> +std::vector<DataType> DataBlob::getVector() const +{ + if (!_data.empty()) + { + std::vector<DataType> vec(_data.cols); + + // Perform a direct memory copy of the data + DataType* vecData = vec.data(); + std::memcpy(vecData, _data.ptr<DataType>(0), _data.cols * sizeof(DataType)); + + return vec; + } + else + return {}; +} + +template<typename DataType> +DataType DataBlob::getScalar() const +{ + if (!_data.empty()) + return _data.at<DataType>(0, 0); + else + return DataType{}; +} + +template<typename TargetType> +void DataBlob::convertTo(const DataDescriptor& dataDesc) +{ + return convertTo(dataDesc, true, std::numeric_limits<TargetType>::min(), std::numeric_limits<TargetType>::max()); +} diff --git a/Grinder/engine/data/DataDescriptor.cpp b/Grinder/engine/data/DataDescriptor.cpp new file mode 100644 index 0000000000000000000000000000000000000000..5d5dc93dd1a0cee952609598b50c06804e561b0e --- /dev/null +++ b/Grinder/engine/data/DataDescriptor.cpp @@ -0,0 +1,209 @@ +/****************************************************************************** + * File: DataDescriptor.cpp + * Date: 15.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "DataDescriptor.h" + +#include <opencv2/core.hpp> + +DataDescriptor DataDescriptor::arbitraryDescriptor(QString name) +{ + return DataDescriptor{name, StructureType::Any, FieldType::Any, ValueType::Any}; +} + +DataDescriptor DataDescriptor::imageDescriptor(bool colorImage, ValueType valueType) +{ + return DataDescriptor{colorImage ? "Color image" : "Grayscale image", StructureType::Matrix, colorImage ? FieldType::Color : FieldType::Basic, valueType}; +} + +DataDescriptor DataDescriptor::adaptiveOutputDescriptor(QString name, DataDescriptor::StructureType structType) +{ + return DataDescriptor{name, structType, FieldType::Adaptive, ValueType::Adaptive}; +} + +DataDescriptor::DataDescriptor(QString name, DataDescriptor::StructureType structureType, DataDescriptor::FieldType fieldType, DataDescriptor::ValueType valueType) : + _name{name}, _structureType{structureType}, _fieldType{fieldType}, _valueType{valueType} +{ + +} + +bool DataDescriptor::canConvertTo(const DataDescriptor& dataDesc) const +{ + // All valid structure type conversions + static const std::vector<std::pair<DataDescriptor::StructureType, DataDescriptor::StructureType>> validStructureTypeConversions = { + {DataDescriptor::StructureType::Scalar, DataDescriptor::StructureType::Vector}, + {DataDescriptor::StructureType::Scalar, DataDescriptor::StructureType::Matrix}, + {DataDescriptor::StructureType::Vector, DataDescriptor::StructureType::Matrix}, + }; + + // All valid field type conversions + static const std::vector<std::pair<DataDescriptor::FieldType, DataDescriptor::FieldType>> validFieldTypeConversions = { + {DataDescriptor::FieldType::Basic, DataDescriptor::FieldType::Color}, + {DataDescriptor::FieldType::Basic, DataDescriptor::FieldType::Point3D}, + {DataDescriptor::FieldType::Color, DataDescriptor::FieldType::Basic}, + {DataDescriptor::FieldType::Color, DataDescriptor::FieldType::Point3D}, + {DataDescriptor::FieldType::Point3D, DataDescriptor::FieldType::Basic}, + {DataDescriptor::FieldType::Point3D, DataDescriptor::FieldType::Color}, + }; + + if (match(dataDesc)) // No conversion needed + return true; + + auto structTypeFrom = _structureType; + auto structTypeTo = dataDesc._structureType; + + if (structTypeFrom == DataDescriptor::StructureType::Adaptive || structTypeTo == DataDescriptor::StructureType::Any || canConvertTypes(validStructureTypeConversions, structTypeFrom, structTypeTo)) + { + auto fieldTypeFrom = _fieldType; + auto fieldTypeTo = dataDesc._fieldType; + + if (fieldTypeFrom == DataDescriptor::FieldType::Adaptive || fieldTypeTo == DataDescriptor::FieldType::Any || canConvertTypes(validFieldTypeConversions, fieldTypeFrom, fieldTypeTo)) + { + // Value type: Everything can be converted to everything, so no further checks needed + return true; + } + } + + // Conversion isn't possible + return false; +} + +bool DataDescriptor::canConvertToColor(const DataDescriptor& dataDesc) const +{ + // All grayscale to color conversions + static const std::vector<std::pair<DataDescriptor::FieldType, DataDescriptor::FieldType>> grayscaleToColorConversions = { + {DataDescriptor::FieldType::Basic, DataDescriptor::FieldType::Color}, + {DataDescriptor::FieldType::Basic, DataDescriptor::FieldType::Point3D}, + }; + + return canConvertTypes(grayscaleToColorConversions, _fieldType, dataDesc._fieldType); +} + +bool DataDescriptor::canConvertToGrayscale(const DataDescriptor& dataDesc) const +{ + // All color to grayscale conversions + static const std::vector<std::pair<DataDescriptor::FieldType, DataDescriptor::FieldType>> colorToGrayscaleConversions = { + {DataDescriptor::FieldType::Color, DataDescriptor::FieldType::Basic}, + {DataDescriptor::FieldType::Point3D, DataDescriptor::FieldType::Basic}, + }; + + return canConvertTypes(colorToGrayscaleConversions, _fieldType, dataDesc._fieldType); +} + +bool DataDescriptor::match(const DataDescriptor& dataDesc, DataDescriptor* adjustedDataDesc) const +{ + DataDescriptor adjusted = *this; + + auto checkType = [&dataDesc](auto type, auto typeOther, auto adaptiveVal, auto anyVal, auto adaptiveAssigner) { + if (type == adaptiveVal) // Handle adaptive type + { + if (typeOther != anyVal) + adaptiveAssigner(typeOther); + else + return false; + } + else if (type != typeOther && typeOther != anyVal) // Types do not match + return false; + + return true; + }; + + bool result = true; + + // Structure type + if (!checkType(_structureType, dataDesc._structureType, StructureType::Adaptive, StructureType::Any, [&adjusted](auto newVal) { adjusted._structureType = newVal; })) + result = false; + + // Field type + if (!checkType(_fieldType, dataDesc._fieldType, FieldType::Adaptive, FieldType::Any, [&adjusted](auto newVal) { adjusted._fieldType = newVal; })) + result = false; + + // Value type + if (!checkType(_valueType, dataDesc._valueType, ValueType::Adaptive, ValueType::Any, [&adjusted](auto newVal) { adjusted._valueType = newVal; })) + result = false; + + if (adjustedDataDesc) + *adjustedDataDesc = adjusted; + + return result; +} + +int DataDescriptor::getCVMatrixType(int* chan, int* dep) const +{ + int channels = 0; + int depth = 0; + + switch (_fieldType) + { + case FieldType::Basic: + channels = 1; + break; + + case FieldType::Point2D: + channels = 2; + break; + + case FieldType::Color: + case FieldType::Point3D: + channels = 3; + break; + + default: + break; + } + + if (chan) + *chan = channels; + + switch (_valueType) + { + case ValueType::Int8: + depth = CV_8S; + break; + + case ValueType::UInt8: + depth = CV_8U; + break; + + case ValueType::Int16: + depth = CV_16S; + break; + + case ValueType::UInt16: + depth = CV_16U; + break; + + case ValueType::Int32: + case ValueType::UInt32: + depth = CV_32S; + break; + + case ValueType::Float: + depth = CV_32F; + break; + + case ValueType::Double: + depth = CV_64F; + break; + + default: + break; + } + + if (dep) + *dep = depth; + + return CV_MAKETYPE(depth, channels); +} + +bool DataDescriptor::isArbitrary() const +{ + return _structureType == StructureType::Any || _fieldType == FieldType::Any || _valueType == ValueType::Any; +} + +bool DataDescriptor::isAdaptive() const +{ + return _structureType == StructureType::Adaptive || _fieldType == FieldType::Adaptive || _valueType == ValueType::Adaptive; +} diff --git a/Grinder/engine/data/DataDescriptor.h b/Grinder/engine/data/DataDescriptor.h new file mode 100644 index 0000000000000000000000000000000000000000..583afb3191cd34bcb1889b95721048977528a543 --- /dev/null +++ b/Grinder/engine/data/DataDescriptor.h @@ -0,0 +1,97 @@ +/****************************************************************************** + * File: DataDescriptor.h + * Date: 15.2.2018 + *****************************************************************************/ + +#ifndef DATADESCRIPTOR_H +#define DATADESCRIPTOR_H + +#include <QString> + +namespace grndr +{ + class DataDescriptor + { + public: + enum class StructureType + { + Any, /* Arbitrary type */ + Adaptive, /* Data adapts to its input */ + + Scalar, + Vector, + Matrix, + }; + + enum class FieldType + { + Any, /* Arbitrary type */ + Adaptive, /* Data adapts to its input */ + + Basic, + Color, + Point2D, + Point3D, + }; + + enum class ValueType + { + Any, /* Arbitrary type */ + Adaptive, /* Data adapts to its input */ + + Int8, + UInt8, + Int16, + UInt16, + Int32, + UInt32, + Float, + Double, + }; + + public: + static DataDescriptor arbitraryDescriptor(QString name = "Arbitrary data"); + static DataDescriptor imageDescriptor(bool colorImage = true, ValueType valueType = ValueType::UInt8); + static DataDescriptor adaptiveOutputDescriptor(QString name, StructureType structType = StructureType::Matrix); + + public: + DataDescriptor() { } + DataDescriptor(QString name, StructureType structureType, FieldType fieldType, ValueType valueType); + + public: + bool canConvertTo(const DataDescriptor& dataDesc) const; + bool canConvertToColor(const DataDescriptor& dataDesc) const; + bool canConvertToGrayscale(const DataDescriptor& dataDesc) const; + + bool match(const DataDescriptor& dataDesc, DataDescriptor* adjustedDataDesc = nullptr) const; + + int getCVMatrixType(int* chan = nullptr, int* dep = nullptr) const; + + public: + QString getName() const { return _name; } + + StructureType getStructureType() const { return _structureType; } + FieldType getFieldType() const { return _fieldType; } + ValueType getValueType() const { return _valueType; } + + bool isArbitrary() const; + bool isAdaptive() const; + + private: + template<typename Type> + bool canConvertTypes(const std::vector<std::pair<Type, Type>> validConversions, Type typeFrom, Type typeTo) const; + + private: + QString _name{""}; + + StructureType _structureType{StructureType::Any}; + FieldType _fieldType{FieldType::Any}; + ValueType _valueType{ValueType::Any}; + }; + + using DataDescriptors = std::vector<DataDescriptor>; +} + +#include "DataDescriptor.impl.h" + +#endif diff --git a/Grinder/engine/data/DataDescriptor.impl.h b/Grinder/engine/data/DataDescriptor.impl.h new file mode 100644 index 0000000000000000000000000000000000000000..d65535ca2e55a31671c93d4d8bf7b99f85d8ac47 --- /dev/null +++ b/Grinder/engine/data/DataDescriptor.impl.h @@ -0,0 +1,24 @@ +/****************************************************************************** + * File: DataDescriptor.impl.h + * Date: 19.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "DataDescriptor.h" + +template<typename Type> +bool DataDescriptor::canConvertTypes(const std::vector<std::pair<Type, Type>> validConversions, Type typeFrom, Type typeTo) const +{ + if (typeFrom != typeTo) + { + for (auto conv : validConversions) + { + if (typeFrom == conv.first && typeTo == conv.second) + return true; + } + + return false; + } + else + return true; +} diff --git a/Grinder/engine/data/DataExceptions.cpp b/Grinder/engine/data/DataExceptions.cpp new file mode 100644 index 0000000000000000000000000000000000000000..930e0fcd17e04eac636a9345262f3a21ccdf8a2f --- /dev/null +++ b/Grinder/engine/data/DataExceptions.cpp @@ -0,0 +1,12 @@ +/****************************************************************************** + * File: DataExceptions.cpp + * Date: 19.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "DataExceptions.h" + +DataException::DataException(QString what) : GrinderException(what) +{ + +} diff --git a/Grinder/engine/data/DataExceptions.h b/Grinder/engine/data/DataExceptions.h new file mode 100644 index 0000000000000000000000000000000000000000..9c85b83b9d758985b803dcad5af4c5536d910c1d --- /dev/null +++ b/Grinder/engine/data/DataExceptions.h @@ -0,0 +1,22 @@ +/****************************************************************************** + * File: DataExceptions.h + * Date: 19.2.2018 + *****************************************************************************/ + +#ifndef DATAEXCEPTIONS_H +#define DATAEXCEPTIONS_H + +#include <QString> + +#include "core/GrinderExceptions.h" + +namespace grndr +{ + class DataException : public GrinderException + { + public: + DataException(QString what); + }; +} + +#endif diff --git a/Grinder/engine/processors/BinaryThresholdProcessor.cpp b/Grinder/engine/processors/BinaryThresholdProcessor.cpp new file mode 100644 index 0000000000000000000000000000000000000000..78e0e7d5792f5c21456578a9f74898f966451118 --- /dev/null +++ b/Grinder/engine/processors/BinaryThresholdProcessor.cpp @@ -0,0 +1,27 @@ +/****************************************************************************** + * File: BinaryThresholdProcessor.cpp + * Date: 21.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "BinaryThresholdProcessor.h" + +#include <opencv2/imgproc.hpp> + +BinaryThresholdProcessor::BinaryThresholdProcessor(const Block* block) : Processor(block) +{ + +} + +void BinaryThresholdProcessor::execute(EngineExecutionContext& ctx) +{ + Processor::execute(ctx); + + if (auto dataBlob = portData(ctx, _block->inPort())) + { + cv::Mat processedImage; + cv::threshold(dataBlob->getMatrix(), processedImage, *_block->threshold(), *_block->targetValue(), *_block->invert() ? cv::THRESH_BINARY_INV : cv::THRESH_BINARY); + + ctx.setContextEntry(_block->outPort(), DataBlob{dataBlob->dataDescriptor(), std::move(processedImage)}); + } +} diff --git a/Grinder/engine/processors/BinaryThresholdProcessor.h b/Grinder/engine/processors/BinaryThresholdProcessor.h new file mode 100644 index 0000000000000000000000000000000000000000..3e36f894137360afda4ef273fe5bf0de68271572 --- /dev/null +++ b/Grinder/engine/processors/BinaryThresholdProcessor.h @@ -0,0 +1,24 @@ +/****************************************************************************** + * File: BinaryThresholdProcessor.h + * Date: 21.2.2018 + *****************************************************************************/ + +#ifndef BINARYTHRESHOLDPROCESSOR_H +#define BINARYTHRESHOLDPROCESSOR_H + +#include "engine/Processor.h" +#include "pipeline/blocks/BinaryThresholdBlock.h" + +namespace grndr +{ + class BinaryThresholdProcessor : public Processor<BinaryThresholdBlock> + { + public: + BinaryThresholdProcessor(const Block* block); + + public: + virtual void execute(EngineExecutionContext& ctx) override; + }; +} + +#endif diff --git a/Grinder/engine/processors/ConvertToGrayscaleProcessor.cpp b/Grinder/engine/processors/ConvertToGrayscaleProcessor.cpp new file mode 100644 index 0000000000000000000000000000000000000000..e4f82e670bfdaae6e560e93fe8b9588501c0244a --- /dev/null +++ b/Grinder/engine/processors/ConvertToGrayscaleProcessor.cpp @@ -0,0 +1,27 @@ +/****************************************************************************** + * File: ConvertToGrayscaleProcessor.cpp + * Date: 22.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ConvertToGrayscaleProcessor.h" + +#include <opencv2/imgproc.hpp> + +ConvertToGrayscaleProcessor::ConvertToGrayscaleProcessor(const Block* block) : Processor(block) +{ + +} + +void ConvertToGrayscaleProcessor::execute(EngineExecutionContext& ctx) +{ + Processor::execute(ctx); + + if (auto dataBlob = portData(ctx, _block->inPort())) + { + cv::Mat processedImage; + cv::cvtColor(dataBlob->getMatrix(), processedImage, cv::COLOR_BGR2GRAY); + + ctx.setContextEntry(_block->outPort(), DataBlob{dataBlob->dataDescriptor(), std::move(processedImage)}); + } +} diff --git a/Grinder/engine/processors/ConvertToGrayscaleProcessor.h b/Grinder/engine/processors/ConvertToGrayscaleProcessor.h new file mode 100644 index 0000000000000000000000000000000000000000..77f6a5a62d321d08ceb79546e92eabc2b756bc27 --- /dev/null +++ b/Grinder/engine/processors/ConvertToGrayscaleProcessor.h @@ -0,0 +1,24 @@ +/****************************************************************************** + * File: ConvertToGrayscaleProcessor.h + * Date: 22.2.2018 + *****************************************************************************/ + +#ifndef CONVERTTOGRAYSCALEPROCESSOR_H +#define CONVERTTOGRAYSCALEPROCESSOR_H + +#include "engine/Processor.h" +#include "pipeline/blocks/ConvertToGrayscaleBlock.h" + +namespace grndr +{ + class ConvertToGrayscaleProcessor : public Processor<ConvertToGrayscaleBlock> + { + public: + ConvertToGrayscaleProcessor(const Block* block); + + public: + virtual void execute(EngineExecutionContext& ctx) override; + }; +} + +#endif diff --git a/Grinder/engine/processors/InputProcessor.cpp b/Grinder/engine/processors/InputProcessor.cpp new file mode 100644 index 0000000000000000000000000000000000000000..3a8532264a08cb7a15c95e11b156c36aacbcd61a --- /dev/null +++ b/Grinder/engine/processors/InputProcessor.cpp @@ -0,0 +1,25 @@ +/****************************************************************************** + * File: InputProcessor.cpp + * Date: 21.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "InputProcessor.h" +#include "engine/EngineExceptions.h" +#include "core/GrinderApplication.h" +#include "controller/ProjectController.h" + +InputProcessor::InputProcessor(const Block* block) : Processor(block) +{ + +} + +void InputProcessor::execute(EngineExecutionContext& ctx) +{ + Processor::execute(ctx); + + if (auto activeImage = grinder()->projectController().activeImageReference()) + ctx.setContextEntry(_block->outPort(), DataBlob{getPortDataDescriptor(_block->outPort()), activeImage->loadImage()}); + else + throwProcessorException("There currently is no active image"); +} diff --git a/Grinder/engine/processors/InputProcessor.h b/Grinder/engine/processors/InputProcessor.h new file mode 100644 index 0000000000000000000000000000000000000000..65cbb9d4eacc78c011fce5ba5cfe17a73d0ebcc7 --- /dev/null +++ b/Grinder/engine/processors/InputProcessor.h @@ -0,0 +1,24 @@ +/****************************************************************************** + * File: InputProcessor.h + * Date: 21.2.2018 + *****************************************************************************/ + +#ifndef INTPUTPROCESSOR_H +#define INTPUTPROCESSOR_H + +#include "engine/Processor.h" +#include "pipeline/blocks/InputBlock.h" + +namespace grndr +{ + class InputProcessor : public Processor<InputBlock> + { + public: + InputProcessor(const Block* block); + + public: + virtual void execute(EngineExecutionContext& ctx) override; + }; +} + +#endif diff --git a/Grinder/engine/processors/OutputProcessor.cpp b/Grinder/engine/processors/OutputProcessor.cpp new file mode 100644 index 0000000000000000000000000000000000000000..87fe3e392e940c633630884f4f2205c34618179a --- /dev/null +++ b/Grinder/engine/processors/OutputProcessor.cpp @@ -0,0 +1,30 @@ +/****************************************************************************** + * File: OutputProcessor.cpp + * Date: 21.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "OutputProcessor.h" +#include "project/Label.h" +#include "core/GrinderApplication.h" + +OutputProcessor::OutputProcessor(const Block* block) : Processor(block) +{ + +} + +void OutputProcessor::execute(EngineExecutionContext& ctx) +{ + Processor::execute(ctx); + + if (auto imageBlob = portData(ctx, _block->inPort())) + { + if (auto imageBuild = ctx.label()->imageBuildPool().imageBuild(_block, grinder()->projectController().activeImageReference())) // TODO: ImageRef flexibler + { + imageBuild->setImageData(imageBlob->getMatrix()); + + if (ctx.getExecutionMode() == Engine::ExecutionMode::View) + grinder()->imageEditorManager().showEditor(_block, imageBuild); + } + } +} diff --git a/Grinder/engine/processors/OutputProcessor.h b/Grinder/engine/processors/OutputProcessor.h new file mode 100644 index 0000000000000000000000000000000000000000..0110fec778a93771bdb731534a0741a8faae223f --- /dev/null +++ b/Grinder/engine/processors/OutputProcessor.h @@ -0,0 +1,24 @@ +/****************************************************************************** + * File: OutputProcessor.h + * Date: 21.2.2018 + *****************************************************************************/ + +#ifndef OUTPUTPROCESSOR_H +#define OUTPUTPROCESSOR_H + +#include "engine/Processor.h" +#include "pipeline/blocks/OutputBlock.h" + +namespace grndr +{ + class OutputProcessor : public Processor<OutputBlock> + { + public: + OutputProcessor(const Block* block); + + public: + virtual void execute(EngineExecutionContext& ctx) override; + }; +} + +#endif diff --git a/Grinder/image/DraftItem.cpp b/Grinder/image/DraftItem.cpp new file mode 100644 index 0000000000000000000000000000000000000000..73e8eea6c5f5be7142cca41b9dd9e4dbbd720cd5 --- /dev/null +++ b/Grinder/image/DraftItem.cpp @@ -0,0 +1,63 @@ +/****************************************************************************** + * File: DraftItem.cpp + * Date: 20.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "DraftItem.h" +#include "Layer.h" +#include "common/properties/RangeConstraint.h" + +const char* DraftItem::Serialization_Value_Type = "Type"; + +DraftItem::DraftItem(Layer* layer, DraftItemType type) : + _layer{layer}, _type{type} +{ + if (!layer) + throw std::invalid_argument{_EXCPT("layer may not be null")}; +} + +void DraftItem::initDraftItem() +{ + createProperties(); +} + +int DraftItem::getZOrder() const +{ + return _layer->getZOrder(); +} + +void DraftItem::serialize(SerializationContext& ctx) const +{ + PropertyObject::serialize(ctx); + + // Serialize values + ctx.settings()[Serialization_Value_Type] = _type; +} + +void DraftItem::deserialize(DeserializationContext& ctx) +{ + PropertyObject::deserialize(ctx); + + // Deserialize values + _type = ctx.settings()[Serialization_Value_Type].toString(); +} + +void DraftItem::createProperties() +{ + PropertyObject::createProperties(); + + // Create standard properties + _primaryColor = createProperty<ColorProperty>(PropertyID::PrimaryColor, "Primary color", QColor{255, 255, 255}, PropertyBase::Flag::Hidden); + primaryColor()->setDescription("The primary color to use."); + + _position = createProperty<PointProperty>(PropertyID::Position, "Position", QPoint{0, 0}); + position()->setDescription("The item position."); + + _hasDirection = createProperty<BoolProperty>(PropertyID::HasDirection, "Has direction", false); + hasDirection()->setDescription("Specifies whether the box has a direction/orientation."); + + _direction = createProperty<AngleProperty>(PropertyID::Direction, "Direction", 0.0); + direction()->createConstraint<RangeConstraint>(0, 360); + direction()->setDescription("The direction/orientation of the box contents."); +} diff --git a/Grinder/image/DraftItem.h b/Grinder/image/DraftItem.h new file mode 100644 index 0000000000000000000000000000000000000000..5ac87df4060fe3f3171375602e60f6c588f44dd6 --- /dev/null +++ b/Grinder/image/DraftItem.h @@ -0,0 +1,74 @@ +/****************************************************************************** + * File: DraftItem.h + * Date: 20.3.2018 + *****************************************************************************/ + +#ifndef DRAFTITEM_H +#define DRAFTITEM_H + +#include "common/PropertyObject.h" +#include "DraftItemType.h" +#include "DraftItemRendererBase.h" + +namespace grndr +{ + class Layer; + + class DraftItem : public PropertyObject + { + Q_OBJECT + + public: + static const char* Serialization_Value_Type; + + public: + DraftItem(Layer* layer, DraftItemType type); + + public: + virtual void initDraftItem(); + + public: + virtual std::unique_ptr<DraftItemRendererBase> createRenderer(const DraftItemRendererBase::RendererStyle& rendererStyle) const = 0; + + public: + Layer* layer() { return _layer; } + const Layer* layer() const { return _layer; } + + DraftItemType getType() const { return _type; } + int getZOrder() const; + + auto primaryColor() { return dynamic_cast<ColorProperty*>(_primaryColor.get()); } + auto primaryColor() const { return dynamic_cast<ColorProperty*>(_primaryColor.get()); } + auto position() { return dynamic_cast<PointProperty*>(_position.get()); } + auto position() const { return dynamic_cast<PointProperty*>(_position.get()); } + auto hasDirection() { return dynamic_cast<BoolProperty*>(_hasDirection.get()); } + auto hasDirection() const { return dynamic_cast<BoolProperty*>(_hasDirection.get()); } + auto direction() { return dynamic_cast<AngleProperty*>(_direction.get()); } + auto direction() const { return dynamic_cast<AngleProperty*>(_direction.get()); } + + public: + virtual void setDefaultPropertyValues() { } + virtual void setDragPropertyValues(QPoint initialPos, QPoint currentPos) { Q_UNUSED(initialPos); Q_UNUSED(currentPos); } + + virtual void normalizePropertyValues() { } + + public: + virtual void serialize(SerializationContext& ctx) const override; + virtual void deserialize(DeserializationContext& ctx) override; + + protected: + virtual void createProperties() override; + + protected: + Layer* _layer{nullptr}; + + DraftItemType _type{DraftItemType::Undefined}; + + std::shared_ptr<PropertyBase> _primaryColor; + std::shared_ptr<PropertyBase> _position; + std::shared_ptr<PropertyBase> _hasDirection; + std::shared_ptr<PropertyBase> _direction; + }; +} + +#endif diff --git a/Grinder/image/DraftItemCatalog.cpp b/Grinder/image/DraftItemCatalog.cpp new file mode 100644 index 0000000000000000000000000000000000000000..65d2ca4a33f2818e093ace09635a5d434a3ee7b7 --- /dev/null +++ b/Grinder/image/DraftItemCatalog.cpp @@ -0,0 +1,61 @@ +/****************************************************************************** + * File: DraftItemCatalog.cpp + * Date: 20.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "DraftItemCatalog.h" +#include "DraftItem.h" + +#include "draftitems/LineDraftItem.h" +#include "draftitems/BoxDraftItem.h" + +#define REGISTER_ITEM_TYPE(cls) registerDraftItemType(cls::type_value, [](Layer* layer) { return std::make_unique<cls>(layer); }) + +std::map<DraftItemType, DraftItemCatalog::item_creator_type> DraftItemCatalog::s_creators; + +std::set<DraftItemType> DraftItemCatalog::getTypes() +{ + std::set<DraftItemType> types; + + for (const auto& creator : s_creators) + types.insert(creator.first); + + return types; +} + +void DraftItemCatalog::registerDraftItemType(DraftItemType type, item_creator_type creator) +{ + if (type == DraftItemType::Undefined) + throw std::invalid_argument{_EXCPT("type may not be DraftItemType::Undefined")}; + + if (!creator) + throw std::invalid_argument{_EXCPT("creator may not be null")}; + + s_creators[type] = creator; +} + +std::unique_ptr<DraftItem> DraftItemCatalog::createDraftItem(Layer* layer, DraftItemType type) +{ + if (type == DraftItemType::Undefined) + throw std::invalid_argument{_EXCPT("type may not be DraftItemType::Undefined")}; + + if (s_creators.find(type) != s_creators.end()) + { + auto creator = s_creators.at(type); + auto item = creator(layer); + + if (!item) + throw std::runtime_error{_EXCPT(QString{"Failed to create a draft item of type %1"}.arg(type))}; + + return item; + } + else + throw std::invalid_argument{_EXCPT("The given type has not been registered")}; +} + +void DraftItemCatalog::registerStandardDraftItems() +{ + REGISTER_ITEM_TYPE(LineDraftItem); + REGISTER_ITEM_TYPE(BoxDraftItem); +} diff --git a/Grinder/image/DraftItemCatalog.h b/Grinder/image/DraftItemCatalog.h new file mode 100644 index 0000000000000000000000000000000000000000..ff5f58f3ecd5cc29d63813b716be64493cd4faf0 --- /dev/null +++ b/Grinder/image/DraftItemCatalog.h @@ -0,0 +1,45 @@ +/****************************************************************************** + * File: DraftItemCatalog.h + * Date: 20.3.2018 + *****************************************************************************/ + +#ifndef DRAFTITEMCATALOG_H +#define DRAFTITEMCATALOG_H + +#include <map> +#include <set> +#include <functional> +#include <memory> + +#include "DraftItemType.h" + +namespace grndr +{ + class DraftItem; + class Layer; + + class DraftItemCatalog + { + private: + using item_creator_type = std::function<std::unique_ptr<DraftItem>(Layer*)>; + + private: + DraftItemCatalog() { } + + public: + static std::unique_ptr<DraftItem> createDraftItem(Layer* layer, DraftItemType type); + + static void registerStandardDraftItems(); + + public: + static std::set<DraftItemType> getTypes(); + + private: + static void registerDraftItemType(DraftItemType type, item_creator_type creator); + + private: + static std::map<DraftItemType, item_creator_type> s_creators; + }; +} + +#endif diff --git a/Grinder/image/DraftItemRenderer.h b/Grinder/image/DraftItemRenderer.h new file mode 100644 index 0000000000000000000000000000000000000000..4566cfe788697702ef17a692e230b23eec27346c --- /dev/null +++ b/Grinder/image/DraftItemRenderer.h @@ -0,0 +1,42 @@ +/****************************************************************************** + * File: DraftItemRenderer.h + * Date: 21.3.2018 + *****************************************************************************/ + +#ifndef DRAFTITEMRENDERER_H +#define DRAFTITEMRENDERER_H + +#include "DraftItemRendererBase.h" + +namespace grndr +{ + class DraftItem; + + template<typename ItemType> + class DraftItemRenderer : public DraftItemRendererBase + { + static_assert(std::is_base_of<DraftItem, ItemType>::value, "ItemType must be derived from DraftItem"); + + public: + using item_type = ItemType; + + public: + DraftItemRenderer(const item_type* item, const RendererStyle& rendererStyle); + + protected: + QPoint getItemPosition(RenderMode mode) const; + + QPen createStandardPen(int lineWidth) const; + QPen createSelectionPen(int lineWidth, bool inactive = false) const; + + protected: + void renderDirectionArrow(QPainter* painter, QPoint centerPos); + + protected: + const item_type* _draftItem{nullptr}; + }; +} + +#include "DraftItemRenderer.impl.h" + +#endif diff --git a/Grinder/image/DraftItemRenderer.impl.h b/Grinder/image/DraftItemRenderer.impl.h new file mode 100644 index 0000000000000000000000000000000000000000..83553ed2b6f116e5b9ffeceaf2c3d2abf6179fd2 --- /dev/null +++ b/Grinder/image/DraftItemRenderer.impl.h @@ -0,0 +1,104 @@ +/****************************************************************************** + * File: DraftItemRenderer.impl.h + * Date: 21.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "DraftItemRenderer.h" +#include "util/MathUtils.h" + +template<typename ItemType> +DraftItemRenderer<ItemType>::DraftItemRenderer(const item_type* item, const RendererStyle& rendererStyle) : DraftItemRendererBase(rendererStyle), + _draftItem{item} +{ + if (!item) + throw std::invalid_argument{_EXCPT("item may not be null")}; +} + +template<typename ItemType> +QPoint DraftItemRenderer<ItemType>::getItemPosition(RenderMode mode) const +{ + QPoint pos{0, 0}; + + if (mode.testFlag(RenderModeFlag::AbsolutePositions)) + pos = *_draftItem->position(); + + return pos; +} + +template<typename ItemType> +QPen DraftItemRenderer<ItemType>::createStandardPen(int lineWidth) const +{ + QPen pen{*_draftItem->primaryColor()}; + pen.setWidth(lineWidth); + pen.setCapStyle(Qt::FlatCap); + pen.setJoinStyle(Qt::MiterJoin); + + return pen; +} + +template<typename ItemType> +QPen DraftItemRenderer<ItemType>::createSelectionPen(int lineWidth, bool inactive) const +{ + auto selectionPen = createStandardPen(lineWidth + _rendererStyle.selectionMargin); + selectionPen.setColor(inactive ? _rendererStyle.selectionColorInactive : _rendererStyle.selectionColor); + selectionPen.setCapStyle(Qt::SquareCap); + + return selectionPen; +} + +template<typename ItemType> +void DraftItemRenderer<ItemType>::renderDirectionArrow(QPainter* painter, QPoint centerPos) +{ + painter->save(); + + QPainterPath path(centerPos); + + // Calculate the arrow direction vector + auto angle = ((360.0 - *_draftItem->direction()) / 180.0) * M_PI; + QPointF arrowDir{std::cos(angle) * _rendererStyle.directionArrowLength, std::sin(angle) * _rendererStyle.directionArrowLength}; + + // Calculate the arrow start and end positions + QPoint startPos = centerPos; + QPoint endPos = centerPos + MathUtils::round(arrowDir); + + // Add the arrow base to the painter path + path.lineTo(endPos); + + // Add the arrow head to the painter path + QPointF arrowPoly[3]; + qreal vecLine[2]; + qreal vecLeft[2]; + + arrowPoly[0] = endPos; + + vecLine[0] = arrowPoly[0].x() - startPos.x(); + vecLine[1] = arrowPoly[0].y() - startPos.y(); + + vecLeft[0] = -vecLine[1]; + vecLeft[1] = vecLine[0]; + + qreal length = std::sqrt(vecLine[0] * vecLine[0] + vecLine[1] * vecLine[1]); + qreal th = _rendererStyle.directionArrowWidth / length; + qreal ta = _rendererStyle.directionArrowWidth / ((std::tan(M_PI / 3.0) / 2.0) * length); + auto base = QPointF{arrowPoly[0].x() + -ta * vecLine[0], arrowPoly[0].y() + -ta * vecLine[1]}; + + arrowPoly[1].setX(base.x() + th * vecLeft[0]); + arrowPoly[1].setY(base.y() + th * vecLeft[1]); + arrowPoly[2].setX(base.x() - th * vecLeft[0]); + arrowPoly[2].setY(base.y() - th * vecLeft[1]); + + path.addPolygon(QPolygonF{} << arrowPoly[0] << arrowPoly[1] << arrowPoly[2]); + path.closeSubpath(); + + // Draw the arrow + QPen pen = createStandardPen(_rendererStyle.directionArrowWidth); + pen.setColor(_rendererStyle.directionArrowColor); + + painter->setRenderHints(QPainter::Antialiasing|QPainter::TextAntialiasing|QPainter::HighQualityAntialiasing|QPainter::SmoothPixmapTransform); + painter->setPen(pen); + painter->setOpacity(_rendererStyle.directionArrowOpacity); + painter->drawPath(path); + + painter->restore(); +} diff --git a/Grinder/image/DraftItemRendererBase.cpp b/Grinder/image/DraftItemRendererBase.cpp new file mode 100644 index 0000000000000000000000000000000000000000..d40c4bab50c9114ed52e48c0d6d9a41be1f6a581 --- /dev/null +++ b/Grinder/image/DraftItemRendererBase.cpp @@ -0,0 +1,13 @@ +/****************************************************************************** + * File: DraftItemRendererBase.cpp + * Date: 21.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "DraftItemRendererBase.h" + +DraftItemRendererBase::DraftItemRendererBase(const RendererStyle& rendererStyle) : + _rendererStyle{rendererStyle} +{ + +} diff --git a/Grinder/image/DraftItemRendererBase.h b/Grinder/image/DraftItemRendererBase.h new file mode 100644 index 0000000000000000000000000000000000000000..1ea587530b50a9f737201aa5e83ea4c8cb67dd96 --- /dev/null +++ b/Grinder/image/DraftItemRendererBase.h @@ -0,0 +1,70 @@ +/****************************************************************************** + * File: DraftItemRendererBase.h + * Date: 21.3.2018 + *****************************************************************************/ + +#ifndef DRAFTITEMRENDERERBASE_H +#define DRAFTITEMRENDERERBASE_H + +#include <QPainter> +#include <QPalette> +#include <memory> + +namespace grndr +{ + class ImageEditorStyle; + + class DraftItemRendererBase + { + public: + enum class RenderModeFlag : unsigned int + { + RelativePositions = 0x0000, + AbsolutePositions = 0x0001, + + RenderToImage = AbsolutePositions, + RenderToScene = RelativePositions, + }; + + Q_DECLARE_FLAGS(RenderMode, RenderModeFlag) + + enum class RenderFlag : unsigned int + { + NoFlag = 0x0000, + Selected = 0x0001, + Inactive = 0x0002, + ShowDirections = 0x0004, + ShowTags = 0x0008, + }; + + Q_DECLARE_FLAGS(RenderFlags, RenderFlag) + + struct RendererStyle + { + QColor selectionColor{QPalette{}.highlight().color().lighter()}; + QColor selectionColorInactive{QPalette{}.color(QPalette::Inactive, QPalette::Highlight).darker()}; + float selectionMargin{6.0f}; + float selectionOpacity{0.6f}; + + float directionArrowLength{40.0}; + QColor directionArrowColor{0, 255, 128}; + float directionArrowWidth{3.5f}; + float directionArrowOpacity{0.8f}; + }; + + public: + DraftItemRendererBase(const RendererStyle& rendererStyle); + + public: + virtual void render(QPainter* painter, RenderMode mode, RenderFlags flags) = 0; + virtual QPainterPath shape() const { return QPainterPath{}; } + + protected: + RendererStyle _rendererStyle; + }; +} + +Q_DECLARE_OPERATORS_FOR_FLAGS(grndr::DraftItemRendererBase::RenderMode) +Q_DECLARE_OPERATORS_FOR_FLAGS(grndr::DraftItemRendererBase::RenderFlags) + +#endif diff --git a/Grinder/image/DraftItemType.cpp b/Grinder/image/DraftItemType.cpp new file mode 100644 index 0000000000000000000000000000000000000000..d861564dc7f02188190894fdefe47320c6d5aad7 --- /dev/null +++ b/Grinder/image/DraftItemType.cpp @@ -0,0 +1,12 @@ +/****************************************************************************** + * File: DraftItemType.cpp + * Date: 20.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "DraftItemType.h" + +const char* DraftItemType::Undefined = ""; + +const char* DraftItemType::Line = "Line"; +const char* DraftItemType::Box = "Box"; diff --git a/Grinder/image/DraftItemType.h b/Grinder/image/DraftItemType.h new file mode 100644 index 0000000000000000000000000000000000000000..b04e6ac2f91b3dff68fbd52d446b80dd9a7c8dee --- /dev/null +++ b/Grinder/image/DraftItemType.h @@ -0,0 +1,35 @@ +/****************************************************************************** + * File: DraftItemType.h + * Date: 20.3.2018 + *****************************************************************************/ + +#ifndef DRAFTITEMTYPE_H +#define DRAFTITEMTYPE_H + +#include <QString> + +namespace grndr +{ + class DraftItemType final : public QString + { + public: + static const char* Undefined; /* Invalid draft item */ + + static const char* Line; + static const char* Box; + + public: + using QString::QString; + + DraftItemType() = default; + DraftItemType(const DraftItemType& other) = default; + DraftItemType(DraftItemType&& other) = default; + DraftItemType(const QString& str) { *static_cast<QString*>(this) = str; } + + DraftItemType& operator =(const DraftItemType& other) = default; + DraftItemType& operator =(DraftItemType&& other) = default; + DraftItemType& operator =(const QString& str) { *static_cast<QString*>(this) = str; return *this; } + }; +} + +#endif diff --git a/Grinder/image/DraftItemVector.cpp b/Grinder/image/DraftItemVector.cpp new file mode 100644 index 0000000000000000000000000000000000000000..c9eecee6af2a2690b3febd5354d59b00678da71a --- /dev/null +++ b/Grinder/image/DraftItemVector.cpp @@ -0,0 +1,22 @@ +/****************************************************************************** + * File: DraftItemVector.cpp + * Date: 20.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "DraftItemVector.h" + +const char* DraftItemVector::Serialization_Group = "DraftItems"; +const char* DraftItemVector::Serialization_Element = "DraftItem"; + +DraftItemVector::DraftItemVector(const Layer* layer) : + _layer{layer} +{ + if (!layer) + throw std::invalid_argument{_EXCPT("layer may not be null")}; +} + +DraftItemVector::pointer_vec_type DraftItemVector::selectByType(DraftItemType type) const +{ + return select([type](auto item) { return item->getType() == type; }); +} diff --git a/Grinder/image/DraftItemVector.h b/Grinder/image/DraftItemVector.h new file mode 100644 index 0000000000000000000000000000000000000000..f409493613462ee399cb8b6ba14b318e6acd80f2 --- /dev/null +++ b/Grinder/image/DraftItemVector.h @@ -0,0 +1,31 @@ +/****************************************************************************** + * File: DraftItemVector.h + * Date: 20.3.2018 + *****************************************************************************/ + +#ifndef DRAFTITEMVECTOR_H +#define DRAFTITEMVECTOR_H + +#include "common/ObjectVector.h" +#include "DraftItem.h" + +namespace grndr +{ + class DraftItemVector : public ObjectVector<DraftItem> + { + public: + static const char* Serialization_Group; + static const char* Serialization_Element; + + public: + DraftItemVector(const Layer* layer); + + public: + pointer_vec_type selectByType(DraftItemType type) const; + + private: + const Layer* _layer{nullptr}; + }; +} + +#endif diff --git a/Grinder/image/ImageBuild.cpp b/Grinder/image/ImageBuild.cpp new file mode 100644 index 0000000000000000000000000000000000000000..a035a70c6fcac9a77c69f01f83eb930af6f1a0c4 --- /dev/null +++ b/Grinder/image/ImageBuild.cpp @@ -0,0 +1,112 @@ +/****************************************************************************** + * File: ImageBuild.cpp + * Date: 12.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ImageBuild.h" +#include "ImageExceptions.h" + +const char* ImageBuild::Serialization_Value_ImageReference = "ImageReference"; + +ImageBuild::ImageBuild(const ImageReference* imageReference) : + _imageReference{imageReference}, _layers{this} +{ + if (!imageReference) + throw std::invalid_argument{_EXCPT("imageReference may not be null")}; +} + +std::shared_ptr<Layer> ImageBuild::createLayer(QString name) +{ + // Create new block using the block factory; cast the unique ptr to a shared one as well + auto layer = std::make_shared<Layer>(this, name); + + try { // Propagate initialization errors to the caller + layer->initLayer(); + } + catch (...) { + throw; + } + + _layers.push_back(layer); + emit layerCreated(layer); + + return layer; +} + +void ImageBuild::removeLayer(const Layer* layer) +{ + if (layer) + { + auto it = _layers.find(layer); + + if (it != _layers.cend()) + { + // Keep a copy of the shared_ptr holding the layer to increase its use count; + // otherwise, the layer will be deleted before it has been removed from the vector, potentially causing a crash + auto layer = *it; + + emit layerRemoved(*it); + _layers.erase(it); + } + else + throw ImageBuildException{this, _EXCPT("Tried to remove a layer not belonging to this image build")}; + } +} + +void ImageBuild::moveLayer(const Layer* layer, bool up) +{ + int index = _layers.indexOf(layer); + + if (index != -1) + { + int indexNew = index + (up ? 1 : -1); // Moving up means moving the layer to a higher index + + if (indexNew < 0) + indexNew = 0; + else if (indexNew >= static_cast<int>(_layers.size())) + indexNew = _layers.size() - 1; + + if (indexNew != index) + { + std::swap(_layers[index], _layers[indexNew]); + emit layerMoved(*_layers.find(layer), index, indexNew); + } + } + else + throw ImageBuildException{this, _EXCPT("Tried to move a layer not belonging to this image build")}; +} + +void ImageBuild::serialize(SerializationContext& ctx) const +{ + // Serialize values + ctx.settings()[Serialization_Value_ImageReference] = ctx.getImageReferenceIndex(_imageReference); + + // Serialize all layers + ctx.beginGroup(LayerVector::Serialization_Group, true); + _layers.serialize(LayerVector::Serialization_Element, ctx); + ctx.endGroup(); +} + +void ImageBuild::deserialize(DeserializationContext& ctx) +{ + // Deserialize values + int imageRefIndex = ctx.settings()[Serialization_Value_ImageReference].toInt(); + + if (imageRefIndex != -1) + { + if (auto imageRef = ctx.getImageReference(imageRefIndex)) + _imageReference = imageRef; + } + + // Deserialize all layers + if (ctx.beginGroup(LayerVector::Serialization_Group)) + { + _layers.deserialize(LayerVector::Serialization_Element, ctx, [this](const SettingsContainer& settings) { + QString name = settings[Layer::Serialization_Value_Name].toString(); + return createLayer(name); + }); + + ctx.endGroup(); + } +} diff --git a/Grinder/image/ImageBuild.h b/Grinder/image/ImageBuild.h new file mode 100644 index 0000000000000000000000000000000000000000..4d6ac052255a850cadd6f51524c4eaf082738a58 --- /dev/null +++ b/Grinder/image/ImageBuild.h @@ -0,0 +1,61 @@ +/****************************************************************************** + * File: ImageBuild.h + * Date: 12.3.2018 + *****************************************************************************/ + +#ifndef IMAGEBUILD_H +#define IMAGEBUILD_H + +#include <opencv2/core.hpp> + +#include "LayerVector.h" + +namespace grndr +{ + class ImageReference; + + class ImageBuild : public QObject + { + Q_OBJECT + + public: + static const char* Serialization_Value_ImageReference; + + public: + ImageBuild(const ImageReference* imageReference); + + public: + std::shared_ptr<Layer> createLayer(QString name = ""); + void removeLayer(const Layer* layer); + + void moveLayer(const Layer* layer, bool up); + + public: + const ImageReference* imageReference() const { return _imageReference; } + + const cv::Mat& imageData() const { return _imageData; } + void setImageData(const cv::Mat& data) { _imageData = data; emit imageDataChanged(); } + void clearImageData() { _imageData.release(); emit imageDataChanged(); } + + const LayerVector& layers() const { return _layers; } + + public: + void serialize(SerializationContext& ctx) const; + void deserialize(DeserializationContext& ctx); + + signals: + void layerCreated(const std::shared_ptr<Layer>&); + void layerRemoved(const std::shared_ptr<Layer>&); + void layerMoved(const std::shared_ptr<Layer>&, int, int); + + void imageDataChanged(); + + private: + const ImageReference* _imageReference{nullptr}; + cv::Mat _imageData; + + LayerVector _layers; + }; +} + +#endif diff --git a/Grinder/image/ImageBuildItem.cpp b/Grinder/image/ImageBuildItem.cpp new file mode 100644 index 0000000000000000000000000000000000000000..fe785e444f7498816172b3c61b6cd5d2426534d3 --- /dev/null +++ b/Grinder/image/ImageBuildItem.cpp @@ -0,0 +1,32 @@ +/****************************************************************************** + * File: ImageBuildItem.cpp + * Date: 17.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ImageBuildItem.h" + +const char* ImageBuildItem::Serialization_Value_Name = "Name"; + +ImageBuildItem::ImageBuildItem(ImageBuild* imageBuild, QString name) : + _imageBuild{imageBuild}, _name{name} +{ + +} + +void ImageBuildItem::initImageBuildItem() +{ + +} + +void ImageBuildItem::serialize(SerializationContext& ctx) const +{ + // Serialize values + ctx.settings()[Serialization_Value_Name] = _name; +} + +void ImageBuildItem::deserialize(DeserializationContext& ctx) +{ + // Deserialize values + _name = ctx.settings()[Serialization_Value_Name].toString(); +} diff --git a/Grinder/image/ImageBuildItem.h b/Grinder/image/ImageBuildItem.h new file mode 100644 index 0000000000000000000000000000000000000000..91b224b608e485ead3b66f8b376136d1773d1d35 --- /dev/null +++ b/Grinder/image/ImageBuildItem.h @@ -0,0 +1,52 @@ +/****************************************************************************** + * File: ImageBuildItem.h + * Date: 17.3.2018 + *****************************************************************************/ + +#ifndef IMAGEBUILDITEM_H +#define IMAGEBUILDITEM_H + +#include <QObject> + +#include "project/serialization/SerializationContext.h" +#include "project/serialization/DeserializationContext.h" + +namespace grndr +{ + class ImageBuild; + + class ImageBuildItem : public QObject + { + Q_OBJECT + + public: + static const char* Serialization_Value_Name; + + public: + ImageBuildItem(ImageBuild* imageBuild, QString name = ""); + + public: + void initImageBuildItem(); + + public: + const ImageBuild* imageBuild() const { return _imageBuild; } + ImageBuild* imageBuild() { return _imageBuild; } + + QString getName() const { return _name; } + void setName(QString name) { if (_name != name) { _name = name; emit itemRenamed(); } } + + public: + virtual void serialize(SerializationContext& ctx) const; + virtual void deserialize(DeserializationContext& ctx); + + signals: + void itemRenamed(); + + protected: + ImageBuild* _imageBuild{nullptr}; + + QString _name{""}; + }; +} + +#endif diff --git a/Grinder/image/ImageBuildPool.cpp b/Grinder/image/ImageBuildPool.cpp new file mode 100644 index 0000000000000000000000000000000000000000..92c8c38715bd7388490eca8a460bd7b8a899f8da --- /dev/null +++ b/Grinder/image/ImageBuildPool.cpp @@ -0,0 +1,183 @@ +/****************************************************************************** + * File: ImageBuildPool.cpp + * Date: 12.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ImageBuildPool.h" +#include "ImageExceptions.h" +#include "project/Project.h" + +const char* ImageBuildPool::Serialization_Group_Item = "Item"; + +const char* ImageBuildPool::Serialization_Value_Block = "Block"; + +ImageBuildPool::ImageBuildPool(Label* label) +{ + if (!label) + throw std::invalid_argument{_EXCPT("label may not be null")}; + + // Whenever a block or image reference is removed, remove the associated image builds + connect(label->pipeline(), &Pipeline::blockRemoved, this, &ImageBuildPool::blockRemoved); + connect(label->project(), &Project::imageReferenceRemoved, this, &ImageBuildPool::imageReferenceRemoved); +} + +const ImageBuildVector* ImageBuildPool::imageBuilds(const Block* block) const +{ + if (_imageBuilds.find(block) != _imageBuilds.cend()) + return _imageBuilds.at(block).get(); + else + return nullptr; +} + +void ImageBuildPool::removeImageBuilds(const Block* block) +{ + if (block) + { + auto it = _imageBuilds.find(block); + + if (it != _imageBuilds.end()) + { + // Send a removal notification for each image build in the build vector + for (auto& build : *it->second) + emit imageBuildRemoved(build); + + _imageBuilds.erase(it); + } + else + throw ImageBuildException{nullptr, _EXCPT("Tried to remove the image builds of a block not belonging to this pool")}; + } +} + +void ImageBuildPool::removeImageBuilds(const ImageReference* imageRef) +{ + for (auto& entry : _imageBuilds) + { + if (entry.second->selectByImageReference(imageRef)) // Only remove existing image builds + removeImageBuild(entry.first, imageRef); + } +} + +std::shared_ptr<ImageBuild> ImageBuildPool::imageBuild(const Block* block, const ImageReference* imageRef) +{ + if (auto buildVector = imageBuildVector(block)) + return imageBuild(buildVector, imageRef); + else + throw ImageBuildException{nullptr, _EXCPT("Unable to create an image build")}; +} + +void ImageBuildPool::removeImageBuild(const Block* block, const ImageReference* imageRef) +{ + if (block && imageRef) + { + auto blockIt = _imageBuilds.find(block); + + if (blockIt != _imageBuilds.end()) + { + if (auto build = blockIt->second->selectByImageReference(imageRef)) + { + auto it = blockIt->second->find(build); // Should always be valid + + emit imageBuildRemoved(*it); + blockIt->second->erase(it); + } + else + throw ImageBuildException{nullptr, _EXCPT("Tried to remove an image build not belonging to this pool")}; + } + else + throw ImageBuildException{nullptr, _EXCPT("Tried to remove an image build from a block not belonging to this pool")}; + } +} + +void ImageBuildPool::serialize(SerializationContext& ctx) const +{ + // Serialize all image builds + for (const auto& build : _imageBuilds) + { + ctx.beginGroup(Serialization_Group_Item); + ctx.settings()[Serialization_Value_Block] = ctx.getBlockIndex(build.first); + + ctx.beginGroup(ImageBuildVector::Serialization_Group, true); + build.second->serialize(ImageBuildVector::Serialization_Element, ctx); + ctx.endGroup(); + + ctx.endGroup(); + } +} + +void ImageBuildPool::deserialize(DeserializationContext& ctx) +{ + // Deserialize all image builds + for (const auto elemSettings : ctx.settings().children(Serialization_Group_Item)) + { + ctx.beginGroup(elemSettings); + int blockIndex = ctx.settings()[Serialization_Value_Block].toInt(); + + if (auto block = ctx.getBlock(blockIndex)) + { + auto buildVector = imageBuildVector(block); + + if (ctx.beginGroup(ImageBuildVector::Serialization_Group)) + { + buildVector->deserialize(ImageBuildVector::Serialization_Element, ctx, [&ctx, &buildVector, this](const SettingsContainer& settings) -> std::shared_ptr<ImageBuild> { + int imageIndex = settings[ImageBuild::Serialization_Value_ImageReference].toInt(); + + if (auto imageReference = ctx.getImageReference(imageIndex)) + return imageBuild(buildVector, imageReference); + else + return nullptr; + }); + + ctx.endGroup(); + } + } + + ctx.endGroup(); + } +} + +std::shared_ptr<ImageBuildVector> ImageBuildPool::imageBuildVector(const Block* block) +{ + // Create a new build vector if necessary + if (_imageBuilds.find(block) == _imageBuilds.cend()) + { + auto buildVector = std::make_shared<ImageBuildVector>(); + _imageBuilds.emplace(block, buildVector); + + return buildVector; + } + else + return _imageBuilds.at(block); +} + +std::shared_ptr<ImageBuild> ImageBuildPool::imageBuild(std::shared_ptr<ImageBuildVector>& builds, const ImageReference* imageRef) +{ + // Create a new build if necessary + if (!builds->selectByImageReference(imageRef)) + { + auto build = std::make_shared<ImageBuild>(imageRef); + + builds->push_back(build); + emit imageBuildCreated(build); + + return build; + } + else + { + auto build = builds->selectByImageReference(imageRef); + return *builds->find(build); + } +} + +void ImageBuildPool::blockRemoved(const std::shared_ptr<Block>& block) +{ + // When a block is deleted, remove all associated image builds + if (_imageBuilds.find(block.get()) != _imageBuilds.end()) // Only remove image builds of blocks currently in the pool + removeImageBuilds(block.get()); +} + +void ImageBuildPool::imageReferenceRemoved(const std::shared_ptr<ImageReference>& imageRef) +{ + // When an image reference is deleted, remove all associated image builds + removeImageBuilds(imageRef.get()); +} diff --git a/Grinder/image/ImageBuildPool.h b/Grinder/image/ImageBuildPool.h new file mode 100644 index 0000000000000000000000000000000000000000..ff3741863facd95efdaa5d22b444c8c269899cfd --- /dev/null +++ b/Grinder/image/ImageBuildPool.h @@ -0,0 +1,57 @@ +/****************************************************************************** + * File: ImageBuildPool.h + * Date: 12.3.2018 + *****************************************************************************/ + +#ifndef IMAGEBUILDPOOL_H +#define IMAGEBUILDPOOL_H + +#include "ImageBuildVector.h" + +namespace grndr +{ + class Label; + class Block; + + class ImageBuildPool : public QObject + { + Q_OBJECT + + public: + static const char* Serialization_Group_Item; + + static const char* Serialization_Value_Block; + + public: + ImageBuildPool(Label* label); + + public: + const ImageBuildVector* imageBuilds(const Block* block) const; + void removeImageBuilds(const Block* block); + void removeImageBuilds(const ImageReference* imageRef); + + std::shared_ptr<ImageBuild> imageBuild(const Block* block, const ImageReference* imageRef); + void removeImageBuild(const Block* block, const ImageReference* imageRef); + + public: + void serialize(SerializationContext& ctx) const; + void deserialize(DeserializationContext& ctx); + + signals: + void imageBuildCreated(const std::shared_ptr<ImageBuild>&); + void imageBuildRemoved(const std::shared_ptr<ImageBuild>&); + + private: + std::shared_ptr<ImageBuildVector> imageBuildVector(const Block* block); + std::shared_ptr<ImageBuild> imageBuild(std::shared_ptr<ImageBuildVector>& builds, const ImageReference* imageRef); + + private slots: + void blockRemoved(const std::shared_ptr<Block>& block); + void imageReferenceRemoved(const std::shared_ptr<ImageReference>& imageRef); + + private: + std::map<const Block*, std::shared_ptr<ImageBuildVector>> _imageBuilds; + }; +} + +#endif diff --git a/Grinder/image/ImageBuildVector.cpp b/Grinder/image/ImageBuildVector.cpp new file mode 100644 index 0000000000000000000000000000000000000000..d59e10003ed8214047f532724450b1df51a3031d --- /dev/null +++ b/Grinder/image/ImageBuildVector.cpp @@ -0,0 +1,15 @@ +/****************************************************************************** + * File: ImageBuildVector.cpp + * Date: 12.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ImageBuildVector.h" + +const char* ImageBuildVector::Serialization_Group = "ImageBuilds"; +const char* ImageBuildVector::Serialization_Element = "ImageBuild"; + +ImageBuildVector::pointer_type ImageBuildVector::selectByImageReference(const ImageReference* imageRef) const +{ + return selectFirst([imageRef](auto build) { return build->imageReference() == imageRef; }); +} diff --git a/Grinder/image/ImageBuildVector.h b/Grinder/image/ImageBuildVector.h new file mode 100644 index 0000000000000000000000000000000000000000..00479511a623db7541315bd0eb86766b2ae0a6fa --- /dev/null +++ b/Grinder/image/ImageBuildVector.h @@ -0,0 +1,25 @@ +/****************************************************************************** + * File: ImageBuildVector.h + * Date: 12.3.2018 + *****************************************************************************/ + +#ifndef IMAGEBUILDVECTOR_H +#define IMAGEBUILDVECTOR_H + +#include "common/ObjectVector.h" +#include "ImageBuild.h" + +namespace grndr +{ + class ImageBuildVector : public ObjectVector<ImageBuild> + { + public: + static const char* Serialization_Group; + static const char* Serialization_Element; + + public: + pointer_type selectByImageReference(const ImageReference* imageRef) const; + }; +} + +#endif diff --git a/Grinder/image/ImageExceptions.cpp b/Grinder/image/ImageExceptions.cpp new file mode 100644 index 0000000000000000000000000000000000000000..80553638260645367498005032fbf47ae42c00a6 --- /dev/null +++ b/Grinder/image/ImageExceptions.cpp @@ -0,0 +1,31 @@ +/****************************************************************************** + * File: ImageExceptions.cpp + * Date: 12.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ImageExceptions.h" +#include "Layer.h" + +ImageException::ImageException(QString what) : GrinderException(what) +{ + +} + +ImageBuildException::ImageBuildException(const ImageBuild* imageBuild, QString what) : ImageException(what), + _imageBuild{imageBuild} +{ + +} + +ImageEditorException::ImageEditorException(const ImageEditor* imageEditor, QString what) : ImageException(what), + _imageEditor{imageEditor} +{ + +} + +LayerException::LayerException(const Layer* layer, QString what) : ImageBuildException(layer ? layer->imageBuild() : nullptr, what), + _layer{layer} +{ + +} diff --git a/Grinder/image/ImageExceptions.h b/Grinder/image/ImageExceptions.h new file mode 100644 index 0000000000000000000000000000000000000000..1079c39d68dee738bc6cae388fc0be148a4d7177 --- /dev/null +++ b/Grinder/image/ImageExceptions.h @@ -0,0 +1,60 @@ +/****************************************************************************** + * File: ImageExceptions.h + * Date: 12.3.2018 + *****************************************************************************/ + +#ifndef IMAGEEXCEPTIONS_H +#define IMAGEEXCEPTIONS_H + +#include "core/GrinderExceptions.h" + +namespace grndr +{ + class ImageBuild; + class ImageEditor; + class Layer; + + class ImageException : public GrinderException + { + public: + ImageException(QString what); + }; + + class ImageBuildException : public ImageException + { + public: + ImageBuildException(const ImageBuild* imageBuild, QString what); + + public: + const ImageBuild* imageBuild() const { return _imageBuild; } + + private: + const ImageBuild* _imageBuild{nullptr}; + }; + + class ImageEditorException : public ImageException + { + public: + ImageEditorException(const ImageEditor* imageEditor, QString what); + + public: + const ImageEditor* imageEditor() const { return _imageEditor; } + + private: + const ImageEditor* _imageEditor{nullptr}; + }; + + class LayerException : public ImageBuildException + { + public: + LayerException(const Layer* layer, QString what); + + public: + const Layer* imageEditor() const { return _layer; } + + private: + const Layer* _layer{nullptr}; + }; +} + +#endif diff --git a/Grinder/image/Layer.cpp b/Grinder/image/Layer.cpp new file mode 100644 index 0000000000000000000000000000000000000000..53f3be1cdf99e8bf4baf8b9a47312129cab2e0b2 --- /dev/null +++ b/Grinder/image/Layer.cpp @@ -0,0 +1,115 @@ +/****************************************************************************** + * File: Layer.cpp + * Date: 17.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "Layer.h" +#include "ImageBuild.h" +#include "ImageExceptions.h" +#include "DraftItemCatalog.h" + +const char* Layer::Serialization_Value_Visible = "Visible"; + +Layer::Layer(ImageBuild* imageBuild, QString name) : ImageBuildItem(imageBuild, name), + _draftItems{this} +{ + +} + +void Layer::initLayer() +{ + ImageBuildItem::initImageBuildItem(); +} + +std::shared_ptr<DraftItem> Layer::createDraftItem(DraftItemType type) +{ + if (type == DraftItemType::Undefined) + throw std::invalid_argument(_EXCPT("type may not be DraftItemType::Undefined")); + + // Create new block using the block factory; cast the unique ptr to a shared one as well + std::shared_ptr<DraftItem> item = DraftItemCatalog::createDraftItem(this, type); + + try { // Propagate initialization errors to the caller + item->initDraftItem(); + } + catch (...) { + throw; + } + + _draftItems.push_back(item); + emit draftItemCreated(item); + + return item; +} + +void Layer::removeDraftItem(const DraftItem* item) +{ + if (item) + { + auto it = _draftItems.find(item); + + if (it != _draftItems.cend()) + { + // Keep a copy of the shared_ptr holding the draft item to increase its use count; + // otherwise, the draft item will be deleted before it has been removed from the vector, potentially causing a crash + auto item = *it; + + emit draftItemRemoved(*it); + _draftItems.erase(it); + } + else + throw LayerException{this, _EXCPT("Tried to remove a draft item not belonging to this layer")}; + } +} + +int Layer::getZOrder() const +{ + return _imageBuild->layers().indexOf(this); +} + +void Layer::setVisible(bool visible) +{ + if (visible != _isVisible) + { + _isVisible = visible; + + if (_isVisible) + emit layerShown(); + else + emit layerHidden(); + } +} + +void Layer::serialize(SerializationContext& ctx) const +{ + ImageBuildItem::serialize(ctx); + + // Serialize values + ctx.settings()[Serialization_Value_Visible] = _isVisible; + + // Serialize all draft items + ctx.beginGroup(DraftItemVector::Serialization_Group, true); + _draftItems.serialize(DraftItemVector::Serialization_Element, ctx); + ctx.endGroup(); +} + +void Layer::deserialize(DeserializationContext& ctx) +{ + ImageBuildItem::deserialize(ctx); + + // Deserialize values + _isVisible = ctx.settings()[Serialization_Value_Visible].toBool(); + + // Deserialize all draft items + if (ctx.beginGroup(DraftItemVector::Serialization_Group)) + { + _draftItems.deserialize(DraftItemVector::Serialization_Element, ctx, [this](const SettingsContainer& settings) { + DraftItemType type = settings[DraftItem::Serialization_Value_Type].toString(); + + return createDraftItem(type); + }); + + ctx.endGroup(); + } +} diff --git a/Grinder/image/Layer.h b/Grinder/image/Layer.h new file mode 100644 index 0000000000000000000000000000000000000000..d0f70425d86dd38c599048de115eb216a320abfa --- /dev/null +++ b/Grinder/image/Layer.h @@ -0,0 +1,57 @@ +/****************************************************************************** + * File: Layer.h + * Date: 17.3.2018 + *****************************************************************************/ + +#ifndef LAYER_H +#define LAYER_H + +#include "ImageBuildItem.h" +#include "DraftItemVector.h" + +namespace grndr +{ + class Layer : public ImageBuildItem + { + Q_OBJECT + + public: + static const char* Serialization_Value_Visible; + + public: + Layer(ImageBuild* imageBuild, QString name = ""); + + public: + void initLayer(); + + public: + std::shared_ptr<DraftItem> createDraftItem(DraftItemType type); + void removeDraftItem(const DraftItem* item); + + public: + int getZOrder() const; + + bool isVisible() const { return _isVisible; } + void setVisible(bool visible = true); + + const DraftItemVector& draftItems() const { return _draftItems; } + + public: + virtual void serialize(SerializationContext& ctx) const override; + virtual void deserialize(DeserializationContext& ctx) override; + + signals: + void layerShown(); + void layerHidden(); + + void draftItemCreated(const std::shared_ptr<DraftItem>&); + void draftItemRemoved(const std::shared_ptr<DraftItem>&); + + private: + bool _isVisible{true}; + + DraftItemVector _draftItems; + }; +} + +#endif diff --git a/Grinder/image/LayerVector.cpp b/Grinder/image/LayerVector.cpp new file mode 100644 index 0000000000000000000000000000000000000000..9a989af150634a4c39b6cc104f4428581d3df44d --- /dev/null +++ b/Grinder/image/LayerVector.cpp @@ -0,0 +1,22 @@ +/****************************************************************************** + * File: LayerVector.cpp + * Date: 17.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "LayerVector.h" + +const char* LayerVector::Serialization_Group = "Layers"; +const char* LayerVector::Serialization_Element = "Layer"; + +LayerVector::LayerVector(const ImageBuild* imageBuild) : + _imageBuild{imageBuild} +{ + if (!imageBuild) + throw std::invalid_argument{_EXCPT("imageBuild may not be null")}; +} + +LayerVector::pointer_type LayerVector::selectByName(QString name, bool caseSensitive) const +{ + return selectFirst([name, caseSensitive](auto layer) { return layer->getName().compare(name, caseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive) == 0; }); +} diff --git a/Grinder/image/LayerVector.h b/Grinder/image/LayerVector.h new file mode 100644 index 0000000000000000000000000000000000000000..678bb0d93cf82035187e79667b2d084aad29ccf5 --- /dev/null +++ b/Grinder/image/LayerVector.h @@ -0,0 +1,31 @@ +/****************************************************************************** + * File: LayerVector.h + * Date: 17.3.2018 + *****************************************************************************/ + +#ifndef LAYERVECTOR_H +#define LAYERVECTOR_H + +#include "common/ObjectVector.h" +#include "Layer.h" + +namespace grndr +{ + class LayerVector : public ObjectVector<Layer> + { + public: + static const char* Serialization_Group; + static const char* Serialization_Element; + + public: + LayerVector(const ImageBuild* imageBuild); + + public: + pointer_type selectByName(QString name, bool caseSensitive = false) const; + + private: + const ImageBuild* _imageBuild{nullptr}; + }; +} + +#endif diff --git a/Grinder/image/draftitems/BoxDraftItem.cpp b/Grinder/image/draftitems/BoxDraftItem.cpp new file mode 100644 index 0000000000000000000000000000000000000000..9b09637196ccfa66577de5e7c1674fe5982e6331 --- /dev/null +++ b/Grinder/image/draftitems/BoxDraftItem.cpp @@ -0,0 +1,63 @@ +/****************************************************************************** + * File: BoxDraftItem.cpp + * Date: 20.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "BoxDraftItem.h" +#include "BoxDraftItemRenderer.h" +#include "common/properties/RangeConstraint.h" + +const DraftItemType BoxDraftItem::type_value = DraftItemType::Box; + +BoxDraftItem::BoxDraftItem(Layer* layer) : DraftItem(layer, type_value) +{ + +} + +std::unique_ptr<DraftItemRendererBase> BoxDraftItem::createRenderer(const DraftItemRendererBase::RendererStyle& rendererStyle) const +{ + return std::make_unique<BoxDraftItemRenderer>(this, rendererStyle); +} + +void BoxDraftItem::createProperties() +{ + DraftItem::createProperties(); + + // Create specific properties + _boxSize = createProperty<SizeProperty>(PropertyID::Size, "Size", QSize{50, 50}); + boxSize()->setDescription("The size of the box."); + + _lineWidth = createProperty<UIntProperty>(PropertyID::LineWidth, "Line width", 5); + lineWidth()->createConstraint<RangeConstraint>(1, 100); + lineWidth()->setDescription("The width of the box lines."); + + // Override some property defaults + hasDirection()->setValue(true); +} + +void BoxDraftItem::setDragPropertyValues(QPoint initialPos, QPoint currentPos) +{ + auto vec = currentPos - initialPos; + QSize size{vec.x(), vec.y()}; + boxSize()->setValue(size); +} + +void BoxDraftItem::normalizePropertyValues() +{ + // Adjust for negative box sizes + QPoint pos = *position(); + QSize size = *boxSize(); + + if (size.width() < 0) + pos.setX(pos.x() + size.width()); + + if (size.height() < 0) + pos.setY(pos.y() + size.height()); + + size.setWidth(std::abs(size.width())); + size.setHeight(std::abs(size.height())); + + position()->setValue(pos); + boxSize()->setValue(size); +} diff --git a/Grinder/image/draftitems/BoxDraftItem.h b/Grinder/image/draftitems/BoxDraftItem.h new file mode 100644 index 0000000000000000000000000000000000000000..ce4f4d0929b0aaa5301bb89dc0b9f3553b32f8c1 --- /dev/null +++ b/Grinder/image/draftitems/BoxDraftItem.h @@ -0,0 +1,46 @@ +/****************************************************************************** + * File: BoxDraftItem.h + * Date: 20.3.2018 + *****************************************************************************/ + +#ifndef BOXDRAFTITEM_H +#define BOXDRAFTITEM_H + +#include "image/DraftItem.h" + +namespace grndr +{ + class BoxDraftItem : public DraftItem + { + Q_OBJECT + + public: + static const DraftItemType type_value; + + public: + BoxDraftItem(Layer* layer); + + public: + virtual std::unique_ptr<DraftItemRendererBase> createRenderer(const DraftItemRendererBase::RendererStyle& rendererStyle) const override; + + public: + auto boxSize() { return dynamic_cast<SizeProperty*>(_boxSize.get()); } + auto boxSize() const { return dynamic_cast<SizeProperty*>(_boxSize.get()); } + auto lineWidth() { return dynamic_cast<UIntProperty*>(_lineWidth.get()); } + auto lineWidth() const { return dynamic_cast<UIntProperty*>(_lineWidth.get()); } + + public: + virtual void setDragPropertyValues(QPoint initialPos, QPoint currentPos) override; + + virtual void normalizePropertyValues() override; + + protected: + virtual void createProperties() override; + + private: + std::shared_ptr<PropertyBase> _boxSize; + std::shared_ptr<PropertyBase> _lineWidth; + }; +} + +#endif diff --git a/Grinder/image/draftitems/BoxDraftItemRenderer.cpp b/Grinder/image/draftitems/BoxDraftItemRenderer.cpp new file mode 100644 index 0000000000000000000000000000000000000000..59931a97e9f5d82ee064bd6453dd2255bba58e1a --- /dev/null +++ b/Grinder/image/draftitems/BoxDraftItemRenderer.cpp @@ -0,0 +1,35 @@ +/****************************************************************************** + * File: BoxDraftItemRenderer.cpp + * Date: 21.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "BoxDraftItemRenderer.h" +#include "util/MathUtils.h" + +void BoxDraftItemRenderer::render(QPainter* painter, RenderMode mode, RenderFlags flags) +{ + painter->save(); + + auto rect = QRect{getItemPosition(mode), *_draftItem->boxSize()}; + + // Draw the selection background as an outer glow + if (flags.testFlag(RenderFlag::Selected)) + { + painter->save(); + painter->setPen(createSelectionPen(*_draftItem->lineWidth(), flags.testFlag(RenderFlag::Inactive))); + painter->setOpacity(_rendererStyle.selectionOpacity); + painter->drawRect(rect); + painter->restore(); + } + + // Draw the box + painter->setPen(createStandardPen(*_draftItem->lineWidth())); + painter->drawRect(rect); + + // Draw an arrow showing the box's direction if the corresponding property has been set + if (_draftItem->hasDirection()->getValue() && (flags.testFlag(RenderFlag::Selected) || flags.testFlag(RenderFlag::ShowDirections))) + renderDirectionArrow(painter, rect.center()); + + painter->restore(); +} diff --git a/Grinder/image/draftitems/BoxDraftItemRenderer.h b/Grinder/image/draftitems/BoxDraftItemRenderer.h new file mode 100644 index 0000000000000000000000000000000000000000..b6d2e3ce4709a3479623b9795cdc0f6c3985e820 --- /dev/null +++ b/Grinder/image/draftitems/BoxDraftItemRenderer.h @@ -0,0 +1,24 @@ +/****************************************************************************** + * File: BoxDraftItemRenderer.h + * Date: 21.3.2018 + *****************************************************************************/ + +#ifndef BOXDRAFTITEMRENDERER_H +#define BOXDRAFTITEMRENDERER_H + +#include "image/DraftItemRenderer.h" +#include "BoxDraftItem.h" + +namespace grndr +{ + class BoxDraftItemRenderer : public DraftItemRenderer<BoxDraftItem> + { + public: + using DraftItemRenderer<BoxDraftItem>::DraftItemRenderer; + + public: + virtual void render(QPainter* painter, RenderMode mode, RenderFlags flags) override; + }; +} + +#endif diff --git a/Grinder/image/draftitems/LineDraftItem.cpp b/Grinder/image/draftitems/LineDraftItem.cpp new file mode 100644 index 0000000000000000000000000000000000000000..d1689263df7fb44d056c97bfa10a51eb8df91eed --- /dev/null +++ b/Grinder/image/draftitems/LineDraftItem.cpp @@ -0,0 +1,49 @@ +/****************************************************************************** + * File: LineDraftItem.cpp + * Date: 20.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "LineDraftItem.h" +#include "LineDraftItemRenderer.h" +#include "common/properties/RangeConstraint.h" + +const DraftItemType LineDraftItem::type_value = DraftItemType::Line; + +LineDraftItem::LineDraftItem(Layer* layer) : DraftItem(layer, type_value) +{ + +} + +std::unique_ptr<DraftItemRendererBase> LineDraftItem::createRenderer(const DraftItemRendererBase::RendererStyle& rendererStyle) const +{ + return std::make_unique<LineDraftItemRenderer>(this, rendererStyle); +} + +void LineDraftItem::setDragPropertyValues(QPoint initialPos, QPoint currentPos) +{ + Q_UNUSED(initialPos); + + endPosition()->setValue(currentPos); +} + +void LineDraftItem::setDefaultPropertyValues() +{ + DraftItem::setDefaultPropertyValues(); + + // Create a default line that extends 50px to the right + endPosition()->setValue(position()->getValue() + QPoint{50, 0}); +} + +void LineDraftItem::createProperties() +{ + DraftItem::createProperties(); + + // Create specific properties + _endPosition = createProperty<PointProperty>(PropertyID::EndPosition, "End position", QPoint{0, 0}); + endPosition()->setDescription("The end position of the line."); + + _lineWidth = createProperty<UIntProperty>(PropertyID::LineWidth, "Line width", 3); + lineWidth()->createConstraint<RangeConstraint>(1, 100); + lineWidth()->setDescription("The width of the line."); +} diff --git a/Grinder/image/draftitems/LineDraftItem.h b/Grinder/image/draftitems/LineDraftItem.h new file mode 100644 index 0000000000000000000000000000000000000000..c27ced9f174556054d05ba0e5ace3c2380e4c00f --- /dev/null +++ b/Grinder/image/draftitems/LineDraftItem.h @@ -0,0 +1,46 @@ +/****************************************************************************** + * File: LineDraftItem.h + * Date: 20.3.2018 + *****************************************************************************/ + +#ifndef LINEDRAFTITEM_H +#define LINEDRAFTITEM_H + +#include "image/DraftItem.h" + +namespace grndr +{ + class LineDraftItem : public DraftItem + { + Q_OBJECT + + public: + static const DraftItemType type_value; + + public: + LineDraftItem(Layer* layer); + + public: + virtual std::unique_ptr<DraftItemRendererBase> createRenderer(const DraftItemRendererBase::RendererStyle& rendererStyle) const override; + + public: + virtual void setDragPropertyValues(QPoint initialPos, QPoint currentPos) override; + + virtual void setDefaultPropertyValues() override; + + public: + auto endPosition() { return dynamic_cast<PointProperty*>(_endPosition.get()); } + auto endPosition() const { return dynamic_cast<PointProperty*>(_endPosition.get()); } + auto lineWidth() { return dynamic_cast<UIntProperty*>(_lineWidth.get()); } + auto lineWidth() const { return dynamic_cast<UIntProperty*>(_lineWidth.get()); } + + protected: + virtual void createProperties() override; + + private: + std::shared_ptr<PropertyBase> _endPosition; + std::shared_ptr<PropertyBase> _lineWidth; + }; +} + +#endif diff --git a/Grinder/image/draftitems/LineDraftItemRenderer.cpp b/Grinder/image/draftitems/LineDraftItemRenderer.cpp new file mode 100644 index 0000000000000000000000000000000000000000..7e6f425156f683db87519b7760fb59884c559baa --- /dev/null +++ b/Grinder/image/draftitems/LineDraftItemRenderer.cpp @@ -0,0 +1,58 @@ +/****************************************************************************** + * File: LineDraftItemRenderer.cpp + * Date: 21.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "LineDraftItemRenderer.h" +#include "util/MathUtils.h" + +void LineDraftItemRenderer::render(QPainter* painter, RenderMode mode, RenderFlags flags) +{ + painter->save(); + + QPoint endPosition = *_draftItem->endPosition(); + + if (mode.testFlag(RenderModeFlag::RelativePositions)) + endPosition -= *_draftItem->position(); + + // Draw the selection background as an outer glow + if (flags.testFlag(RenderFlag::Selected)) + { + painter->save(); + painter->setPen(createSelectionPen(*_draftItem->lineWidth(), flags.testFlag(RenderFlag::Inactive))); + painter->setOpacity(_rendererStyle.selectionOpacity); + painter->drawLine(getItemPosition(mode), endPosition); + painter->restore(); + } + + // Draw the line + painter->setPen(createStandardPen(*_draftItem->lineWidth())); + painter->drawLine(getItemPosition(mode), endPosition); + + // Draw an arrow showing the line's direction if the corresponding property has been set + if (_draftItem->hasDirection()->getValue() && (flags.testFlag(RenderFlag::Selected) || flags.testFlag(RenderFlag::ShowDirections))) + { + // Calculate the line center + auto vec = *_draftItem->endPosition() - *_draftItem->position(); + auto center = endPosition - (vec * 0.5); + + renderDirectionArrow(painter, center); + } + + painter->restore(); +} + +QPainterPath LineDraftItemRenderer::shape() const +{ + // Create a shape that is slightly thicker than the shown line + QPainterPath linePath; + linePath.moveTo(QPointF{0.0, 0.0}); + linePath.lineTo(*_draftItem->endPosition() - *_draftItem->position()); + + QPainterPathStroker ps{createSelectionPen(*_draftItem->lineWidth())}; + auto path = ps.createStroke(linePath); + path.addPath(linePath); + + return path; +} diff --git a/Grinder/image/draftitems/LineDraftItemRenderer.h b/Grinder/image/draftitems/LineDraftItemRenderer.h new file mode 100644 index 0000000000000000000000000000000000000000..b9f95d9a9b03fae0cae96587390c584188dc34f6 --- /dev/null +++ b/Grinder/image/draftitems/LineDraftItemRenderer.h @@ -0,0 +1,25 @@ +/****************************************************************************** + * File: LineDraftItemRenderer.h + * Date: 21.3.2018 + *****************************************************************************/ + +#ifndef LINEDRAFTITEMRENDERER_H +#define LINEDRAFTITEMRENDERER_H + +#include "image/DraftItemRenderer.h" +#include "LineDraftItem.h" + +namespace grndr +{ + class LineDraftItemRenderer : public DraftItemRenderer<LineDraftItem> + { + public: + using DraftItemRenderer<LineDraftItem>::DraftItemRenderer; + + public: + virtual void render(QPainter* painter, RenderMode mode, RenderFlags flags) override; + virtual QPainterPath shape() const override; + }; +} + +#endif diff --git a/Grinder/main.cpp b/Grinder/main.cpp new file mode 100644 index 0000000000000000000000000000000000000000..c64d54569cb33f8df3a8464edc9c75543a39d20f --- /dev/null +++ b/Grinder/main.cpp @@ -0,0 +1,37 @@ +/****************************************************************************** + * File: main.cpp + * Date: 11.1.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "core/GrinderApplication.h" + +int main(int argc, char *argv[]) +{ + // Add additional search paths for Qt libraries/plugins +#if !defined(Q_OS_OSX) + QString libraryDir = "lib"; + + if (argc >= 1) + { + QFileInfo fi(argv[0]); + libraryDir = QFileInfo{fi.absolutePath(), "lib"}.absoluteFilePath(); + } + + QCoreApplication::addLibraryPath(libraryDir); + qputenv("QT_PLUGIN_PATH", libraryDir.toLatin1()); +#endif + + GrinderApplication app{argc, argv}; + + try { + return app.run(); + } catch (std::exception& e) { + ShowExceptionMessage(e.what()); + throw; + } + catch (...) { + ShowExceptionMessage(""); + throw; + } +} diff --git a/Grinder/pipeline/Block.cpp b/Grinder/pipeline/Block.cpp new file mode 100644 index 0000000000000000000000000000000000000000..e79526e4fee3af373fdc187a135640ee4c92aaaa --- /dev/null +++ b/Grinder/pipeline/Block.cpp @@ -0,0 +1,127 @@ +/****************************************************************************** + * File: Block.cpp + * Date: 15.1.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "Block.h" +#include "Pipeline.h" +#include "PipelineManager.h" +#include "BlockCatalog.h" +#include "PipelineExceptions.h" + +const char* Block::Serialization_Value_Type = "Type"; +const char* Block::Serialization_Value_Index = "Index"; + +Block::Block(Pipeline* pipeline, BlockType type, BlockCategory category, QString name) : PipelineItem(pipeline, name), + _type{type}, _category{category}, _ports{this} +{ + +} + +void Block::initBlock() +{ + PipelineItem::initPipelineItem(); + + createPorts(); +} + +void Block::verifyConnection(const Port* src, const Port* dest) const +{ + if (!src || !dest) + throw std::invalid_argument{_EXCPT("src and dest may not be null")}; + + // The source port must belong to this block + if (src->block() != this) + throw BlockException{this, _EXCPT("The source port must belong to the current block")}; + + // Do not connect to self + if (dest->block() == this) + throw BlockException{this, _EXCPT("Cannot connect a block to itself")}; +} + +QString Block::getFormattedName() const +{ + return QString{"%1::%2"}.arg(_pipeline->getName()).arg(_name); +} + +void Block::serialize(SerializationContext& ctx) const +{ + PipelineItem::serialize(ctx); + + // Serialize values + ctx.settings()[Serialization_Value_Type] = _type; + ctx.settings()[Serialization_Value_Index] = ctx.addBlock(this); + + // Serialize all ports + ctx.beginGroup(PortVector::Serialization_Group, true); + _ports.serialize(PortVector::Serialization_Element, ctx); + ctx.endGroup(); +} + +void Block::deserialize(DeserializationContext& ctx) +{ + PipelineItem::deserialize(ctx); + + // Deserialize values + _type = ctx.settings()[Serialization_Value_Type].toString(); + ctx.addBlock(ctx.settings()[Serialization_Value_Index].toInt(), this); + + // Deserialize all ports + if (ctx.beginGroup(PortVector::Serialization_Group)) + { + _ports.deserialize(PortVector::Serialization_Element, ctx, [this](const SettingsContainer& settings) { + PortType type = settings[Port::Serialization_Value_Type].toString(); + return _ports.selectByType(type); + }); + + ctx.endGroup(); + } +} + +std::shared_ptr<Port> Block::createPort(PortType type, Port::Direction dir, DataDescriptors dataDescriptors, QString name) +{ + if (type == PortType::Undefined) + throw std::invalid_argument{_EXCPT("type may not be PortType::Undefined")}; + + if (_ports.selectByType(type)) + throw BlockException{this, _EXCPT(QString{"A port of type %1 already exists"}.arg(type))}; + + auto port = std::make_shared<Port>(this, type, dir, dataDescriptors, name); + + try { // Propage initialization errors to the caller + port->initPort(); + } + catch (...) { + throw; + } + + _ports.push_back(port); + return port; +} + +void Block::removePort(PortType type) +{ + if (type != PortType::Undefined) + { + auto port = _ports.selectByType(type); + + if (port) + removePort(port.get()); + else + throw BlockException{this, _EXCPT(QString{"Tried to remove a non-existing port (Type: %1)"}.arg(type))}; + } +} + +void Block::removePort(const Port* port) +{ + if (port) + { + auto it = _ports.find(port); + + if (it != _ports.cend()) + _ports.erase(it); + else + throw BlockException{this, _EXCPT("Tried to remove a port not belonging to this block")}; + } +} diff --git a/Grinder/pipeline/Block.h b/Grinder/pipeline/Block.h new file mode 100644 index 0000000000000000000000000000000000000000..bb0455da8506e6891824f990f8ad9e59f4415cae --- /dev/null +++ b/Grinder/pipeline/Block.h @@ -0,0 +1,62 @@ +/****************************************************************************** + * File: Block.h + * Date: 15.1.2018 + *****************************************************************************/ + +#ifndef BLOCK_H +#define BLOCK_H + +#include "PipelineItem.h" +#include "BlockType.h" +#include "BlockCategory.h" +#include "PortVector.h" +#include "engine/ProcessorBase.h" + +namespace grndr +{ + class Block : public PipelineItem + { + Q_OBJECT + + public: + static const char* Serialization_Value_Type; + static const char* Serialization_Value_Index; + + public: + Block(Pipeline* pipeline, BlockType type, BlockCategory category, QString name = ""); + + public: + virtual void initBlock(); + + virtual void verifyConnection(const Port* src, const Port* dest) const; + + public: + virtual std::unique_ptr<ProcessorBase> createProcessor() const = 0; + + public: + QString getFormattedName() const; + BlockType getType() const { return _type; } + BlockCategory getCategory() const { return _category; } + + const PortVector& ports() const { return _ports; } + + public: + virtual void serialize(SerializationContext& ctx) const override; + virtual void deserialize(DeserializationContext& ctx) override; + + protected: + virtual void createPorts() = 0; + + std::shared_ptr<Port> createPort(PortType type, Port::Direction dir, DataDescriptors dataDescriptors, QString name = ""); + void removePort(PortType type); + void removePort(const Port* port); + + protected: + BlockType _type{BlockType::Undefined}; + BlockCategory _category{BlockCategory::Undefined}; + + PortVector _ports; + }; +} + +#endif diff --git a/Grinder/pipeline/BlockCatalog.cpp b/Grinder/pipeline/BlockCatalog.cpp new file mode 100644 index 0000000000000000000000000000000000000000..94bf2d9aeeeb19b410e200f6de73ba8d00dbabbe --- /dev/null +++ b/Grinder/pipeline/BlockCatalog.cpp @@ -0,0 +1,103 @@ +/****************************************************************************** + * File: BlockCatalog.cpp + * Date: 29.1.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "BlockCatalog.h" + +#include "blocks/InputBlock.h" +#include "blocks/OutputBlock.h" +#include "blocks/ConvertToGrayscaleBlock.h" +#include "blocks/BinaryThresholdBlock.h" + +#define REGISTER_BLOCK_TYPE(cls, desc) registerBlockType(cls::type_value, cls::category_value, [](Pipeline* pipeline, QString name) { return std::make_unique<cls>(pipeline, name); }, desc) + +std::map<BlockType, BlockCatalog::BlockDescriptor> BlockCatalog::s_descriptors; + +std::set<BlockType> BlockCatalog::getTypes(BlockCategory category) +{ + std::set<BlockType> types; + + for (const auto& desc : s_descriptors) + { + if (category == BlockCategory::Undefined || category == desc.second.category) + types.insert(desc.second.type); + } + + return types; +} + +std::set<BlockCategory> BlockCatalog::getCategories() +{ + std::set<BlockCategory> cats; + + for (const auto& desc : s_descriptors) + { + if (desc.second.category != BlockCategory::Undefined) + cats.insert(desc.second.category); + } + + return cats; +} + +BlockCategory BlockCatalog::getCategory(BlockType type) +{ + auto it = s_descriptors.find(type); + + if (it != s_descriptors.cend()) + return it->second.category; + else + return BlockCategory::Undefined; +} + +QString BlockCatalog::getDescription(BlockType type) +{ + auto it = s_descriptors.find(type); + + if (it != s_descriptors.cend()) + return it->second.description; + else + return ""; +} + +void BlockCatalog::registerBlockType(BlockType type, BlockCategory category, BlockCatalog::block_creator_type creator, QString description) +{ + if (type == BlockType::Undefined) + throw std::invalid_argument{_EXCPT("type may not be BlockType::Undefined")}; + + if (category == BlockCategory::Undefined) + throw std::invalid_argument{_EXCPT("category may not be BlockCategory::Undefined")}; + + if (!creator) + throw std::invalid_argument{_EXCPT("creator may not be null")}; + + s_descriptors[type] = {type, category, creator, description}; +} + +std::unique_ptr<Block> BlockCatalog::createBlock(Pipeline* pipeline, BlockType type, QString name) +{ + if (type == BlockType::Undefined) + throw std::invalid_argument{_EXCPT("type may not be BlockType::Undefined")}; + + if (s_descriptors.find(type) != s_descriptors.end()) + { + auto descriptor = s_descriptors.at(type); + auto block = descriptor.creator(pipeline, name); + + if (!block) + throw std::runtime_error{_EXCPT(QString{"Failed to create a block of type %1"}.arg(type))}; + + return block; + } + else + throw std::invalid_argument{_EXCPT("The given type has not been registered")}; +} + +void BlockCatalog::registerStandardBlocks() +{ + REGISTER_BLOCK_TYPE(InputBlock, "The beginning of every pipeline; provides the currently active image of the image stack."); + REGISTER_BLOCK_TYPE(OutputBlock, "The final stage of every pipeline; makes the image available for editing and saving."); + REGISTER_BLOCK_TYPE(ConvertToGrayscaleBlock, "Converts its input to a grayscale image."); + REGISTER_BLOCK_TYPE(BinaryThresholdBlock, "Applies a binary threshold to its input."); +} diff --git a/Grinder/pipeline/BlockCatalog.h b/Grinder/pipeline/BlockCatalog.h new file mode 100644 index 0000000000000000000000000000000000000000..45432802c0b233d0830c4150bc26aea74a52e0ec --- /dev/null +++ b/Grinder/pipeline/BlockCatalog.h @@ -0,0 +1,60 @@ +/****************************************************************************** + * File: BlockCatalog.h + * Date: 29.1.2018 + *****************************************************************************/ + +#ifndef BLOCKCATALOG_H +#define BLOCKCATALOG_H + +#include <map> +#include <set> +#include <functional> +#include <memory> + +#include "BlockType.h" +#include "BlockCategory.h" + +namespace grndr +{ + class Pipeline; + class Block; + + class BlockCatalog + { + private: + using block_creator_type = std::function<std::unique_ptr<Block>(Pipeline*, QString)>; + + private: + BlockCatalog() { } + + public: + static std::unique_ptr<Block> createBlock(Pipeline* pipeline, BlockType type, QString name = ""); + + static void registerStandardBlocks(); + + public: + static std::set<BlockType> getTypes(BlockCategory category = BlockCategory::Undefined); + static std::set<BlockCategory> getCategories(); + + static BlockCategory getCategory(BlockType type); + static QString getDescription(BlockType type); + + private: + static void registerBlockType(BlockType type, BlockCategory category, block_creator_type creator, QString description); + + private: + struct BlockDescriptor + { + BlockType type; + BlockCategory category; + + block_creator_type creator; + + QString description; + }; + + static std::map<BlockType, BlockDescriptor> s_descriptors; + }; +} + +#endif diff --git a/Grinder/pipeline/BlockCategory.cpp b/Grinder/pipeline/BlockCategory.cpp new file mode 100644 index 0000000000000000000000000000000000000000..d2fcd74562d1aca5b480e9a1c15edbd7dc6fd3ec --- /dev/null +++ b/Grinder/pipeline/BlockCategory.cpp @@ -0,0 +1,15 @@ +/****************************************************************************** + * File: BlockCategory.cpp + * Date: 24.1.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "BlockCategory.h" + +const char* BlockCategory::Undefined = ""; + +const char* BlockCategory::Input = "Input"; +const char* BlockCategory::Output = "Output"; + +const char* BlockCategory::Conversion = "Conversion"; +const char* BlockCategory::Thresholding = "Thresholding"; diff --git a/Grinder/pipeline/BlockCategory.h b/Grinder/pipeline/BlockCategory.h new file mode 100644 index 0000000000000000000000000000000000000000..f02ea60402d7a9621da8163d824763827789ea35 --- /dev/null +++ b/Grinder/pipeline/BlockCategory.h @@ -0,0 +1,42 @@ +/****************************************************************************** + * File: BlockCategory.h + * Date: 24.1.2018 + *****************************************************************************/ + +#ifndef BLOCKCATEGORY_H +#define BLOCKCATEGORY_H + +#include <vector> +#include <map> +#include <QString> + +#include "BlockType.h" + +namespace grndr +{ + class BlockCategory : public QString + { + public: + static const char* Undefined; + + static const char* Input; + static const char* Output; + + static const char* Conversion; + static const char* Thresholding; + + public: + using QString::QString; + + BlockCategory() = default; + BlockCategory(const BlockCategory& other) = default; + BlockCategory(BlockCategory&& other) = default; + BlockCategory(const QString& str) { *static_cast<QString*>(this) = str; } + + BlockCategory& operator =(const BlockCategory& other) = default; + BlockCategory& operator =(BlockCategory&& other) = default; + BlockCategory& operator =(const QString& str) { *static_cast<QString*>(this) = str; return *this; } + }; +} + +#endif diff --git a/Grinder/pipeline/BlockHierarchy.cpp b/Grinder/pipeline/BlockHierarchy.cpp new file mode 100644 index 0000000000000000000000000000000000000000..8f08457ee1534abebb966e04457d1d4dee784133 --- /dev/null +++ b/Grinder/pipeline/BlockHierarchy.cpp @@ -0,0 +1,158 @@ +/****************************************************************************** + * File: BlockHierarchy.cpp + * Date: 20.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "BlockHierarchy.h" +#include "PipelineExceptions.h" +#include "Pipeline.h" +#include "Block.h" + +BlockHierarchy::BlockHierarchy(const Pipeline* pipeline) +{ + if (pipeline) + createFromPipeline(pipeline); +} + +void BlockHierarchy::createFromPipeline(const Pipeline* pipeline) +{ + if (!pipeline) + throw std::invalid_argument{_EXCPT("pipeline may not be null")}; + + _hierarchy.clear(); + + // Level-0 blocks mark the beginning(s) of our pipeline + auto level0 = findLevel0Blocks(pipeline); + + if (level0.empty()) // No level-0 blocks means that the pipeline has no beginnings (the pipeline starts with cycles) + throw PipelineException{pipeline, _EXCPT("The pipeline has no starting blocks")}; + + _hierarchy.push_back(std::move(level0)); + + // Recursively create all following levels + createNextLevel(pipeline); + + // All blocks must be part of the hierarchy; if this isn't the case, cyclic dependencies most likely exist + unsigned int blocksInHierarchy = 0; + + for (const auto& level : _hierarchy) + blocksInHierarchy += level.size(); + + if (blocksInHierarchy != pipeline->blocks().size()) + throw PipelineException{pipeline, _EXCPT("Not all blocks could be added to the hierarchy; this is usually caused by cyclic dependencies in the graph")}; +} + +int BlockHierarchy::getBlockLevel(const Block* block) const +{ + for (unsigned int i = 0; i < _hierarchy.size(); ++i) + { + const auto& level = _hierarchy[i]; + + for (const auto& blockInHierarchy : level) + { + if (blockInHierarchy == block) + return i; + } + } + + return -1; +} + +void BlockHierarchy::createNextLevel(const Pipeline* pipeline) +{ + const auto nextLevel = findNextLevelBlocks(pipeline, _hierarchy.back()); + + // If a new level was created, add it and create the next level once again + if (!nextLevel.empty()) + { + _hierarchy.push_back(std::move(nextLevel)); + createNextLevel(pipeline); + } +} + +bool BlockHierarchy::isBlockInHierarchy(const Block* block) const +{ + return getBlockLevel(block) != -1; +} + +BlockHierarchy::HierarchyLevel BlockHierarchy::findLevel0Blocks(const Pipeline* pipeline) const +{ + HierarchyLevel level0; + + // Find all blocks that have no inputs (level-0 blocks) + for (const auto& block : pipeline->blocks()) + { + bool isLevel0Block = true; + + for (const auto& port : block->ports().selectByDirection(Port::Direction::In)) + { + if (port->isConnected()) + { + isLevel0Block = false; + break; + } + } + + if (isLevel0Block) + level0.push_back(block.get()); + } + + return level0; +} + +BlockHierarchy::HierarchyLevel BlockHierarchy::findNextLevelBlocks(const Pipeline* pipeline, const HierarchyLevel& level) const +{ + HierarchyLevel nextLevel; + + for (const auto& block : level) + { + auto nextLevelForBlock = findNextLevelBlocks(block); + + for (const auto& newBlock : nextLevelForBlock) + { + // If the found block is already in a lower level of the hierarchy, a cycle exists + if (isBlockInHierarchy(newBlock)) + throw PipelineException{pipeline, _EXCPT("A cycle exists in the pipeline")}; + + // Otherwise, add the found block to the level + if (std::find(nextLevel.begin(), nextLevel.end(), newBlock) == nextLevel.end()) + nextLevel.push_back(newBlock); + } + } + + return nextLevel; +} + +BlockHierarchy::HierarchyLevel BlockHierarchy::findNextLevelBlocks(const Block* block) const +{ + HierarchyLevel nextLevel; + + // Find all blocks this block is connected to + for (const auto& port : block->ports().selectByDirection(Port::Direction::Out)) + { + for (const auto& con : port->connections()) + { + // Check if all inputs of the connected block are in the hierarchy; otherwise, skip it + if (checkBlockInputs(con->dest(), block, nextLevel)) + nextLevel.push_back(con->dest()); + } + } + + return nextLevel; +} + +bool BlockHierarchy::checkBlockInputs(const Block* block, const Block* currentBlock, const HierarchyLevel& currentLevel) const +{ + for (const auto& port : block->ports().selectByDirection(Port::Direction::In)) + { + for (const auto& con : port->getConnections(Port::Direction::In)) + { + // Check if the source is already in the hierarchy or in the currently built level; if not, this block must be postponed to a higher level + if (con->source() != currentBlock && !isBlockInHierarchy(con->source()) && std::find(currentLevel.begin(), currentLevel.end(), con->source()) == currentLevel.end()) + return false; + } + } + + return true; +} diff --git a/Grinder/pipeline/BlockHierarchy.h b/Grinder/pipeline/BlockHierarchy.h new file mode 100644 index 0000000000000000000000000000000000000000..6409770f83af158c7d18b8947aedee38cd8dfa6b --- /dev/null +++ b/Grinder/pipeline/BlockHierarchy.h @@ -0,0 +1,59 @@ +/****************************************************************************** + * File: BlockHierarchy.h + * Date: 20.2.2018 + *****************************************************************************/ + +#ifndef BLOCKHIERARCHY_H +#define BLOCKHIERARCHY_H + +#include <vector> + +namespace grndr +{ + class Pipeline; + class Block; + + class BlockHierarchy + { + public: + using HierarchyLevel = std::vector<const Block*>; + + public: + BlockHierarchy(const Pipeline* pipeline = nullptr); + + public: + void createFromPipeline(const Pipeline* pipeline = nullptr); + + public: + auto at(int index) { return _hierarchy.at(index); } + auto at(int index) const { return _hierarchy.at(index); } + + auto size() const { return _hierarchy.size(); } + auto empty() const { return _hierarchy.empty(); } + + auto begin() { return _hierarchy.begin(); } + auto begin() const { return _hierarchy.begin(); } + auto cbegin() const { return _hierarchy.cbegin(); } + auto end() { return _hierarchy.end(); } + auto end() const { return _hierarchy.end(); } + auto cend() const { return _hierarchy.cend(); } + + int getBlockLevel(const Block* block) const; + + private: + void createNextLevel(const Pipeline* pipeline); + + bool isBlockInHierarchy(const Block* block) const; + + HierarchyLevel findLevel0Blocks(const Pipeline* pipeline) const; + HierarchyLevel findNextLevelBlocks(const Pipeline* pipeline, const HierarchyLevel& level) const; + HierarchyLevel findNextLevelBlocks(const Block* block) const; + + bool checkBlockInputs(const Block* block, const Block* currentBlock, const HierarchyLevel& currentLevel) const; + + private: + std::vector<HierarchyLevel> _hierarchy; + }; +} + +#endif diff --git a/Grinder/pipeline/BlockType.cpp b/Grinder/pipeline/BlockType.cpp new file mode 100644 index 0000000000000000000000000000000000000000..ad7ec6c5ad9932202e3ba5936231b1a8391d8650 --- /dev/null +++ b/Grinder/pipeline/BlockType.cpp @@ -0,0 +1,16 @@ +/****************************************************************************** + * File: BlockType.cpp + * Date: 15.1.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "BlockType.h" + +const char* BlockType::Undefined = ""; + +const char* BlockType::Input = "Input"; +const char* BlockType::Output = "Output"; + +const char* BlockType::ConvertToGrayscale = "ConvertToGrayscale"; + +const char* BlockType::BinaryThreshold = "BinaryThreshold"; diff --git a/Grinder/pipeline/BlockType.h b/Grinder/pipeline/BlockType.h new file mode 100644 index 0000000000000000000000000000000000000000..e46cf37c35077f86522c10387d6a65b84fdab51c --- /dev/null +++ b/Grinder/pipeline/BlockType.h @@ -0,0 +1,39 @@ +/****************************************************************************** + * File: BlockType.h + * Date: 15.1.2018 + *****************************************************************************/ + +#ifndef BLOCKTYPE_H +#define BLOCKTYPE_H + +#include <QString> + +namespace grndr +{ + class BlockType final : public QString + { + public: + static const char* Undefined; /* Invalid block */ + + static const char* Input; + static const char* Output; + + static const char* ConvertToGrayscale; + + static const char* BinaryThreshold; + + public: + using QString::QString; + + BlockType() = default; + BlockType(const BlockType& other) = default; + BlockType(BlockType&& other) = default; + BlockType(const QString& str) { *static_cast<QString*>(this) = str; } + + BlockType& operator =(const BlockType& other) = default; + BlockType& operator =(BlockType&& other) = default; + BlockType& operator =(const QString& str) { *static_cast<QString*>(this) = str; return *this; } + }; +} + +#endif diff --git a/Grinder/pipeline/BlockVector.cpp b/Grinder/pipeline/BlockVector.cpp new file mode 100644 index 0000000000000000000000000000000000000000..9e73fbb42c53063472376a02dd521b66df9d17c7 --- /dev/null +++ b/Grinder/pipeline/BlockVector.cpp @@ -0,0 +1,27 @@ +/****************************************************************************** + * File: BlockVector.cpp + * Date: 15.1.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "BlockVector.h" + +const char* BlockVector::Serialization_Group = "Blocks"; +const char* BlockVector::Serialization_Element = "Block"; + +BlockVector::BlockVector(const Pipeline* pipeline) : + _pipeline{pipeline} +{ + if (!pipeline) + throw std::invalid_argument{_EXCPT("pipeline may not be null")}; +} + +BlockVector::pointer_vec_type BlockVector::selectByType(BlockType type) const +{ + return select([type](auto block) { return block->getType() == type; }); +} + +BlockVector::pointer_type BlockVector::selectByName(QString name, bool caseSensitive) const +{ + return selectFirst([name, caseSensitive](auto block) { return block->getName().compare(name, caseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive) == 0; }); +} diff --git a/Grinder/pipeline/BlockVector.h b/Grinder/pipeline/BlockVector.h new file mode 100644 index 0000000000000000000000000000000000000000..b986c83d17d0672dea064b11f54c17d0f31b4050 --- /dev/null +++ b/Grinder/pipeline/BlockVector.h @@ -0,0 +1,32 @@ +/****************************************************************************** + * File: BlockVector.h + * Date: 15.1.2018 + *****************************************************************************/ + +#ifndef BLOCKVECTOR_H +#define BLOCKVECTOR_H + +#include "common/ObjectVector.h" +#include "Block.h" + +namespace grndr +{ + class BlockVector : public ObjectVector<Block> + { + public: + static const char* Serialization_Group; + static const char* Serialization_Element; + + public: + BlockVector(const Pipeline* pipeline); + + public: + pointer_vec_type selectByType(BlockType type) const; + pointer_type selectByName(QString name, bool caseSensitive = false) const; + + private: + const Pipeline* _pipeline{nullptr}; + }; +} + +#endif diff --git a/Grinder/pipeline/Connection.cpp b/Grinder/pipeline/Connection.cpp new file mode 100644 index 0000000000000000000000000000000000000000..dfa9fc919436c4825e4af775fdb394444bf6b1e9 --- /dev/null +++ b/Grinder/pipeline/Connection.cpp @@ -0,0 +1,67 @@ +/****************************************************************************** + * File: Connection.cpp + * Date: 15.1.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "Connection.h" +#include "Port.h" + +const char* Connection::Serialization_Value_Source = "Source"; +const char* Connection::Serialization_Value_Dest = "Dest"; + +Connection::Connection(Port* src, Port* dest) : PipelineItem(src->pipeline()), + _sourcePort{src}, _destPort{dest} +{ + if (!src) + throw std::invalid_argument{_EXCPT("src may not be null")}; + + if (!dest) + throw std::invalid_argument{_EXCPT("dest may not be null")}; +} + +void Connection::initConnection() +{ + PipelineItem::initPipelineItem(); +} + +Block* Connection::_source() const +{ + if (_sourcePort) + return _sourcePort->block(); + else + return nullptr; +} + +Block* Connection::_dest() const +{ + if (_destPort) + return _destPort->block(); + else + return nullptr; +} + +void Connection::serialize(SerializationContext& ctx) const +{ + PipelineItem::serialize(ctx); + + // Serialize values + ctx.settings()[Serialization_Value_Source] = ctx.getPortIndex(_sourcePort); + ctx.settings()[Serialization_Value_Dest] = ctx.getPortIndex(_destPort); +} + +void Connection::deserialize(DeserializationContext& ctx) +{ + PipelineItem::deserialize(ctx); + + int sourceIndex = ctx.settings()[Serialization_Value_Source].toInt(); + int destIndex = ctx.settings()[Serialization_Value_Dest].toInt(); + auto sourcePort = ctx.getPort(sourceIndex); + auto destPort = ctx.getPort(destIndex); + + if (sourcePort && destPort) + { + _sourcePort = sourcePort; + _destPort = destPort; + } +} diff --git a/Grinder/pipeline/Connection.h b/Grinder/pipeline/Connection.h new file mode 100644 index 0000000000000000000000000000000000000000..c8260b252625f3d72409993426a60706ad9026f0 --- /dev/null +++ b/Grinder/pipeline/Connection.h @@ -0,0 +1,56 @@ +/****************************************************************************** + * File: Connection.h + * Date: 15.1.2018 + *****************************************************************************/ + +#ifndef CONNECTION_H +#define CONNECTION_H + +#include "PipelineItem.h" + +namespace grndr +{ + class Port; + class Block; + + class Connection : public PipelineItem + { + Q_OBJECT + + public: + static const char* Serialization_Value_Source; + static const char* Serialization_Value_Dest; + + public: + Connection(Port* src, Port* dest); + + public: + void initConnection(); + + public: + Block* source() { return _source(); } + const Block* source() const { return _source(); } + Port* sourcePort() { return _sourcePort; } + const Port* sourcePort() const { return _sourcePort; } + + Block* dest() { return _dest(); } + const Block* dest() const { return _dest(); } + Port* destPort() { return _destPort; } + const Port* destPort() const { return _destPort; } + + public: + virtual void serialize(SerializationContext& ctx) const override; + virtual void deserialize(DeserializationContext& ctx) override; + + private: + Block* _source() const; + Block* _dest() const; + + private: + Port* _sourcePort{nullptr}; + Port* _destPort{nullptr}; + }; + +} + +#endif diff --git a/Grinder/pipeline/ConnectionVector.cpp b/Grinder/pipeline/ConnectionVector.cpp new file mode 100644 index 0000000000000000000000000000000000000000..58111b636218cd0dab31a1fbd88758e13b2859f3 --- /dev/null +++ b/Grinder/pipeline/ConnectionVector.cpp @@ -0,0 +1,22 @@ +/****************************************************************************** + * File: ConnectionVector.cpp + * Date: 15.1.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ConnectionVector.h" + +const char* ConnectionVector::Serialization_Group = "Connections"; +const char* ConnectionVector::Serialization_Element = "Connection"; + +ConnectionVector::ConnectionVector(const Port* srcPort) : + _sourcePort{srcPort} +{ + if (!srcPort) + throw std::invalid_argument{_EXCPT("srcPort may not be null")}; +} + +ConnectionVector::pointer_type ConnectionVector::selectByDestination(const Port* dest) const +{ + return selectFirst([dest](auto con) { return con->destPort() == dest; }); +} diff --git a/Grinder/pipeline/ConnectionVector.h b/Grinder/pipeline/ConnectionVector.h new file mode 100644 index 0000000000000000000000000000000000000000..61576490e4dca42ff9d92af5cc5a4ae39a1f352d --- /dev/null +++ b/Grinder/pipeline/ConnectionVector.h @@ -0,0 +1,33 @@ +/****************************************************************************** + * File: ConnectionVector.h + * Date: 15.1.2018 + *****************************************************************************/ + +#ifndef CONNECTIONVECTOR_H +#define CONNECTIONVECTOR_H + +#include "common/ObjectVector.h" +#include "Connection.h" + +namespace grndr +{ + class Port; + + class ConnectionVector : public ObjectVector<Connection> + { + public: + static const char* Serialization_Group; + static const char* Serialization_Element; + + public: + ConnectionVector(const Port* srcPort); + + public: + pointer_type selectByDestination(const Port* dest) const; + + private: + const Port* _sourcePort{nullptr}; + }; +} + +#endif diff --git a/Grinder/pipeline/Pipeline.cpp b/Grinder/pipeline/Pipeline.cpp new file mode 100644 index 0000000000000000000000000000000000000000..d5cccae00419114fbf75ad561abfec1b5074304a --- /dev/null +++ b/Grinder/pipeline/Pipeline.cpp @@ -0,0 +1,124 @@ +/****************************************************************************** + * File: Pipeline.cpp + * Date: 15.1.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "Pipeline.h" +#include "PipelineManager.h" +#include "Block.h" +#include "BlockCatalog.h" +#include "PipelineExceptions.h" +#include "core/GrinderApplication.h" +#include "util/SerializationUtils.h" + +const char* Pipeline::Serialization_Value_Name = "Name"; + +Pipeline::Pipeline(QString name) : + _name{name}, _blocks{this} +{ + +} + +void Pipeline::initPipeline() +{ + +} + +std::shared_ptr<Block> Pipeline::createBlock(BlockType type, QString name) +{ + if (type == BlockType::Undefined) + throw std::invalid_argument(_EXCPT("type may not be BlockType::Undefined")); + + // Create new block using the block factory; cast the unique ptr to a shared one as well + std::shared_ptr<Block> block = BlockCatalog::createBlock(this, type, name); + + try { // Propagate initialization errors to the caller + block->initBlock(); + } + catch (...) { + throw; + } + + _blocks.push_back(block); + emit blockCreated(block); + + return block; +} + +void Pipeline::removeBlock(const Block* block) +{ + if (block) + { + auto it = _blocks.find(block); + + if (it != _blocks.cend()) + { + // Keep a copy of the shared_ptr holding the block to increase its use count; + // otherwise, the block will be deleted before it has been removed from the vector, potentially causing a crash + auto block = *it; + + // Remove all connections to or from this block + for (auto port : block->ports()) + port->disconnectFromAll(); + + emit blockRemoved(*it); + _blocks.erase(it); + } + else + throw PipelineException{this, _EXCPT("Tried to remove a block not belonging to this pipeline")}; + } +} + +void Pipeline::serialize(SerializationContext& ctx) const +{ + // Serialize values + ctx.settings()[Serialization_Value_Name] = _name; + + // Serialize all blocks + ctx.beginGroup(BlockVector::Serialization_Group, true); + _blocks.serialize(BlockVector::Serialization_Element, ctx); + ctx.endGroup(); + + // Serialize all connections + ctx.beginGroup(ConnectionVector::Serialization_Group, true); + SerializationUtils::serializeContainer(ctx.connections(), ConnectionVector::Serialization_Element, ctx); + ctx.endGroup(); +} + +void Pipeline::deserialize(DeserializationContext& ctx) +{ + // Deserialize values + _name = ctx.settings()[Serialization_Value_Name].toString(); + + // Deserialize all blocks + if (ctx.beginGroup(BlockVector::Serialization_Group)) + { + _blocks.deserialize(BlockVector::Serialization_Element, ctx, [this](const SettingsContainer& settings) { + BlockType type = settings[Block::Serialization_Value_Type].toString(); + QString name = settings[Block::Serialization_Value_Name].toString(); + + return createBlock(type, name); + }); + + ctx.endGroup(); + } + + // Deserialize all connections + if (ctx.beginGroup(ConnectionVector::Serialization_Group)) + { + SerializationUtils::deserializeContainer<ConnectionVector>(ConnectionVector::Serialization_Element, ctx, [&ctx](const SettingsContainer& settings) -> std::shared_ptr<Connection> { + int sourceIndex = settings[Connection::Serialization_Value_Source].toInt(); + int destIndex = settings[Connection::Serialization_Value_Dest].toInt(); + auto sourcePort = ctx.getPort(sourceIndex); + auto destPort = ctx.getPort(destIndex); + + if (sourcePort && destPort) + return sourcePort->connectTo(destPort); + else + return nullptr; + }); + + ctx.endGroup(); + } +} diff --git a/Grinder/pipeline/Pipeline.h b/Grinder/pipeline/Pipeline.h new file mode 100644 index 0000000000000000000000000000000000000000..0a3e622e8f54517a58174c58e293640c1f264340 --- /dev/null +++ b/Grinder/pipeline/Pipeline.h @@ -0,0 +1,57 @@ +/****************************************************************************** + * File: Pipeline.h + * Date: 15.1.2018 + *****************************************************************************/ + +#ifndef PIPELINE_H +#define PIPELINE_H + +#include "BlockVector.h" + +namespace grndr +{ + class PipelineManager; + + class Pipeline : public QObject + { + Q_OBJECT + + public: + static const char* Serialization_Value_Name; + + public: + Pipeline(QString name = ""); + + public: + void initPipeline(); + + public: + std::shared_ptr<Block> createBlock(BlockType type, QString name = ""); + void removeBlock(const Block* block); + + public: + QString getName() const { return _name; } + void setName(QString name) { if (name != _name) { _name = name; emit pipelineRenamed(); } } + + const BlockVector& blocks() const { return _blocks; } + + public: + void serialize(SerializationContext& ctx) const; + void deserialize(DeserializationContext& ctx); + + signals: + void pipelineRenamed(); + + void blockCreated(const std::shared_ptr<Block>&); + void blockRemoved(const std::shared_ptr<Block>&); + void connectionCreated(const std::shared_ptr<Connection>&); + void connectionRemoved(const std::shared_ptr<Connection>&); + + private: + QString _name{""}; + + BlockVector _blocks; + }; +} + +#endif diff --git a/Grinder/pipeline/PipelineExceptions.cpp b/Grinder/pipeline/PipelineExceptions.cpp new file mode 100644 index 0000000000000000000000000000000000000000..5d9cd030612030c5df62187adf7b3d00911ea801 --- /dev/null +++ b/Grinder/pipeline/PipelineExceptions.cpp @@ -0,0 +1,33 @@ +/****************************************************************************** + * File: PipelineExceptions.cpp + * Date: 15.1.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "PipelineExceptions.h" +#include "PipelineItem.h" +#include "Block.h" + +PipelineException::PipelineException(const Pipeline* pipeline, QString what) : GrinderException(what), + _pipeline{pipeline} +{ + +} + +PipelineItemException::PipelineItemException(const PipelineItem* item, QString what) : PipelineException(item ? item->pipeline() : nullptr, what), + _pipelineItem{item} +{ + +} + +BlockException::BlockException(const Block* block, QString what) : PipelineItemException(block, what), + _block{block} +{ + +} + +PortException::PortException(const Port* port, QString what) : BlockException(port->block(), what), + _port{port} +{ + +} diff --git a/Grinder/pipeline/PipelineExceptions.h b/Grinder/pipeline/PipelineExceptions.h new file mode 100644 index 0000000000000000000000000000000000000000..e6faa0e529910153ea03ad7ec0b6453cc7ac6d25 --- /dev/null +++ b/Grinder/pipeline/PipelineExceptions.h @@ -0,0 +1,69 @@ +/****************************************************************************** + * File: PipelineExceptions.h + * Date: 15.1.2018 + *****************************************************************************/ + +#ifndef PIPELINEEXCEPTIONS_H +#define PIPELINEEXCEPTIONS_H + +#include <QString> + +#include "core/GrinderExceptions.h" + +namespace grndr +{ + class Pipeline; + class PipelineItem; + class Block; + class Port; + + class PipelineException : public GrinderException + { + public: + PipelineException(const Pipeline* pipeline, QString what); + + public: + const Pipeline* pipeline() const { return _pipeline; } + + protected: + const Pipeline* _pipeline{nullptr}; + }; + + class PipelineItemException : public PipelineException + { + public: + PipelineItemException(const PipelineItem* item, QString what); + + public: + const PipelineItem* pipelineItem() const { return _pipelineItem; } + + protected: + const PipelineItem* _pipelineItem{nullptr}; + }; + + class BlockException : public PipelineItemException + { + public: + BlockException(const Block* block, QString what); + + public: + const Block* block() const { return _block; } + + protected: + const Block* _block{nullptr}; + }; + + class PortException : public BlockException + { + public: + PortException(const Port* port, QString what); + + public: + const Port* port() const { return _port; } + + protected: + const Port* _port{nullptr}; + }; +} + +#endif diff --git a/Grinder/pipeline/PipelineItem.cpp b/Grinder/pipeline/PipelineItem.cpp new file mode 100644 index 0000000000000000000000000000000000000000..21c8b9295b79557287d8b5976d51835df621bcbd --- /dev/null +++ b/Grinder/pipeline/PipelineItem.cpp @@ -0,0 +1,38 @@ +/****************************************************************************** + * File: PipelineItem.cpp + * Date: 13.1.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "PipelineItem.h" +#include "PipelineExceptions.h" + +const char* PipelineItem::Serialization_Value_Name = "Name"; + +PipelineItem::PipelineItem(Pipeline* pipeline, QString name) : + _pipeline{pipeline}, _name{name} +{ + if (!pipeline) + throw std::invalid_argument{_EXCPT("pipeline may not be null")}; +} + +void PipelineItem::initPipelineItem() +{ + createProperties(); +} + +void PipelineItem::serialize(SerializationContext& ctx) const +{ + PropertyObject::serialize(ctx); + + // Serialize values + ctx.settings()[Serialization_Value_Name] = _name; +} + +void PipelineItem::deserialize(DeserializationContext& ctx) +{ + PropertyObject::deserialize(ctx); + + // Deserialize values + _name = ctx.settings()[Serialization_Value_Name].toString(); +} diff --git a/Grinder/pipeline/PipelineItem.h b/Grinder/pipeline/PipelineItem.h new file mode 100644 index 0000000000000000000000000000000000000000..61e1bff1c5a922926c06d1900b4d85a4870d9235 --- /dev/null +++ b/Grinder/pipeline/PipelineItem.h @@ -0,0 +1,49 @@ +/****************************************************************************** + * File: PipelineItem.h + * Date: 13.1.2018 + *****************************************************************************/ + +#ifndef PIPELINEITEM_H +#define PIPELINEITEM_H + +#include "common/PropertyObject.h" + +namespace grndr +{ + class Pipeline; + + class PipelineItem : public PropertyObject + { + Q_OBJECT + + public: + static const char* Serialization_Value_Name; + + public: + PipelineItem(Pipeline* pipeline, QString name = ""); + + public: + void initPipelineItem(); + + public: + const Pipeline* pipeline() const { return _pipeline; } + Pipeline* pipeline() { return _pipeline; } + + QString getName() const { return _name; } + void setName(QString name) { if (_name != name) { _name = name; emit itemRenamed(); } } + + public: + virtual void serialize(SerializationContext& ctx) const; + virtual void deserialize(DeserializationContext& ctx); + + signals: + void itemRenamed(); + + protected: + Pipeline* _pipeline{nullptr}; + + QString _name{""}; + }; +} + +#endif diff --git a/Grinder/pipeline/PipelineManager.cpp b/Grinder/pipeline/PipelineManager.cpp new file mode 100644 index 0000000000000000000000000000000000000000..71fc08b76f1909d21d681d83cbc741c3ca244aa3 --- /dev/null +++ b/Grinder/pipeline/PipelineManager.cpp @@ -0,0 +1,64 @@ +/****************************************************************************** + * File: PipelineManager.cpp + * Date: 16.1.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "PipelineManager.h" +#include "PipelineExceptions.h" + +std::shared_ptr<Pipeline> PipelineManager::createPipeline(QString name) +{ + if (name.isEmpty()) + throw std::invalid_argument{_EXCPT("name may not be empty")}; + + if (_pipelines.selectByName(name)) + throw PipelineException{nullptr, _EXCPT(QString{"A pipeline with name '%1' already exists"}.arg(name))}; + + auto pipeline = std::make_shared<Pipeline>(name); + + try { // Propage initialization errors to the caller + pipeline->initPipeline(); + } + catch (...) { + throw; + } + + _pipelines.push_back(pipeline); + emit pipelineCreated(pipeline); + + return pipeline; +} + +void PipelineManager::removePipeline(QString name) +{ + if (!name.isEmpty()) + { + auto pipeline = _pipelines.selectByName(name); + + if (pipeline) + removePipeline(pipeline.get()); + else + throw PipelineException{nullptr, _EXCPT(QString{"Tried to remove a non-existing pipeline (Name: %1)"}.arg(name))}; + } +} + +void PipelineManager::removePipeline(const Pipeline* pipeline) +{ + if (pipeline) + { + auto it = _pipelines.find(pipeline); + + if (it != _pipelines.cend()) + { + // Keep a copy of the shared_ptr holding the pipeline to increase its use count; + // otherwise, the pipeline will be deleted before it has been removed from the vector, potentially causing a crash + auto pipeline = *it; + + emit pipelineRemoved(*it); + _pipelines.erase(it); + } + else + throw PipelineException{pipeline, _EXCPT("Tried to remove a pipeline not currently managed")}; + } +} diff --git a/Grinder/pipeline/PipelineManager.h b/Grinder/pipeline/PipelineManager.h new file mode 100644 index 0000000000000000000000000000000000000000..ea3c0212c5b107219fb812194cd326b1f28e120c --- /dev/null +++ b/Grinder/pipeline/PipelineManager.h @@ -0,0 +1,36 @@ +/****************************************************************************** + * File: PipelineManager.h + * Date: 16.1.2018 + *****************************************************************************/ + +#ifndef PIPELINEMANAGER_H +#define PIPELINEMANAGER_H + +#include <QObject> + +#include "PipelineVector.h" + +namespace grndr +{ + class PipelineManager : public QObject + { + Q_OBJECT + + public: + std::shared_ptr<Pipeline> createPipeline(QString name); + void removePipeline(QString name); + void removePipeline(const Pipeline* pipeline); + + public: + const PipelineVector& pipelines() const { return _pipelines; } + + signals: + void pipelineCreated(const std::shared_ptr<Pipeline>&); + void pipelineRemoved(const std::shared_ptr<Pipeline>&); + + private: + PipelineVector _pipelines; + }; +} + +#endif diff --git a/Grinder/pipeline/PipelineVector.cpp b/Grinder/pipeline/PipelineVector.cpp new file mode 100644 index 0000000000000000000000000000000000000000..cf5ec814e57b987dce70ea4ba7d3f7ea04d1693b --- /dev/null +++ b/Grinder/pipeline/PipelineVector.cpp @@ -0,0 +1,15 @@ +/****************************************************************************** + * File: PipelineVector.cpp + * Date: 16.1.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "PipelineVector.h" + +const char* PipelineVector::Serialization_Group = "Pipelines"; +const char* PipelineVector::Serialization_Element = "Pipeline"; + +PipelineVector::pointer_type PipelineVector::selectByName(QString name, bool caseSensitive) const +{ + return selectFirst([name, caseSensitive](auto pipeline) { return pipeline->getName().compare(name, caseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive) == 0; }); +} diff --git a/Grinder/pipeline/PipelineVector.h b/Grinder/pipeline/PipelineVector.h new file mode 100644 index 0000000000000000000000000000000000000000..716c8813451dd48d62185518e6dc33d3f0fd246c --- /dev/null +++ b/Grinder/pipeline/PipelineVector.h @@ -0,0 +1,25 @@ +/****************************************************************************** + * File: PipelineVector.h + * Date: 16.1.2018 + *****************************************************************************/ + +#ifndef PIPELINEVECTOR_H +#define PIPELINEVECTOR_H + +#include "common/ObjectVector.h" +#include "Pipeline.h" + +namespace grndr +{ + class PipelineVector : public ObjectVector<Pipeline> + { + public: + static const char* Serialization_Group; + static const char* Serialization_Element; + + public: + pointer_type selectByName(QString name, bool caseSensitive = false) const; + }; +} + +#endif diff --git a/Grinder/pipeline/Port.cpp b/Grinder/pipeline/Port.cpp new file mode 100644 index 0000000000000000000000000000000000000000..8bd5c468a2fe4352598018f137c81442dbbdda29 --- /dev/null +++ b/Grinder/pipeline/Port.cpp @@ -0,0 +1,233 @@ +/****************************************************************************** + * File: Port.cpp + * Date: 15.1.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "Port.h" +#include "Block.h" +#include "Pipeline.h" +#include "PipelineExceptions.h" + +const char* Port::Serialization_Value_Type = "Type"; +const char* Port::Serialization_Value_Index = "Index"; + +Port::Port(Block* parent, PortType type, Direction dir, const DataDescriptors& dataDescriptors, QString name) : PipelineItem(parent->pipeline(), name), + _block{parent}, _type{type}, _direction{dir}, _dataDescriptors{dataDescriptors}, _connections{this} +{ + // Verify the given data descriptors + if (_direction == Direction::Out && dataDescriptors.size() > 1) + throw std::invalid_argument{_EXCPT("Out-ports may only have one data descriptor")}; + + for (const auto& dataDesc : dataDescriptors) + { + if (_direction == Direction::Out && dataDesc.isArbitrary()) + throw std::invalid_argument{_EXCPT("Out-ports may not have arbitrary data descriptors")}; + else if (_direction == Direction::In && dataDesc.isAdaptive()) + throw std::invalid_argument{_EXCPT("In-ports may not have adaptive data descriptors")}; + } +} + +void Port::initPort() +{ + PipelineItem::initPipelineItem(); +} + +void Port::verifyConnection(const Port* dest) const +{ + if (!dest) + throw std::invalid_argument{_EXCPT("dest may not be null")}; + + // Perform sanity checks: (1) Do not connect to self; (2) Port directions must match; (3) Connection may not already exist + if (dest == this) + throw PortException{this, _EXCPT("Cannot connect a port to itself")}; + + if (!isOut() || !dest->isIn()) + throw PortException{this, _EXCPT("The participating ports do not accept the respective connection directions")}; + + if (_connections.selectByDestination(dest)) + throw PortException{this, _EXCPT("The connection already exists")}; + + // An in-port may only have one connection at a time + if (dest->isConnected()) + throw PortException{this, _EXCPT("The incoming port is already connected")}; + + // Check if the data types of this port and the destination are compatible + bool canConvertData = false; + + for (const auto& dataDescFrom : _dataDescriptors) + { + for (const auto& dataDescTo : dest->dataDescriptors()) + { + if (dataDescFrom.canConvertTo(dataDescTo)) + { + canConvertData = true; + break; + } + } + } + + if (!canConvertData) + throw PortException{this, _EXCPT("The data types of the ports are not compatible")}; + + // Ask the block if the connection is valid + try { + _block->verifyConnection(this, dest); // Throws a BlockException if the connection can't be accepted + } catch(BlockException& e) { + throw PortException{this, e.what()}; + } +} + +std::shared_ptr<Connection> Port::connectTo(Port* dest) +{ + // Check whether the connection can be established (throws if it can't) + verifyConnection(dest); + + auto connection = std::make_shared<Connection>(this, dest); + + try { // Propage initialization errors to the caller + connection->initConnection(); + } + catch (...) { + throw; + } + + _connections.push_back(connection); + emit _pipeline->connectionCreated(connection); + + return connection; +} + +void Port::disconnectFrom(const Port* dest) +{ + if (!dest) + throw std::invalid_argument{_EXCPT("dest may not be null")}; + + auto connection = _connections.selectByDestination(dest); + + if (connection) + removeConnection(connection.get()); + else + throw PortException{this, _EXCPT("Tried to remove a non-existing connection")}; +} + +void Port::disconnectFromAll() +{ + try { + // Remove outgoing connections + auto connections = _connections; // Need a copy since disconnectFrom will modify _connections + + for (const auto& connection : connections) + disconnectFrom(connection->destPort()); + + // Remove incoming connections + for (auto& connection : getConnections(Direction::In)) + connection->sourcePort()->disconnectFrom(this); + } catch (PipelineException&) { + // Ignore any pipeline exceptions during disconnection (shouldn't happen anyway) + } +} + +void Port::removeConnection(const Connection* connection) +{ + if (!connection) + throw std::invalid_argument{_EXCPT("connection may not be null")}; + + auto it = _connections.find(connection); + + if (it != _connections.cend()) + { + // Keep a copy of the shared_ptr holding the connection to increase its use count; + // otherwise, the connection will be deleted before it has been removed from the vector, potentially causing a crash + auto connection = *it; + + emit _pipeline->connectionRemoved(*it); + _connections.erase(it); + } + else + throw PortException{this, _EXCPT("Tried to remove a connection not belonging to this port")}; +} + +QString Port::getFormattedName() const +{ + return QString{"%1::%2"}.arg(_block->getName()).arg(_name); +} + +bool Port::isConnected() const +{ + if (isOut()) + return !_connections.empty(); + else if (isIn()) + return !getConnections(Direction::In).empty(); + else + return false; +} + +ConnectionVector::pointer_vec_type Port::getConnections(Direction dir, BlockType blockType) const +{ + if (dir == Direction::Dry) + throw std::invalid_argument{_EXCPT("dir may not be Direction::Dry")}; + + ConnectionVector::pointer_vec_type connections; + + if (dir == Direction::Out) // Assemble outgoing connections + { + ConnectionVector::pointer_vec_type cons; + + if (blockType != BlockType::Undefined) // Only add connections which target to a block of type blockType + cons = _connections.select([blockType](auto con) { return con->dest() && con->dest()->getType() == blockType; }); + else + cons = _connections; + + connections.insert(connections.cend(), cons.cbegin(), cons.cend()); + } + else if (dir == Direction::In) // Assemble incoming connections: Check all blocks of the pipeline for connections to us + { + for (const auto& block : _block->pipeline()->blocks()) + { + if (block.get() == _block) + continue; + + for (const auto& port : block->ports()) + { + for (const auto& con : port->connections()) + { + if (con->destPort() == this) // Connection targets to us, add it + { + if (blockType != BlockType::Undefined) // Only add connections which stem from a block of type blockType + { + if (con->source() && con->source()->getType() == blockType) + connections.push_back(con); + } + else + connections.push_back(con); + } + } + } + } + } + + return connections; +} + +void Port::serialize(SerializationContext& ctx) const +{ + PipelineItem::serialize(ctx); + + // Serialize values + ctx.settings()[Serialization_Value_Type] = _type; + ctx.settings()[Serialization_Value_Index] = ctx.addPort(this); + + // Save all connections for later serialization + for (const auto& connection : _connections) + ctx.addConnection(connection.get()); +} + +void Port::deserialize(DeserializationContext& ctx) +{ + PipelineItem::deserialize(ctx); + + // Deserialize values + _type = ctx.settings()[Serialization_Value_Type].toString(); + ctx.addPort(ctx.settings()[Serialization_Value_Index].toInt(), this); +} diff --git a/Grinder/pipeline/Port.h b/Grinder/pipeline/Port.h new file mode 100644 index 0000000000000000000000000000000000000000..0cf99f583f96f7c8d1a17796ef3e7e992a7e273c --- /dev/null +++ b/Grinder/pipeline/Port.h @@ -0,0 +1,79 @@ +/****************************************************************************** + * File: Port.h + * Date: 15.1.2018 + *****************************************************************************/ + +#ifndef PORT_H +#define PORT_H + +#include "PipelineItem.h" +#include "PortType.h" +#include "ConnectionVector.h" +#include "BlockType.h" +#include "engine/data/DataDescriptor.h" + +namespace grndr +{ + class Block; + + class Port : public PipelineItem + { + Q_OBJECT + + public: + static const char* Serialization_Value_Type; + static const char* Serialization_Value_Index; + + public: + enum class Direction + { + Dry, /* Usually indicates an invalid port */ + In, + Out, + }; + + public: + Port(Block* parent, PortType type, Direction dir, const DataDescriptors& dataDescriptors, QString name = ""); + + public: + void initPort(); + + void verifyConnection(const Port* dest) const; + std::shared_ptr<Connection> connectTo(Port* dest); + void disconnectFrom(const Port* dest); + void disconnectFromAll(); + void removeConnection(const Connection* connection); + + public: + Block* block() { return _block; } + const Block* block() const { return _block; } + + QString getFormattedName() const; + PortType getType() const { return _type; } + Direction getDirection() const { return _direction; } + void setDirection(Direction dir) { _direction = dir; } + bool isIn() const { return _direction == Direction::In; } + bool isOut() const { return _direction == Direction::Out; } + bool isConnected() const; + + const DataDescriptors& dataDescriptors() const { return _dataDescriptors; } + + const ConnectionVector& connections() const { return _connections; } + ConnectionVector::pointer_vec_type getConnections(Direction dir, BlockType blockType = BlockType::Undefined) const; + + public: + virtual void serialize(SerializationContext& ctx) const override; + virtual void deserialize(DeserializationContext& ctx) override; + + private: + Block* _block{nullptr}; + + PortType _type{PortType::Undefined}; + Direction _direction{Direction::Dry}; + DataDescriptors _dataDescriptors; + + ConnectionVector _connections; + }; +} + +#endif diff --git a/Grinder/pipeline/PortType.cpp b/Grinder/pipeline/PortType.cpp new file mode 100644 index 0000000000000000000000000000000000000000..71166da2c26cc2e38dc1eb0f9d1678ddbff64692 --- /dev/null +++ b/Grinder/pipeline/PortType.cpp @@ -0,0 +1,15 @@ +/****************************************************************************** + * File: PortType.cpp + * Date: 15.1.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "PortType.h" + +const char* PortType::Undefined = ""; + +const char* PortType::GenericIn = "In"; +const char* PortType::GenericOut = "Out"; + +const char* PortType::ImageIn = "ImageIn"; +const char* PortType::ImageOut = "ImageOut"; diff --git a/Grinder/pipeline/PortType.h b/Grinder/pipeline/PortType.h new file mode 100644 index 0000000000000000000000000000000000000000..fd86706dae338112287de8c86eb40fd1f8013320 --- /dev/null +++ b/Grinder/pipeline/PortType.h @@ -0,0 +1,38 @@ +/****************************************************************************** + * File: PortType.h + * Date: 15.1.2018 + *****************************************************************************/ + +#ifndef PORTTYPE_H +#define PORTTYPE_H + +#include <QString> + +namespace grndr +{ + class PortType final : public QString + { + public: + static const char* Undefined; /* Invalid port */ + + static const char* GenericIn; + static const char* GenericOut; + + static const char* ImageIn; + static const char* ImageOut; + + public: + using QString::QString; + + PortType() = default; + PortType(const PortType& other) = default; + PortType(PortType&& other) = default; + PortType(const QString& str) { *static_cast<QString*>(this) = str; } + + PortType& operator =(const PortType& other) = default; + PortType& operator =(PortType&& other) = default; + PortType& operator =(const QString& str) { *static_cast<QString*>(this) = str; return *this; } + }; +} + +#endif diff --git a/Grinder/pipeline/PortVector.cpp b/Grinder/pipeline/PortVector.cpp new file mode 100644 index 0000000000000000000000000000000000000000..ee4b2cab3b3e7ce1adec7267738be22ddc48e970 --- /dev/null +++ b/Grinder/pipeline/PortVector.cpp @@ -0,0 +1,27 @@ +/****************************************************************************** + * File: PortVector.cpp + * Date: 15.1.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "PortVector.h" + +const char* PortVector::Serialization_Group = "Ports"; +const char* PortVector::Serialization_Element = "Port"; + +PortVector::PortVector(const Block* parent) : + _block{parent} +{ + if (!parent) + throw std::invalid_argument{_EXCPT("parent may not be null")}; +} + +PortVector::pointer_type PortVector::selectByType(PortType type) const +{ + return selectFirst([type](auto port) { return port->getType() == type; }); +} + +PortVector::pointer_vec_type PortVector::selectByDirection(Port::Direction dir) const +{ + return select([dir](auto port) { return port->getDirection() == dir; }); +} diff --git a/Grinder/pipeline/PortVector.h b/Grinder/pipeline/PortVector.h new file mode 100644 index 0000000000000000000000000000000000000000..281a49cbcd331176dcf05ea98f5c8ff25d0cfffc --- /dev/null +++ b/Grinder/pipeline/PortVector.h @@ -0,0 +1,35 @@ +/****************************************************************************** + * File: PortVector.h + * Date: 15.1.2018 + *****************************************************************************/ + +#ifndef PORTVECTOR_H +#define PORTVECTOR_H + +#include "common/ObjectVector.h" +#include "Port.h" + +namespace grndr +{ + class Block; + + class PortVector : public ObjectVector<Port> + { + public: + static const char* Serialization_Group; + static const char* Serialization_Element; + + public: + PortVector(const Block* parent); + + public: + pointer_type selectByType(PortType type) const; + pointer_vec_type selectByDirection(Port::Direction dir) const; + + private: + const Block* _block{nullptr}; + }; + +} + +#endif diff --git a/Grinder/pipeline/blocks/BinaryThresholdBlock.cpp b/Grinder/pipeline/blocks/BinaryThresholdBlock.cpp new file mode 100644 index 0000000000000000000000000000000000000000..073a3c35357cb7203127f0112dd38e1f8c70f166 --- /dev/null +++ b/Grinder/pipeline/blocks/BinaryThresholdBlock.cpp @@ -0,0 +1,45 @@ +/****************************************************************************** + * File: BinaryThresholdBlock.cpp + * Date: 20.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "BinaryThresholdBlock.h" +#include "common/properties/RangeConstraint.h" +#include "engine/processors/BinaryThresholdProcessor.h" + +const BlockType BinaryThresholdBlock::type_value = BlockType::BinaryThreshold; +const BlockCategory BinaryThresholdBlock::category_value = BlockCategory::Thresholding; + +BinaryThresholdBlock::BinaryThresholdBlock(Pipeline* pipeline, QString name) : Block(pipeline, type_value, category_value, name) +{ + +} + +std::unique_ptr<ProcessorBase> BinaryThresholdBlock::createProcessor() const +{ + return std::make_unique<BinaryThresholdProcessor>(this); +} + +void BinaryThresholdBlock::createProperties() +{ + _threshold = createProperty<UIntProperty>(PropertyID::Threshold, "Threshold", 128); + threshold()->createConstraint<RangeConstraint>(0, 255); + threshold()->setDescription("The threshold value to use; ranges from 0 to 255."); + + _targetValue = createProperty<UIntProperty>(PropertyID::TargetValue, "Target value", 255); + targetValue()->createConstraint<RangeConstraint>(0, 255); + targetValue()->setDescription("The value to use if a pixel is above (below if inversed) the threshold value; ranges from 0 to 255."); + + _invert = createProperty<BoolProperty>(PropertyID::Invert, "Invert", false); + invert()->setDescription("Invert the thresholding (i.e., discard a pixel if it is <em>above</em> the threshold value)."); +} + +void BinaryThresholdBlock::createPorts() +{ + DataDescriptors inPortDataDescs = {DataDescriptor::imageDescriptor(true, DataDescriptor::ValueType::Any), DataDescriptor::imageDescriptor(false, DataDescriptor::ValueType::Any)}; + _inPort = createPort(PortType::ImageIn, Port::Direction::In, inPortDataDescs, "In"); + + DataDescriptors outPortDataDescs = {DataDescriptor::adaptiveOutputDescriptor("Image")}; + _outPort = createPort(PortType::ImageOut, Port::Direction::Out, outPortDataDescs, "Out"); +} diff --git a/Grinder/pipeline/blocks/BinaryThresholdBlock.h b/Grinder/pipeline/blocks/BinaryThresholdBlock.h new file mode 100644 index 0000000000000000000000000000000000000000..22603e81db297747babee3dffd378cbae2dc75ee --- /dev/null +++ b/Grinder/pipeline/blocks/BinaryThresholdBlock.h @@ -0,0 +1,54 @@ +/****************************************************************************** + * File: BinaryThresholdBlock.h + * Date: 20.2.2018 + *****************************************************************************/ + +#ifndef BINARYTHRESHOLDBLOCK_H +#define BINARYTHRESHOLDBLOCK_H + +#include "pipeline/Block.h" + +namespace grndr +{ + class BinaryThresholdBlock : public Block + { + Q_OBJECT + + public: + static const BlockType type_value; + static const BlockCategory category_value; + + public: + BinaryThresholdBlock(Pipeline* pipeline, QString name = ""); + + public: + virtual std::unique_ptr<ProcessorBase> createProcessor() const override; + + public: + auto threshold() { return dynamic_cast<UIntProperty*>(_threshold.get()); } + auto threshold() const { return dynamic_cast<const UIntProperty*>(_threshold.get()); } + auto targetValue() { return dynamic_cast<UIntProperty*>(_targetValue.get()); } + auto targetValue() const { return dynamic_cast<const UIntProperty*>(_targetValue.get()); } + auto invert() { return dynamic_cast<BoolProperty*>(_invert.get()); } + auto invert() const { return dynamic_cast<const BoolProperty*>(_invert.get()); } + + Port* inPort() { return _inPort.get(); } + const Port* inPort() const { return _inPort.get(); } + Port* outPort() { return _outPort.get(); } + const Port* outPort() const { return _outPort.get(); } + + protected: + virtual void createProperties() override; + virtual void createPorts() override; + + private: + std::shared_ptr<PropertyBase> _threshold; + std::shared_ptr<PropertyBase> _targetValue; + std::shared_ptr<PropertyBase> _invert; + + std::shared_ptr<Port> _inPort; + std::shared_ptr<Port> _outPort; + }; +} + +#endif diff --git a/Grinder/pipeline/blocks/ConvertToGrayscaleBlock.cpp b/Grinder/pipeline/blocks/ConvertToGrayscaleBlock.cpp new file mode 100644 index 0000000000000000000000000000000000000000..e9e69afcc2c093cbdcbd86bb23ff624a4ef65220 --- /dev/null +++ b/Grinder/pipeline/blocks/ConvertToGrayscaleBlock.cpp @@ -0,0 +1,30 @@ +/****************************************************************************** + * File: ConvertToGrayscaleBlock.cpp + * Date: 22.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ConvertToGrayscaleBlock.h" +#include "engine/processors/ConvertToGrayscaleProcessor.h" + +const BlockType ConvertToGrayscaleBlock::type_value = BlockType::ConvertToGrayscale; +const BlockCategory ConvertToGrayscaleBlock::category_value = BlockCategory::Conversion; + +ConvertToGrayscaleBlock::ConvertToGrayscaleBlock(Pipeline* pipeline, QString name) : Block(pipeline, type_value, category_value, name) +{ + +} + +std::unique_ptr<ProcessorBase> ConvertToGrayscaleBlock::createProcessor() const +{ + return std::make_unique<ConvertToGrayscaleProcessor>(this); +} + +void ConvertToGrayscaleBlock::createPorts() +{ + DataDescriptors inPortDataDescs = {DataDescriptor::imageDescriptor(true, DataDescriptor::ValueType::Any), DataDescriptor::imageDescriptor(false, DataDescriptor::ValueType::Any)}; + _inPort = createPort(PortType::ImageIn, Port::Direction::In, inPortDataDescs, "In"); + + DataDescriptors outPortDataDescs = {DataDescriptor::imageDescriptor(false, DataDescriptor::ValueType::Adaptive)}; + _outPort = createPort(PortType::ImageOut, Port::Direction::Out, outPortDataDescs, "Out"); +} diff --git a/Grinder/pipeline/blocks/ConvertToGrayscaleBlock.h b/Grinder/pipeline/blocks/ConvertToGrayscaleBlock.h new file mode 100644 index 0000000000000000000000000000000000000000..d9d5b2dee9b16e9392d684e15b7d5914f722493a --- /dev/null +++ b/Grinder/pipeline/blocks/ConvertToGrayscaleBlock.h @@ -0,0 +1,42 @@ +/****************************************************************************** + * File: ConvertToGrayscaleBlock.h + * Date: 22.2.2018 + *****************************************************************************/ + +#ifndef CONVERTTOGRAYSCALEBLOCK_H +#define CONVERTTOGRAYSCALEBLOCK_H + +#include "pipeline/Block.h" + +namespace grndr +{ + class ConvertToGrayscaleBlock : public Block + { + Q_OBJECT + + public: + static const BlockType type_value; + static const BlockCategory category_value; + + public: + ConvertToGrayscaleBlock(Pipeline* pipeline, QString name = ""); + + public: + virtual std::unique_ptr<ProcessorBase> createProcessor() const override; + + public: + Port* inPort() { return _inPort.get(); } + const Port* inPort() const { return _inPort.get(); } + Port* outPort() { return _outPort.get(); } + const Port* outPort() const { return _outPort.get(); } + + protected: + virtual void createPorts() override; + + private: + std::shared_ptr<Port> _inPort; + std::shared_ptr<Port> _outPort; + }; +} + +#endif diff --git a/Grinder/pipeline/blocks/InputBlock.cpp b/Grinder/pipeline/blocks/InputBlock.cpp new file mode 100644 index 0000000000000000000000000000000000000000..8a4b9a62fd4827a3ac20c6d077b6aea7eda8e3a8 --- /dev/null +++ b/Grinder/pipeline/blocks/InputBlock.cpp @@ -0,0 +1,26 @@ +/****************************************************************************** + * File: InputBlock.cpp + * Date: 03.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "InputBlock.h" +#include "engine/processors/InputProcessor.h" + +const BlockType InputBlock::type_value = BlockType::Input; +const BlockCategory InputBlock::category_value = BlockCategory::Input; + +InputBlock::InputBlock(Pipeline* pipeline, QString name) : Block(pipeline, type_value, category_value, name) +{ + +} + +std::unique_ptr<ProcessorBase> InputBlock::createProcessor() const +{ + return std::make_unique<InputProcessor>(this); +} + +void InputBlock::createPorts() +{ + _outPort = createPort(PortType::ImageOut, Port::Direction::Out, {DataDescriptor::imageDescriptor()}, "Out"); +} diff --git a/Grinder/pipeline/blocks/InputBlock.h b/Grinder/pipeline/blocks/InputBlock.h new file mode 100644 index 0000000000000000000000000000000000000000..0b408f8a037fcb87bf02dc406d911a2d2f1590d4 --- /dev/null +++ b/Grinder/pipeline/blocks/InputBlock.h @@ -0,0 +1,39 @@ +/****************************************************************************** + * File: InputBlock.h + * Date: 03.2.2018 + *****************************************************************************/ + +#ifndef INPUTBLOCK_H +#define INPUTBLOCK_H + +#include "pipeline/Block.h" + +namespace grndr +{ + class InputBlock : public Block + { + Q_OBJECT + + public: + static const BlockType type_value; + static const BlockCategory category_value; + + public: + InputBlock(Pipeline* pipeline, QString name = ""); + + public: + virtual std::unique_ptr<ProcessorBase> createProcessor() const override; + + public: + Port* outPort() { return _outPort.get(); } + const Port* outPort() const { return _outPort.get(); } + + protected: + virtual void createPorts() override; + + private: + std::shared_ptr<Port> _outPort; + }; +} + +#endif diff --git a/Grinder/pipeline/blocks/OutputBlock.cpp b/Grinder/pipeline/blocks/OutputBlock.cpp new file mode 100644 index 0000000000000000000000000000000000000000..9d12232af76fb415fd3a996a20216a843b9d907b --- /dev/null +++ b/Grinder/pipeline/blocks/OutputBlock.cpp @@ -0,0 +1,27 @@ +/****************************************************************************** + * File: OutputBlock.cpp + * Date: 15.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "OutputBlock.h" +#include "engine/processors/OutputProcessor.h" + +const BlockType OutputBlock::type_value = BlockType::Output; +const BlockCategory OutputBlock::category_value = BlockCategory::Output; + +OutputBlock::OutputBlock(Pipeline* pipeline, QString name) : Block(pipeline, type_value, category_value, name) +{ + +} + +std::unique_ptr<ProcessorBase> OutputBlock::createProcessor() const +{ + return std::make_unique<OutputProcessor>(this); +} + +void OutputBlock::createPorts() +{ + 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/pipeline/blocks/OutputBlock.h b/Grinder/pipeline/blocks/OutputBlock.h new file mode 100644 index 0000000000000000000000000000000000000000..7f7f13e6db1c9eea96cce3becbd022f17473295c --- /dev/null +++ b/Grinder/pipeline/blocks/OutputBlock.h @@ -0,0 +1,39 @@ +/****************************************************************************** + * File: OutputBlock.h + * Date: 15.2.2018 + *****************************************************************************/ + +#ifndef OUTPUTBLOCK_H +#define OUTPUTBLOCK_H + +#include "pipeline/Block.h" + +namespace grndr +{ + class OutputBlock : public Block + { + Q_OBJECT + + public: + static const BlockType type_value; + static const BlockCategory category_value; + + public: + OutputBlock(Pipeline* pipeline, QString name = ""); + + public: + virtual std::unique_ptr<ProcessorBase> createProcessor() const override; + + public: + Port* inPort() { return _inPort.get(); } + const Port* inPort() const { return _inPort.get(); } + + protected: + virtual void createPorts() override; + + private: + std::shared_ptr<Port> _inPort; + }; +} + +#endif diff --git a/Grinder/project/ImageReference.cpp b/Grinder/project/ImageReference.cpp new file mode 100644 index 0000000000000000000000000000000000000000..2d954dac7705986af270b88b3043553150803389 --- /dev/null +++ b/Grinder/project/ImageReference.cpp @@ -0,0 +1,90 @@ +/****************************************************************************** + * File: ImageReference.cpp + * Date: 10.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ImageReference.h" +#include "project/ProjectExceptions.h" + +#include <QCollator> +#include <opencv2/highgui.hpp> + +const char* ImageReference::Serialization_Value_File = "File"; +const char* ImageReference::Serialization_Value_Index = "Index"; + +ImageReference::ImageReference(Project* project, QString imageFilePath) : ProjectItem(project), + _imageFilePath{imageFilePath} +{ + +} + +cv::Mat ImageReference::loadImage() const +{ + if (_isValid) + return cv::imread(_imageFilePath.toStdString(), cv::IMREAD_COLOR); + else + throw ImageReferenceException{this, _EXCPT("The image is invalid")}; +} + +void ImageReference::refreshImageInfo() +{ + _isValid = false; + + // Get general file info + QFileInfo fileInfo{_imageFilePath}; + + if (fileInfo.exists()) + { + _imageInfo.fileSize = fileInfo.size(); + _imageInfo.fileDate = fileInfo.fileTime(QFile::FileModificationTime); + + // Try loading the image + auto image = cv::imread(_imageFilePath.toStdString()); + + if (!image.empty()) + _imageInfo.imageSize = QSize{image.cols, image.rows}; + else + throw ImageReferenceException{this, _EXCPT("Invalid image format")}; + } + else + throw ImageReferenceException{this, _EXCPT("Invalid image file path")}; + + _isValid = true; +} + +bool ImageReference::operator <(QString image) const +{ + QCollator collator; + collator.setNumericMode(true); + + return collator.compare(_imageFilePath, image) < 0; +} + +bool ImageReference::operator ==(QString image) const +{ + QFileInfo fileInfo1{_imageFilePath}; + QFileInfo fileInfo2{image}; + + return fileInfo1 == fileInfo2; +} + +QString ImageReference::getImageFileName() const +{ + QFileInfo fileInfo{_imageFilePath}; + return fileInfo.fileName(); +} + +void ImageReference::serialize(SerializationContext& ctx) const +{ + // Serialize values + ctx.settings()[Serialization_Value_File] = _imageFilePath; + ctx.settings()[Serialization_Value_Index] = ctx.addImageReference(this); +} + +void ImageReference::deserialize(DeserializationContext& ctx) +{ + // Deserialize values + _imageFilePath = ctx.settings()[Serialization_Value_File].toString(); + ctx.addImageReference(ctx.settings()[Serialization_Value_Index].toInt(), this); +} diff --git a/Grinder/project/ImageReference.h b/Grinder/project/ImageReference.h new file mode 100644 index 0000000000000000000000000000000000000000..6c0f575ad313e9f7ee4ffab56cd178411eb98654 --- /dev/null +++ b/Grinder/project/ImageReference.h @@ -0,0 +1,70 @@ +/****************************************************************************** + * File: ImageReference.h + * Date: 10.2.2018 + *****************************************************************************/ + +#ifndef IMAGEREFERENCE_H +#define IMAGEREFERENCE_H + +#include <QDateTime> +#include <QSize> +#include <opencv2/core.hpp> + +#include "project/ProjectItem.h" +#include "project/serialization/SerializationContext.h" +#include "project/serialization/DeserializationContext.h" + +namespace grndr +{ + class Project; + + class ImageReference : public ProjectItem + { + Q_OBJECT + + public: + static const char* Serialization_Value_File; + static const char* Serialization_Value_Index; + + public: + struct ImageInfo + { + qint64 fileSize{0}; + QDateTime fileDate; + + QSize imageSize; + }; + + public: + ImageReference(Project* project, QString imageFilePath); + + bool operator ==(const ImageReference& ref) const { return *this == ref._imageFilePath; } + bool operator ==(QString image) const; + bool operator <(const ImageReference& ref) const { return *this < ref._imageFilePath; } + bool operator <(QString image) const; + + public: + cv::Mat loadImage() const; + + void refreshImageInfo(); + + public: + QString getImageFilePath() const { return _imageFilePath; } + QString getImageFileName() const; + ImageInfo getImageInfo() const { return _imageInfo; } + + bool isValid() const { return _isValid; } + + public: + void serialize(SerializationContext& ctx) const; + void deserialize(DeserializationContext& ctx); + + private: + QString _imageFilePath{""}; + ImageInfo _imageInfo; + + bool _isValid{false}; + }; +} + +#endif diff --git a/Grinder/project/ImageReferenceVector.cpp b/Grinder/project/ImageReferenceVector.cpp new file mode 100644 index 0000000000000000000000000000000000000000..cc64f7a6b19837fb57e821caeb41b6ab2925a11d --- /dev/null +++ b/Grinder/project/ImageReferenceVector.cpp @@ -0,0 +1,20 @@ +/****************************************************************************** + * File: ImageReferenceVector.cpp + * Date: 10.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ImageReferenceVector.h" + +const char* ImageReferenceVector::Serialization_Group = "ImageReferences"; +const char* ImageReferenceVector::Serialization_Element = "ImageReference"; + +ImageReferenceVector::pointer_type ImageReferenceVector::selectByFilePath(QString filePath) const +{ + return selectFirst([filePath](auto ref) { return *ref == filePath; }); +} + +ImageReferenceVector::pointer_type ImageReferenceVector::selectByFileName(QString fileName) const +{ + return selectFirst([fileName](auto ref) { return ref->getImageFileName().compare(fileName, Qt::CaseInsensitive) == 0; }); +} diff --git a/Grinder/project/ImageReferenceVector.h b/Grinder/project/ImageReferenceVector.h new file mode 100644 index 0000000000000000000000000000000000000000..229bc2f324c78c9c9379698fe1f31aa257e8f8fa --- /dev/null +++ b/Grinder/project/ImageReferenceVector.h @@ -0,0 +1,26 @@ +/****************************************************************************** + * File: ImageReferenceVector.h + * Date: 10.2.2018 + *****************************************************************************/ + +#ifndef IMAGEREFERENCEVECTOR_H +#define IMAGEREFERENCEVECTOR_H + +#include "common/ObjectVector.h" +#include "ImageReference.h" + +namespace grndr +{ + class ImageReferenceVector : public ObjectVector<ImageReference> + { + public: + static const char* Serialization_Group; + static const char* Serialization_Element; + + public: + pointer_type selectByFilePath(QString filePath) const; + pointer_type selectByFileName(QString fileName) const; + }; +} + +#endif diff --git a/Grinder/project/Label.cpp b/Grinder/project/Label.cpp new file mode 100644 index 0000000000000000000000000000000000000000..0ea909bc63c15e9d82ea80c87cc310de9f877997 --- /dev/null +++ b/Grinder/project/Label.cpp @@ -0,0 +1,65 @@ +/****************************************************************************** + * File: Label.cpp + * Date: 07.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "Label.h" + +const char* Label::Serialization_Group_Pipeline = "Pipeline"; +const char* Label::Serialization_Group_Layout = "Layout"; +const char* Label::Serialization_Group_ImageBuildPool = "ImageBuildPool"; + +const char* Label::Serialization_Value_Name = "Name"; + +Label::Label(Project* project, const std::shared_ptr<Pipeline>& pipeline) : ProjectItem(project), + _pipeline{pipeline}, _imageBuildPool{this} +{ + if (!pipeline) + throw std::invalid_argument{_EXCPT("pipeline may not be null")}; +} + +void Label::serialize(SerializationContext& ctx) const +{ + // Serialize values + ctx.settings()[Serialization_Value_Name] = getName(); + + // Serialize the pipeline + ctx.beginGroup(Serialization_Group_Pipeline); + _pipeline->serialize(ctx); + ctx.endGroup(); + + // Serialize the graph layout + ctx.beginGroup(Serialization_Group_Layout, true); + _graphLayout.serialize(ctx); + ctx.endGroup(); + + // Serialize the image builds + ctx.beginGroup(Serialization_Group_ImageBuildPool, true); + _imageBuildPool.serialize(ctx); + ctx.endGroup(); +} + +void Label::deserialize(DeserializationContext& ctx) +{ + // Deserialize the pipeline + if (ctx.beginGroup(Serialization_Group_Pipeline)) + { + _pipeline->deserialize(ctx); + ctx.endGroup(); + } + + // Deserialize the graph layout + if (ctx.beginGroup(Serialization_Group_Layout)) + { + _graphLayout.deserialize(_pipeline.get(), ctx); + ctx.endGroup(); + } + + // Deserialize the image builds + if (ctx.beginGroup(Serialization_Group_ImageBuildPool)) + { + _imageBuildPool.deserialize(ctx); + ctx.endGroup(); + } +} diff --git a/Grinder/project/Label.h b/Grinder/project/Label.h new file mode 100644 index 0000000000000000000000000000000000000000..437f2809ee7bc855b327a91f8ddc8a0b87ed4196 --- /dev/null +++ b/Grinder/project/Label.h @@ -0,0 +1,58 @@ +/****************************************************************************** + * File: Label.h + * Date: 07.2.2018 + *****************************************************************************/ + +#ifndef LABEL_H +#define LABEL_H + +#include <memory> + +#include "project/ProjectItem.h" +#include "pipeline/Pipeline.h" +#include "image/ImageBuildPool.h" +#include "ui/graph/GraphLayout.h" + +namespace grndr +{ + class Project; + + class Label : public ProjectItem + { + Q_OBJECT + + public: + static const char* Serialization_Group_Pipeline; + static const char* Serialization_Group_Layout; + static const char* Serialization_Group_ImageBuildPool; + + static const char* Serialization_Value_Name; + + public: + Label(Project* project, const std::shared_ptr<Pipeline>& pipeline); + + public: + Pipeline* pipeline() { return _pipeline.get(); } + const Pipeline* pipeline() const { return _pipeline.get(); } + GraphLayout& graphLayout() { return _graphLayout; } + const GraphLayout& graphLayout() const { return _graphLayout; } + + ImageBuildPool& imageBuildPool() { return _imageBuildPool; } + const ImageBuildPool& imageBuildPool() const { return _imageBuildPool; } + + QString getName() const { return _pipeline->getName(); } + void setName(QString name) { _pipeline->setName(name); } + + public: + void serialize(SerializationContext& ctx) const; + void deserialize(DeserializationContext& ctx); + + private: + std::shared_ptr<Pipeline> _pipeline; + GraphLayout _graphLayout; + + ImageBuildPool _imageBuildPool; + }; +} + +#endif diff --git a/Grinder/project/LabelVector.cpp b/Grinder/project/LabelVector.cpp new file mode 100644 index 0000000000000000000000000000000000000000..0b950d96638dfefe3ab983ab659600663df502de --- /dev/null +++ b/Grinder/project/LabelVector.cpp @@ -0,0 +1,15 @@ +/****************************************************************************** + * File: LabelVector.cpp + * Date: 07.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "LabelVector.h" + +const char* LabelVector::Serialization_Group = "Labels"; +const char* LabelVector::Serialization_Element = "Label"; + +LabelVector::pointer_type LabelVector::selectByName(QString name, bool caseSensitive) const +{ + return selectFirst([name, caseSensitive](auto label) { return label->getName().compare(name, caseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive) == 0; }); +} diff --git a/Grinder/project/LabelVector.h b/Grinder/project/LabelVector.h new file mode 100644 index 0000000000000000000000000000000000000000..dda5105d14e2ef0adb61837e2bb2066f706eb46c --- /dev/null +++ b/Grinder/project/LabelVector.h @@ -0,0 +1,25 @@ +/****************************************************************************** + * File: LabelVector.h + * Date: 07.2.2018 + *****************************************************************************/ + +#ifndef LABELVECTOR_H +#define LABELVECTOR_H + +#include "common/ObjectVector.h" +#include "Label.h" + +namespace grndr +{ + class LabelVector : public ObjectVector<Label> + { + public: + static const char* Serialization_Group; + static const char* Serialization_Element; + + public: + pointer_type selectByName(QString name, bool caseSensitive = false) const; + }; +} + +#endif diff --git a/Grinder/project/Project.cpp b/Grinder/project/Project.cpp new file mode 100644 index 0000000000000000000000000000000000000000..8aea8229eb47761d0af409c5017f4002973d0692 --- /dev/null +++ b/Grinder/project/Project.cpp @@ -0,0 +1,182 @@ +/****************************************************************************** + * File: Project.cpp + * Date: 07.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "Project.h" +#include "ProjectExceptions.h" +#include "core/GrinderApplication.h" +#include "pipeline/PipelineManager.h" + +const char* Project::Serialization_Group = "Project"; +const char* Project::Serialization_Value_Name = "Name"; + +std::shared_ptr<Label> Project::createLabel(QString name) +{ + if (name.isEmpty()) + throw std::invalid_argument{_EXCPT("name may not be empty")}; + + if (_labels.selectByName(name)) + throw LabelException{nullptr, _EXCPT(QString{"A label with name '%1' already exists"}.arg(name))}; + + // Create a pipeline for the label; if this fails, re-throw + try { + auto pipeline = grinder()->pipelineManager().createPipeline(name); + + // Pipeline creation succeeded, so create a label for it + auto label = std::make_shared<Label>(this, pipeline); + + _labels.push_back(label); + emit labelCreated(label); + + return label; + } catch (std::exception& e) { + throw LabelException{nullptr, _EXCPT(QString{"Failed to create label '%1': %2"}.arg(name).arg(e.what()))}; + } +} + +void Project::removeLabel(QString name) +{ + if (!name.isEmpty()) + { + auto label = _labels.selectByName(name); + + if (label) + removeLabel(label.get()); + else + throw LabelException{nullptr, _EXCPT(QString{"Tried to remove a non-existing label (Name: %1)"}.arg(name))}; + } +} + +void Project::removeLabel(const Label* label) +{ + if (label) + { + auto it = _labels.find(label); + + if (it != _labels.cend()) + { + // Remove the pipeline associated with the label from the pipeline manager + grinder()->pipelineManager().removePipeline(it->get()->pipeline()); + + // Keep a copy of the shared_ptr holding the label to increase its use count; + // otherwise, the label will be deleted before it has been removed from the vector, potentially causing a crash + auto label = *it; + + emit labelRemoved(*it); + _labels.erase(it); + } + else + throw LabelException{label, _EXCPT("Tried to remove a label not currently part of the project")}; + } +} + +std::shared_ptr<ImageReference> Project::createImageReference(QString imagePath) +{ + if (imagePath.isEmpty()) + throw std::invalid_argument{_EXCPT("imagePath may not be empty")}; + + if (_imageReferences.selectByFilePath(imagePath)) + throw ImageReferenceException{nullptr, _EXCPT(QString{"The image '%1' has already been added to the project"}.arg(imagePath))}; + + auto imageRef = std::make_shared<ImageReference>(this, imagePath); + + try { // Propagate image reference errors to the caller + imageRef->refreshImageInfo(); + } catch (...) { + throw; + } + + _imageReferences.push_back(imageRef); + emit imageReferenceCreated(imageRef); + + return imageRef; +} + +void Project::removeImageReference(QString imagePath) +{ + if (!imagePath.isEmpty()) + { + auto ref = _imageReferences.selectByFilePath(imagePath); + + if (ref) + removeImageReference(ref.get()); + else + throw ImageReferenceException{nullptr, _EXCPT(QString{"Tried to remove a non-existing image reference (Path: %1)"}.arg(imagePath))}; + } +} + +void Project::removeImageReference(const ImageReference* imageRef) +{ + if (imageRef) + { + auto it = _imageReferences.find(imageRef); + + if (it != _imageReferences.cend()) + { + // Keep a copy of the shared_ptr holding the image ref to increase its use count; + // otherwise, the image ref will be deleted before it has been removed from the vector, potentially causing a crash + auto imageRef = *it; + + emit imageReferenceRemoved(*it); + _imageReferences.erase(it); + } + else + throw ImageReferenceException{imageRef, _EXCPT("Tried to remove an image reference not currently part of the project")}; + } +} + +void Project::clear() +{ + // Remove all items using the corresponding project functions so that they are removed in a proper manner + while (!_labels.empty()) + removeLabel(_labels.back().get()); + + while (!_imageReferences.empty()) + removeImageReference(_imageReferences.back().get()); +} + +void Project::serialize(SerializationContext& ctx) const +{ + // Serialize values + ctx.settings()[Serialization_Value_Name] = _name; + + // Serialize all image references (has to be done before the label serialization, since labels reference them) + ctx.beginGroup(ImageReferenceVector::Serialization_Group, true); + _imageReferences.serialize(ImageReferenceVector::Serialization_Element, ctx); + ctx.endGroup(); + + // Serialize all labels + ctx.beginGroup(LabelVector::Serialization_Group, true); + _labels.serialize(LabelVector::Serialization_Element, ctx); + ctx.endGroup(); +} + +void Project::deserialize(DeserializationContext& ctx) +{ + // Deserialize values + _name = ctx.settings()[Serialization_Value_Name].toString(); + + // Deserialize all image references (has to be done before the label deserialization, since labels reference them) + if (ctx.beginGroup(ImageReferenceVector::Serialization_Group)) + { + _imageReferences.deserialize(ImageReferenceVector::Serialization_Element, ctx, [this](const SettingsContainer& settings) { + QString file = settings[ImageReference::Serialization_Value_File].toString(); + return createImageReference(file); + }); + + ctx.endGroup(); + } + + // Deserialize all labels + if (ctx.beginGroup(LabelVector::Serialization_Group)) + { + _labels.deserialize(LabelVector::Serialization_Element, ctx, [this](const SettingsContainer& settings) { + QString name = settings[Label::Serialization_Value_Name].toString(); + return createLabel(name); + }); + + ctx.endGroup(); + } +} diff --git a/Grinder/project/Project.h b/Grinder/project/Project.h new file mode 100644 index 0000000000000000000000000000000000000000..b8b7fb5c04dfec56e57f97e2cc9763e1fce4165c --- /dev/null +++ b/Grinder/project/Project.h @@ -0,0 +1,64 @@ +/****************************************************************************** + * File: Project.h + * Date: 07.2.2018 + *****************************************************************************/ + +#ifndef PROJECT_H +#define PROJECT_H + +#include <QObject> + +#include "LabelVector.h" +#include "ImageReferenceVector.h" +#include "serialization/SerializationContext.h" +#include "serialization/DeserializationContext.h" + +namespace grndr +{ + class Project : public QObject + { + Q_OBJECT + + public: + static const char* Serialization_Group; + static const char* Serialization_Value_Name; + + public: + std::shared_ptr<Label> createLabel(QString name); + void removeLabel(QString name); + void removeLabel(const Label* label); + + std::shared_ptr<ImageReference> createImageReference(QString imagePath); + void removeImageReference(QString imagePath); + void removeImageReference(const ImageReference* imageRef); + + public: + QString name() const { return _name; } + void setName(QString name) { _name = name; } + + const LabelVector& labels() const { return _labels; } + const ImageReferenceVector& imageReferences() const { return _imageReferences; } + + public: + void clear(); + + public: + void serialize(SerializationContext& ctx) const; + void deserialize(DeserializationContext& ctx); + + signals: + void labelCreated(const std::shared_ptr<Label>&); + void labelRemoved(const std::shared_ptr<Label>&); + + void imageReferenceCreated(const std::shared_ptr<ImageReference>&); + void imageReferenceRemoved(const std::shared_ptr<ImageReference>&); + + private: + QString _name{""}; + + LabelVector _labels; + ImageReferenceVector _imageReferences; + }; +} + +#endif diff --git a/Grinder/project/ProjectExceptions.cpp b/Grinder/project/ProjectExceptions.cpp new file mode 100644 index 0000000000000000000000000000000000000000..9e807613a5bbaec58fa58092aeadb60395a06c15 --- /dev/null +++ b/Grinder/project/ProjectExceptions.cpp @@ -0,0 +1,27 @@ +/****************************************************************************** + * File: ProjectExceptions.cpp + * Date: 07.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ProjectExceptions.h" +#include "Label.h" +#include "ImageReference.h" + +ProjectException::ProjectException(const Project* project, QString what) : GrinderException{what}, + _project{project} +{ + +} + +LabelException::LabelException(const Label* label, QString what) : ProjectException(label ? label->project() : nullptr, what), + _label{label} +{ + +} + +ImageReferenceException::ImageReferenceException(const ImageReference* imageRef, QString what) : ProjectException(imageRef ? imageRef->project() : nullptr, what), + _imageReference{imageRef} +{ + +} diff --git a/Grinder/project/ProjectExceptions.h b/Grinder/project/ProjectExceptions.h new file mode 100644 index 0000000000000000000000000000000000000000..ca1313339eb3cb45645902cb8fa5ab71a15bb1fd --- /dev/null +++ b/Grinder/project/ProjectExceptions.h @@ -0,0 +1,56 @@ +/****************************************************************************** + * File: ProjectExceptions.h + * Date: 07.2.2018 + *****************************************************************************/ + +#ifndef PROJECTEXCEPTIONS_H +#define PROJECTEXCEPTIONS_H + +#include <QString> + +#include "core/GrinderExceptions.h" + +namespace grndr +{ + class Project; + class Label; + class ImageReference; + + class ProjectException : public GrinderException + { + public: + ProjectException(const Project* project, QString what); + + public: + const Project* project() const { return _project; } + + protected: + const Project* _project{nullptr}; + }; + + class LabelException : public ProjectException + { + public: + LabelException(const Label* label, QString what); + + public: + const Label* label() const { return _label; } + + protected: + const Label* _label{nullptr}; + }; + + class ImageReferenceException : public ProjectException + { + public: + ImageReferenceException(const ImageReference* imageRef, QString what); + + public: + const ImageReference* imageReference() const { return _imageReference; } + + protected: + const ImageReference* _imageReference{nullptr}; + }; +} + +#endif diff --git a/Grinder/project/ProjectItem.cpp b/Grinder/project/ProjectItem.cpp new file mode 100644 index 0000000000000000000000000000000000000000..04de9b10812465528d73b91d2f5a88f41c725fa2 --- /dev/null +++ b/Grinder/project/ProjectItem.cpp @@ -0,0 +1,14 @@ +/****************************************************************************** + * File: ProjectItem.cpp + * Date: 10.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ProjectItem.h" + +ProjectItem::ProjectItem(grndr::Project* project) : + _project{project} +{ + if (!project) + throw std::invalid_argument{_EXCPT("project may not be null")}; +} diff --git a/Grinder/project/ProjectItem.h b/Grinder/project/ProjectItem.h new file mode 100644 index 0000000000000000000000000000000000000000..f60c49ee2776b0b1bd56051db3c51e23bde0d6f6 --- /dev/null +++ b/Grinder/project/ProjectItem.h @@ -0,0 +1,31 @@ +/****************************************************************************** + * File: ProjectItem.h + * Date: 10.2.2018 + *****************************************************************************/ + +#ifndef PROJECTITEM_H +#define PROJECTITEM_H + +#include <QObject> + +namespace grndr +{ + class Project; + + class ProjectItem : public QObject + { + Q_OBJECT + + public: + ProjectItem(Project* project); + + public: + Project* project() { return _project; } + const Project* project() const { return _project; } + + protected: + Project* _project{nullptr}; + }; +} + +#endif diff --git a/Grinder/project/serialization/DeserializationContext.cpp b/Grinder/project/serialization/DeserializationContext.cpp new file mode 100644 index 0000000000000000000000000000000000000000..68de26ac1c5cfe3482022fcdbf35e724d74993ed --- /dev/null +++ b/Grinder/project/serialization/DeserializationContext.cpp @@ -0,0 +1,24 @@ +/****************************************************************************** + * File: DeserializationContext.cpp + * Date: 28.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "DeserializationContext.h" + +DeserializationContext::DeserializationContext(const SettingsContainer& settings) : + _settings{settings} +{ + +} + +bool DeserializationContext::beginGroup(QString name) +{ + if (auto child = settings().child(name)) + { + _settingsStack.push(child); + return true; + } + else + return false; +} diff --git a/Grinder/project/serialization/DeserializationContext.h b/Grinder/project/serialization/DeserializationContext.h new file mode 100644 index 0000000000000000000000000000000000000000..b9504d25c897ae87ed238a5663a8ce6b60a9d598 --- /dev/null +++ b/Grinder/project/serialization/DeserializationContext.h @@ -0,0 +1,59 @@ +/****************************************************************************** + * File: DeserializationContext.h + * Date: 28.2.2018 + *****************************************************************************/ + +#ifndef DESERIALIZATIONCONTEXT_H +#define DESERIALIZATIONCONTEXT_H + +#include <stack> + +#include "SettingsContainer.h" + +namespace grndr +{ + class Block; + class Port; + class ImageReference; + + class DeserializationContext + { + public: + DeserializationContext(const SettingsContainer& settings); + + public: + SettingsContainer& settings(bool rootSettings = false) { if (!rootSettings && !_settingsStack.empty()) return *_settingsStack.top(); else return _settings; } + const SettingsContainer& settings(bool rootSettings = false) const { if (!rootSettings && !_settingsStack.empty()) return *_settingsStack.top(); else return _settings; } + + public: + bool beginGroup(QString name); + void beginGroup(SettingsContainer* settings) { _settingsStack.push(settings); } + void endGroup() { _settingsStack.pop(); } + + public: + void addBlock(int index, Block* block) { _blocks[index] = block; } + Block* getBlock(int index) const { return getObject(index, _blocks); } + + void addPort(int index, Port* port) { _ports[index] = port; } + Port* getPort(int index) const { return getObject(index, _ports); } + + void addImageReference(int index, ImageReference* imageRef) { _imageReferences[index] = imageRef; } + ImageReference* getImageReference(int index) const { return getObject(index, _imageReferences); } + + private: + template<typename ObjType> + ObjType* getObject(int index, const std::map<int, ObjType*>& vec) const; + + private: + SettingsContainer _settings; + std::stack<SettingsContainer*> _settingsStack; + + std::map<int, Block*> _blocks; + std::map<int, Port*> _ports; + std::map<int, ImageReference*> _imageReferences; + }; +} + +#include "DeserializationContext.impl.h" + +#endif diff --git a/Grinder/project/serialization/DeserializationContext.impl.h b/Grinder/project/serialization/DeserializationContext.impl.h new file mode 100644 index 0000000000000000000000000000000000000000..f1a257468562ad933104f4d8a4ddb37797ed078b --- /dev/null +++ b/Grinder/project/serialization/DeserializationContext.impl.h @@ -0,0 +1,16 @@ +/****************************************************************************** + * File: DeserializationContext.impl.h + * Date: 12.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "DeserializationContext.h" + +template<typename ObjType> +ObjType* DeserializationContext::getObject(int index, const std::map<int, ObjType*>& vec) const +{ + if (vec.find(index) != vec.cend()) + return vec.at(index); + else + return nullptr; +} diff --git a/Grinder/project/serialization/JsonSettingsCodec.cpp b/Grinder/project/serialization/JsonSettingsCodec.cpp new file mode 100644 index 0000000000000000000000000000000000000000..fc3ef490dfafeb1d760858aacf36c5ae77a8eb57 --- /dev/null +++ b/Grinder/project/serialization/JsonSettingsCodec.cpp @@ -0,0 +1,119 @@ +/****************************************************************************** + * File: JsonSettingsCodec.cpp + * Date: 27.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "JsonSettingsCodec.h" +#include "SerializationExceptions.h" + +void JsonSettingsCodec::encodeSettings(const SettingsContainer& settings) +{ + _document.setObject(encode(settings)); +} + +void JsonSettingsCodec::decodeSettings(SettingsContainer& settings) +{ + if (!_document.isObject()) + throw SerializationException{_EXCPT("Invalid JSON document")}; + + settings.clear(); + decode(settings, _document.object()); +} + +QString JsonSettingsCodec::getText() const +{ + if (_document.isNull()) + throw SerializationException{_EXCPT("Invalid JSON document")}; + + return _document.toJson(); +} + +void JsonSettingsCodec::setText(const QString& text) +{ + QJsonParseError parseError; + _document = QJsonDocument::fromJson(text.toLatin1(), &parseError); + + if (_document.isNull()) + throw SerializationException{_EXCPT(QString{"Invalid JSON document: %1"}.arg(parseError.errorString()))}; +} + +QJsonObject JsonSettingsCodec::encode(const SettingsContainer& settings) +{ + QJsonObject object = QJsonObject::fromVariantMap(settings.values()); + + // Add all children of the current container + for (const auto& child : settings.children()) + { + if (child->isArray()) + { + QJsonArray array; + + // Add all children of the current child to the array + for (const auto& arrayElem : child->children()) + { + QJsonObject containerObj; + containerObj[arrayElem->getName()] = encode(*arrayElem); + array.append(containerObj); + } + + object[child->getName()] = array; + } + else + object[child->getName()] = encode(*child); + } + + return object; +} + +void JsonSettingsCodec::decode(SettingsContainer& settings, const QJsonObject& object) +{ + for (const auto& key : object.keys()) + { + const auto& value = object[key]; + + if (value.isObject()) + { + SettingsContainer childContainer{key}; + decode(childContainer, value.toObject()); + settings << childContainer; + } + else if (value.isArray()) + { + SettingsContainer childContainer{key, true}; + QJsonArray array = value.toArray(); + + // Iterate over all array elements and create setting containers for each + for (const auto& arrayElem : array) + { + if (arrayElem.isObject()) + { + auto containerObj = arrayElem.toObject(); + + // The array element must contain a single object which contains the actual (named) object + if (containerObj.size() == 1) + { + QString elemName = containerObj.keys().front(); + + if (containerObj[elemName].isObject()) + { + SettingsContainer elemContainer{elemName}; + decode(elemContainer, containerObj[elemName].toObject()); + childContainer << elemContainer; + } + else + throw SerializationException{_EXCPT("Invalid array element found")}; + } + else + throw SerializationException{_EXCPT("Invalid container object found")}; + } + else + throw SerializationException{_EXCPT("Invalid array found")}; + } + + settings << childContainer; + } + else + settings[key] = value.toVariant(); + } +} diff --git a/Grinder/project/serialization/JsonSettingsCodec.h b/Grinder/project/serialization/JsonSettingsCodec.h new file mode 100644 index 0000000000000000000000000000000000000000..0c89b6e141e2addcc6f59ad462865c89bbba04c0 --- /dev/null +++ b/Grinder/project/serialization/JsonSettingsCodec.h @@ -0,0 +1,34 @@ +/****************************************************************************** + * File: JsonSettingsCodec.h + * Date: 27.2.2018 + *****************************************************************************/ + +#ifndef JSONSETTINGSCODEC_H +#define JSONSETTINGSCODEC_H + +#include <QJsonDocument> + +#include "SettingsCodec.h" + +namespace grndr +{ + class JsonSettingsCodec : public SettingsCodec + { + public: + virtual void encodeSettings(const SettingsContainer& settings) override; + virtual void decodeSettings(SettingsContainer& settings) override; + + protected: + virtual QString getText() const override; + virtual void setText(const QString& text) override; + + private: + QJsonObject encode(const SettingsContainer& settings); + void decode(SettingsContainer& settings, const QJsonObject& object); + + private: + QJsonDocument _document; + }; +} + +#endif diff --git a/Grinder/project/serialization/ProjectSerializer.cpp b/Grinder/project/serialization/ProjectSerializer.cpp new file mode 100644 index 0000000000000000000000000000000000000000..4964a7fad305349c9ac14d88ea2da3fd00e57f70 --- /dev/null +++ b/Grinder/project/serialization/ProjectSerializer.cpp @@ -0,0 +1,41 @@ +/****************************************************************************** + * File: ProjectSerializer.cpp + * Date: 27.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ProjectSerializer.h" +#include "SerializationExceptions.h" +#include "SerializationContext.h" +#include "DeserializationContext.h" +#include "project/Project.h" + +ProjectSerializer::ProjectSerializer(Project* project) : + _project{project} +{ + if (!project) + throw std::invalid_argument{_EXCPT("project may not be null")}; +} + +SettingsContainer ProjectSerializer::serializeProject() const +{ + if (_project) + { + SerializationContext ctx{_project}; + _project->serialize(ctx); + return ctx.settings(true); + } + else + throw SerializationException{_EXCPT("No project to serialize")}; +} + +void ProjectSerializer::deserializeProject(const SettingsContainer& settings) +{ + if (_project) + { + DeserializationContext ctx{settings}; + _project->deserialize(ctx); + } + else + throw SerializationException{_EXCPT("No project to deserialize")}; +} diff --git a/Grinder/project/serialization/ProjectSerializer.h b/Grinder/project/serialization/ProjectSerializer.h new file mode 100644 index 0000000000000000000000000000000000000000..50ee5f3b8466aaf860f21b6be75473702bdeb04f --- /dev/null +++ b/Grinder/project/serialization/ProjectSerializer.h @@ -0,0 +1,29 @@ +/****************************************************************************** + * File: ProjectSerializer.h + * Date: 27.2.2018 + *****************************************************************************/ + +#ifndef PROJECTSERIALIZER_H +#define PROJECTSERIALIZER_H + +#include "SettingsContainer.h" + +namespace grndr +{ + class Project; + + class ProjectSerializer + { + public: + ProjectSerializer(Project* project); + + public: + SettingsContainer serializeProject() const; + void deserializeProject(const SettingsContainer& settings); + + private: + Project* _project{nullptr}; + }; +} + +#endif diff --git a/Grinder/project/serialization/SerializationContext.cpp b/Grinder/project/serialization/SerializationContext.cpp new file mode 100644 index 0000000000000000000000000000000000000000..c436154f8ef76187b7361183a5bebdb832824253 --- /dev/null +++ b/Grinder/project/serialization/SerializationContext.cpp @@ -0,0 +1,33 @@ +/****************************************************************************** + * File: SerializationContext.cpp + * Date: 28.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "SerializationContext.h" +#include "pipeline/ConnectionVector.h" +#include "project/Project.h" + +SerializationContext::SerializationContext(Project* project, const SettingsContainer* settings) : + _project{project}, _settings{Project::Serialization_Group} +{ + if (!project) + throw std::invalid_argument{_EXCPT("project may not be null")}; + + if (settings) + _settings = *settings; +} + +void SerializationContext::endGroup() +{ + // Remove the top group from the stack and add it to the preceding container + auto currentSettings = std::move(_settingsStack.top()); + _settingsStack.pop(); + settings() << currentSettings; +} + +void SerializationContext::addConnection(const Connection* con) +{ + if (std::find(_connections.cbegin(), _connections.cend(), con) == _connections.cend()) + _connections.push_back(con); +} diff --git a/Grinder/project/serialization/SerializationContext.h b/Grinder/project/serialization/SerializationContext.h new file mode 100644 index 0000000000000000000000000000000000000000..6d494973fd642dfa6159e14fa53ba715897a23c5 --- /dev/null +++ b/Grinder/project/serialization/SerializationContext.h @@ -0,0 +1,68 @@ +/****************************************************************************** + * File: SerializationContext.h + * Date: 28.2.2018 + *****************************************************************************/ + +#ifndef SERIALIZATIONCONTEXT_H +#define SERIALIZATIONCONTEXT_H + +#include <stack> + +#include "SettingsContainer.h" + +namespace grndr +{ + class Project; + class Block; + class Port; + class Connection; + class ImageReference; + + class SerializationContext + { + public: + SerializationContext(Project* project, const SettingsContainer* settings = nullptr); + + public: + SettingsContainer& settings(bool rootSettings = false) { if (!rootSettings && !_settingsStack.empty()) return _settingsStack.top(); else return _settings; } + const SettingsContainer& settings(bool rootSettings = false) const { if (!rootSettings && !_settingsStack.empty()) return _settingsStack.top(); else return _settings; } + + public: + void beginGroup(QString name, bool isArray = false) { _settingsStack.emplace(name, isArray); } + void endGroup(); + + public: + int addBlock(const Block* block) { return addObject(block, _blocks); } + int getBlockIndex(const Block* block) const { return getObjectIndex(block, _blocks); } + + int addPort(const Port* port) { return addObject(port, _ports); } + int getPortIndex(const Port* port) const { return getObjectIndex(port, _ports); } + + void addConnection(const Connection* con); + const std::vector<const Connection*>& connections() { return _connections; } + + int addImageReference(const ImageReference* imageRef) { return addObject(imageRef, _imageReferences); } + int getImageReferenceIndex(const ImageReference* imageRef) const { return getObjectIndex(imageRef, _imageReferences); } + + private: + template<typename ObjType> + int addObject(const ObjType* obj, std::vector<const ObjType*>& vec); + template<typename ObjType> + int getObjectIndex(const ObjType* obj, const std::vector<const ObjType*>& vec) const; + + private: + Project* _project{nullptr}; + + SettingsContainer _settings; + std::stack<SettingsContainer> _settingsStack; + + std::vector<const Block*> _blocks; + std::vector<const Port*> _ports; + std::vector<const Connection*> _connections; + std::vector<const ImageReference*> _imageReferences; + }; +} + +#include "SerializationContext.impl.h" + +#endif diff --git a/Grinder/project/serialization/SerializationContext.impl.h b/Grinder/project/serialization/SerializationContext.impl.h new file mode 100644 index 0000000000000000000000000000000000000000..01e4170b9dfefe28799f5fcd27eaa59d4e6ca10f --- /dev/null +++ b/Grinder/project/serialization/SerializationContext.impl.h @@ -0,0 +1,32 @@ +/****************************************************************************** + * File: SerializationContext.impl.h + * Date: 12.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "SerializationContext.h" + +template<typename ObjType> +int SerializationContext::addObject(const ObjType* obj, std::vector<const ObjType*>& vec) +{ + auto index = getObjectIndex(obj, vec); + + if (index == -1) + { + vec.push_back(obj); + index = vec.size() - 1; + } + + return index; +} + +template<typename ObjType> +int SerializationContext::getObjectIndex(const ObjType* obj, const std::vector<const ObjType*>& vec) const +{ + auto it = std::find(vec.cbegin(), vec.cend(), obj); + + if (it != vec.cend()) + return it - vec.cbegin(); + else + return -1; +} diff --git a/Grinder/project/serialization/SerializationExceptions.cpp b/Grinder/project/serialization/SerializationExceptions.cpp new file mode 100644 index 0000000000000000000000000000000000000000..428f8a1077d616a6c893248af979d5846efb83fe --- /dev/null +++ b/Grinder/project/serialization/SerializationExceptions.cpp @@ -0,0 +1,12 @@ +/****************************************************************************** + * File: SerializationExceptions.cpp + * Date: 27.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "SerializationExceptions.h" + +SerializationException::SerializationException(QString what) : GrinderException(what) +{ + +} diff --git a/Grinder/project/serialization/SerializationExceptions.h b/Grinder/project/serialization/SerializationExceptions.h new file mode 100644 index 0000000000000000000000000000000000000000..930f3d00beb615e329b9df1d0d5c2066326abb4e --- /dev/null +++ b/Grinder/project/serialization/SerializationExceptions.h @@ -0,0 +1,20 @@ +/****************************************************************************** + * File: SerializationExceptions.h + * Date: 27.2.2018 + *****************************************************************************/ + +#ifndef SERIALIZATIONEXCEPTIONS_H +#define SERIALIZATIONEXCEPTIONS_H + +#include "core/GrinderExceptions.h" + +namespace grndr +{ + class SerializationException : public GrinderException + { + public: + SerializationException(QString what); + }; +} + +#endif diff --git a/Grinder/project/serialization/SettingsCodec.cpp b/Grinder/project/serialization/SettingsCodec.cpp new file mode 100644 index 0000000000000000000000000000000000000000..e41ba6e085fde6a7fe8f9ae384fe4d842598595e --- /dev/null +++ b/Grinder/project/serialization/SettingsCodec.cpp @@ -0,0 +1,52 @@ +/****************************************************************************** + * File: SettingsCodec.cpp + * Date: 27.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "SettingsCodec.h" +#include "SerializationExceptions.h" + +void SettingsCodec::saveContainer(const SettingsContainer& settings, QString fileName) +{ + try { + encodeSettings(settings); + + QFile file{fileName}; + + if (file.open(QIODevice::WriteOnly|QIODevice::Truncate|QIODevice::Text)) + { + QTextStream out{&file}; + out << getText(); + } + else + throw SerializationException{_EXCPT(QString{"Unable to open file '%1' for writing"}.arg(fileName))}; + } catch (SerializationException&) { + // Just re-throw + throw; + } catch (std::exception& e) { + // Forward any exceptions as a SerializationException + throw SerializationException{_EXCPT(e.what())}; + } +} + +void SettingsCodec::loadContainer(SettingsContainer& settings, QString fileName) +{ + try { + QFile file{fileName}; + + if (file.open(QIODevice::ReadOnly|QIODevice::Text)) + { + setText(file.readAll()); + decodeSettings(settings); + } + else + throw SerializationException{_EXCPT(QString{"Unable to open file '%1' for reading"}.arg(fileName))}; + } catch (SerializationException&) { + // Just re-throw + throw; + } catch (std::exception& e) { + // Forward any exceptions as a SerializationException + throw SerializationException{_EXCPT(e.what())}; + } +} diff --git a/Grinder/project/serialization/SettingsCodec.h b/Grinder/project/serialization/SettingsCodec.h new file mode 100644 index 0000000000000000000000000000000000000000..f30b3292cece0372cac6f47c75d7ac3e8f5485cf --- /dev/null +++ b/Grinder/project/serialization/SettingsCodec.h @@ -0,0 +1,28 @@ +/****************************************************************************** + * File: SettingsCodec.h + * Date: 27.2.2018 + *****************************************************************************/ + +#ifndef SETTINGSCODEC_H +#define SETTINGSCODEC_H + +#include "SettingsContainer.h" + +namespace grndr +{ + class SettingsCodec + { + public: + virtual void encodeSettings(const SettingsContainer& settings) = 0; + virtual void decodeSettings(SettingsContainer& settings) = 0; + + void saveContainer(const SettingsContainer& settings, QString fileName); + void loadContainer(SettingsContainer& settings, QString fileName); + + protected: + virtual QString getText() const = 0; + virtual void setText(const QString& text) = 0; + }; +} + +#endif diff --git a/Grinder/project/serialization/SettingsContainer.cpp b/Grinder/project/serialization/SettingsContainer.cpp new file mode 100644 index 0000000000000000000000000000000000000000..deecb3b47a07f7d874b25a408f471d74487c32b7 --- /dev/null +++ b/Grinder/project/serialization/SettingsContainer.cpp @@ -0,0 +1,62 @@ +/****************************************************************************** + * File: SettingsContainer.cpp + * Date: 26.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "SettingsContainer.h" + +SettingsContainer::SettingsContainer(QString name, bool isArray) : + _name{name}, _isArray{isArray} +{ + +} + +bool SettingsContainer::operator ==(const SettingsContainer& container) +{ + // Compare basic settings + if (_name != container._name || _isArray != container._isArray) + return false; + + // Compare all stored values + if (_values != container._values) + return false; + + // Compare children + auto compare = [](const auto& child1, const auto& child2) { return *child1 == *child2; }; + + if (_isArray) // Arrays need to have the same order to be considered equal + { + if (!std::equal(container._childContainers.cbegin(), container._childContainers.cend(), _childContainers.cbegin(), _childContainers.cend(), compare)) + return false; + } + else + { + if (!std::is_permutation(container._childContainers.cbegin(), container._childContainers.cend(), _childContainers.cbegin(), _childContainers.cend(), compare)) + return false; + } + + return true; +} + +SettingsContainer& SettingsContainer::operator <<(const SettingsContainer& child) +{ + auto container = std::make_shared<SettingsContainer>(child); + _childContainers.push_back(container); + return *this; +} + +SettingsContainer& SettingsContainer::operator <<(SettingsContainer&& child) +{ + auto container = std::make_shared<SettingsContainer>(std::move(child)); + _childContainers.push_back(container); + return *this; +} + +void SettingsContainer::removeChildren(QString name) +{ + if (name.isEmpty()) + throw std::invalid_argument{_EXCPT("name may not be empty")}; + + std::remove_if(_childContainers.begin(), _childContainers.end(), [name](const auto& child) { return child->_name.compare(name, Qt::CaseInsensitive) == 0; }); +} diff --git a/Grinder/project/serialization/SettingsContainer.h b/Grinder/project/serialization/SettingsContainer.h new file mode 100644 index 0000000000000000000000000000000000000000..4feb392981e8f402b0658a46134202b299210ffd --- /dev/null +++ b/Grinder/project/serialization/SettingsContainer.h @@ -0,0 +1,84 @@ +/****************************************************************************** + * File: SettingsContainer.h + * Date: 26.2.2018 + *****************************************************************************/ + +#ifndef SETTINGSCONTAINER_H +#define SETTINGSCONTAINER_H + +#include <QString> +#include <QVariant> +#include <memory> + +namespace grndr +{ + class SettingsContainer + { + public: + SettingsContainer(QString name = "", bool isArray = false); + SettingsContainer(const SettingsContainer& container) = default; + SettingsContainer(SettingsContainer&& container) = default; + + SettingsContainer& operator =(const SettingsContainer& container) = default; + SettingsContainer& operator =(SettingsContainer&& container) = default; + + public: + QString getName() const { return _name; } + bool isArray() const { return _isArray; } + + bool isEmpty() const { return _childContainers.empty() && _values.isEmpty(); } + + void clear() { clearChildren(); clearValues(); } + + bool operator ==(const SettingsContainer& container); + bool operator !=(const SettingsContainer& container) { return !(*this == container); } + + public: + SettingsContainer& operator <<(const SettingsContainer& child); + SettingsContainer& operator <<(SettingsContainer&& child); + + SettingsContainer* child(QString name) { return _child<SettingsContainer*>(name); } + const SettingsContainer* child(QString name) const { return _child<const SettingsContainer*>(name); } + const std::vector<SettingsContainer*> children() { return children(""); } + const std::vector<const SettingsContainer*> children() const { return children(""); } + const std::vector<SettingsContainer*> children(QString name) { return _children<SettingsContainer*>(name); } + const std::vector<const SettingsContainer*> children(QString name) const { return _children<const SettingsContainer*>(name); } + + void removeChildren(QString name); + void clearChildren() { _childContainers.clear(); } + + public: + QVariant& operator[](QString name) { return _values[name]; } + const QVariant operator[](QString name) const { return _values[name]; } + + const QVariantMap& values() const { return _values; } + + QStringList keys() const { return _values.keys(); } + bool contains(QString name) { return _values.find(name) != _values.end(); } + + void removeValue(QString name) { _values.remove(name); } + void clearValues() { _values.clear(); } + + auto begin() { return _values.begin(); } + auto cbegin() const { return _values.cbegin(); } + auto end() { return _values.end(); } + auto cend() const { return _values.cend(); } + + private: + template<typename DataType> + DataType _child(QString name) const; + template<typename DataType> + const std::vector<DataType> _children(QString name) const; + + private: + QString _name{""}; + bool _isArray{false}; + + std::vector<std::shared_ptr<SettingsContainer>> _childContainers; + QVariantMap _values; + }; +} + +#include "SettingsContainer.impl.h" + +#endif diff --git a/Grinder/project/serialization/SettingsContainer.impl.h b/Grinder/project/serialization/SettingsContainer.impl.h new file mode 100644 index 0000000000000000000000000000000000000000..deb06c91019ab1bc8c4117a1ea8a014258882662 --- /dev/null +++ b/Grinder/project/serialization/SettingsContainer.impl.h @@ -0,0 +1,34 @@ +/****************************************************************************** + * File: SettingsContainer.impl.h + * Date: 27.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "SettingsContainer.h" + +template<typename DataType> +DataType SettingsContainer::_child(QString name) const +{ + auto children = _children<DataType>(name); + + if (children.size() == 1) + return children.front(); + else if (children.size() > 1) + throw std::runtime_error{_EXCPT(QString{"The container '%1' is not unique"}.arg(name))}; + else + return nullptr; +} + +template<typename DataType> +const std::vector<DataType> SettingsContainer::_children(QString name) const +{ + std::vector<DataType> children; + + for (const auto& child : _childContainers) + { + if (name.isEmpty() || child->_name.compare(name, Qt::CaseInsensitive) == 0) + children.push_back(child.get()); + } + + return children; +} diff --git a/Grinder/res/Grinder.ico b/Grinder/res/Grinder.ico new file mode 100644 index 0000000000000000000000000000000000000000..aa8495a3735f4244c6b34e56a8c0f795b2585501 Binary files /dev/null and b/Grinder/res/Grinder.ico differ diff --git a/Grinder/res/Grinder.manifest b/Grinder/res/Grinder.manifest new file mode 100644 index 0000000000000000000000000000000000000000..47d33bc26afe4a66151a090def374fbf4d0cc750 --- /dev/null +++ b/Grinder/res/Grinder.manifest @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> + <assemblyIdentity + version="1.0.0.0" + processorArchitecture="X86" + name="WWU Muenster.Grinder" + type="win32" + /> + <description>Grinder</description> + <dependency> + <dependentAssembly> + <assemblyIdentity + type="win32" + name="Microsoft.Windows.Common-Controls" + version="6.0.0.0" + processorArchitecture="*" + publicKeyToken="6595b64144ccf1df" + language="*" + /> + </dependentAssembly> + </dependency> + <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2"> + <security> + <requestedPrivileges> + <requestedExecutionLevel + level="asInvoker" + uiAccess="false"/> + </requestedPrivileges> + </security> + </trustInfo> +</assembly> diff --git a/Grinder/res/Grinder.qrc b/Grinder/res/Grinder.qrc new file mode 100644 index 0000000000000000000000000000000000000000..65b460948d7de489c5e8f7f54742b75a62ca81a3 --- /dev/null +++ b/Grinder/res/Grinder.qrc @@ -0,0 +1,55 @@ +<RCC> + <qresource prefix="/icons"> + <file>GrinderIcon.png</file> + <file>icons/door-exit.png</file> + <file>icons/question-mark-in-a-circle.png</file> + <file>icons/edit.png</file> + <file>icons/delete.png</file> + <file>icons/zoom-in.png</file> + <file>icons/zoom-orig.png</file> + <file>icons/zoom-out.png</file> + <file>icons/delete-sel-items.png</file> + <file>icons/select-all.png</file> + <file>icons/block.png</file> + <file>icons/arrow-shuffle.png</file> + <file>icons/map-route.png</file> + <file>icons/plus-sign.png</file> + <file>icons/map-of-roads.png</file> + <file>icons/find-text.png</file> + <file>icons/maximize-size-option.png</file> + <file>icons/layout.png</file> + <file>icons/start.png</file> + <file>icons/floppy-disk-digital-data-storage-or-save-interface-symbol.png</file> + <file>icons/folder-with-information.png</file> + <file>icons/document-empty.png</file> + <file>icons/open-window-with-gear-sign.png</file> + <file>icons/zoom-fit.png</file> + <file>icons/documents-empty.png</file> + <file>icons/arrow-down.png</file> + <file>icons/arrow-up.png</file> + <file>icons/painting/transform-box.png</file> + <file>icons/painting/line.png</file> + <file>icons/painting/eyedropper.png</file> + <file>icons/show-arrows.png</file> + <file>icons/show-tags.png</file> + <file>icons/drag.png</file> + <file>icons/arrowheads-of-thin-outline-to-the-left.png</file> + <file>icons/right-thin-arrowheads.png</file> + </qresource> + <qresource prefix="/"> + <file>css/global.css</file> + <file>css/controlBar.css</file> + <file>css/propertyDesc.css</file> + <file>css/blockList.css</file> + <file>css/imageEditorDockWidget.css</file> + <file>css/imageEditorProperties.css</file> + </qresource> + <qresource prefix="/misc"> + <file>misc/checkerboard.png</file> + </qresource> + <qresource prefix="/cursors"> + <file>cursors/box-cursor.png</file> + <file>cursors/line-cursor.png</file> + <file>cursors/picker-cursor.png</file> + </qresource> +</RCC> diff --git a/Grinder/res/Grinder.rc b/Grinder/res/Grinder.rc new file mode 100644 index 0000000000000000000000000000000000000000..c5d17a765ba5d57408bac45b1cb562df2556def2 --- /dev/null +++ b/Grinder/res/Grinder.rc @@ -0,0 +1,28 @@ +IDI_ICON1 ICON DISCARDABLE "Grinder.ico" + +1 VERSIONINFO +FILEVERSION 0,2,0,71 +PRODUCTVERSION 0,2,0,71 +FILEOS 0x4 +FILETYPE 0x0 +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904B0" + BEGIN + VALUE "CompanyName", "WWU Muenster" + VALUE "FileDescription", "Grinder" + VALUE "FileVersion", "0,2,0,71" + VALUE "InternalName", "Grinder" + VALUE "LegalCopyright", "Copyright (c) WWU Muenster" + VALUE "OriginalFilename", "Grinder.exe" + VALUE "ProductName", "Grinder" + VALUE "ProductVersion", "0,2,0,71" + END + END + + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0409, 0x04B0 + END +END diff --git a/Grinder/res/GrinderIcon.png b/Grinder/res/GrinderIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..144ec6e3e7c8c1df2b805240d02ac989acb5bb42 Binary files /dev/null and b/Grinder/res/GrinderIcon.png differ diff --git a/Grinder/res/Resources.h b/Grinder/res/Resources.h new file mode 100644 index 0000000000000000000000000000000000000000..6c083f38bde30d4d00355a9f241f7ac8732edb74 --- /dev/null +++ b/Grinder/res/Resources.h @@ -0,0 +1,60 @@ +/****************************************************************************** + * File: Resources.h + * Date: 08.3.2018 + *****************************************************************************/ + +#ifndef RESOURCES_H +#define RESOURCES_H + +/* Stylesheets */ + +#define FILE_STYLESHEET_GLOBAL "global.css" +#define FILE_STYLESHEET_CONTROLBAR "controlBar.css" +#define FILE_STYLESHEET_CAPTIONLABEL "captionLabel.css" +#define FILE_STYLESHEET_BLOCKLIST "blockList.css" +#define FILE_STYLESHEET_PROPERTYDESC "propertyDesc.css" +#define FILE_STYLESHEET_IMAGEEDITORDOCKWIDGET "imageEditorDockWidget.css" +#define FILE_STYLESHEET_IMAGEEDITORPROPERTIES "imageEditorProperties.css" + +/* Icons */ + +#define FILE_ICON_MAIN ":/icons/GrinderIcon.png" +#define FILE_ICON_BLOCK ":/icons/icons/block.png" +#define FILE_ICON_LABEL ":/icons/icons/map-route.png" +#define FILE_ICON_IMAGEREFERENCE ":/icons/icons/map-of-roads.png" +#define FILE_ICON_LAYER ":/icons/icons/documents-empty.png" + +#define FILE_ICON_ADD ":/icons/icons/plus-sign.png" +#define FILE_ICON_EDIT ":/icons/icons/edit.png" +#define FILE_ICON_DELETE ":/icons/icons/delete.png" +#define FILE_ICON_DELETE_SELECTED ":/icons/icons/delete-sel-items.png" +#define FILE_ICON_SELECTALL ":/icons/icons/select-all.png" + +#define FILE_ICON_MOVEUP ":/icons/icons/arrow-up.png" +#define FILE_ICON_MOVEDOWN ":/icons/icons/arrow-down.png" + +#define FILE_ICON_ACTIVATE ":/icons/icons/arrow-shuffle.png" +#define FILE_ICON_VIEWIMAGE ":/icons/icons/find-text.png" + +#define FILE_ICON_ZOOMIN ":/icons/icons/zoom-in.png" +#define FILE_ICON_ZOOMOUT ":/icons/icons/zoom-out.png" +#define FILE_ICON_ZOOMFULL ":/icons/icons/zoom-orig.png" +#define FILE_ICON_ZOOMFIT ":/icons/icons/zoom-fit.png" +#define FILE_ICON_LAYOUTGRAPH ":/icons/icons/layout.png" + +#define FILE_ICON_EDITOR_BACKGROUND ":/misc/misc/checkerboard.png" +#define FILE_ICON_EDITOR_NOIMAGE ":/icons/icons/question-mark-in-a-circle.png" +#define FILE_ICON_EDITOR_DEFAULT ":/icons/icons/drag.png" +#define FILE_ICON_EDITOR_LINE ":/icons/icons/painting/line.png" +#define FILE_ICON_EDITOR_BOX ":/icons/icons/painting/transform-box.png" +#define FILE_ICON_EDITOR_COLORPICKER ":/icons/icons/painting/eyedropper.png" +#define FILE_ICON_EDITOR_SHOWDIRECTIONS ":/icons/icons/show-arrows.png" +#define FILE_ICON_EDITOR_SHOWTAGS ":/icons/icons/show-tags.png" + +/* Cursors */ + +#define FILE_CURSOR_EDITOR_LINE ":/cursors/cursors/line-cursor.png" +#define FILE_CURSOR_EDITOR_BOX ":/cursors/cursors/box-cursor.png" +#define FILE_CURSOR_EDITOR_COLORPICKER ":/cursors/cursors/picker-cursor.png" + +#endif diff --git a/Grinder/res/css/blockList.css b/Grinder/res/css/blockList.css new file mode 100644 index 0000000000000000000000000000000000000000..1edbd6ecf28714b3f6c574fa3ee8f2bf6b75534e --- /dev/null +++ b/Grinder/res/css/blockList.css @@ -0,0 +1,16 @@ +#BlockStockpileCtrl +{ + border: none; + border-right: 1px solid %BORDER%; +} + +#BlockStockpileCtrl QLabel +{ + padding: 5px; +} + +#BlockStockpileCtrl QToolButton +{ + margin: 0px 2px 0px 2px; + padding: 2px; +} diff --git a/Grinder/res/css/controlBar.css b/Grinder/res/css/controlBar.css new file mode 100644 index 0000000000000000000000000000000000000000..2e68ac2c17a6c43fe3316b15d6635b038277238e --- /dev/null +++ b/Grinder/res/css/controlBar.css @@ -0,0 +1,50 @@ +QFrame +{ + background: %LIGHTBG%; + border: 1px solid %BORDER%; + padding: 0px; + margin: 0px; +} + +QFrame QToolButton +{ + margin-bottom: -1px; +} + +QFrame QFrame +{ + border: none; + border-right: 1px solid %LIGHTGRAY%; +} + +QFrame QLabel +{ + border: none; +} + +/* ControlBar inside a splitter */ + +QSplitter QFrame +{ + background: %LIGHTBG%; + border: 1px solid %BORDER%; + padding: 0px; + margin: 0px; +} + +QSplitter QFrame QToolButton +{ + margin-bottom: -1px; +} + + +QSplitter QFrame QFrame +{ + border: none; + border-right: 1px solid %LIGHTGRAY%; +} + +QSplitter QFrame QLabel +{ + border: none; +} diff --git a/Grinder/res/css/global.css b/Grinder/res/css/global.css new file mode 100644 index 0000000000000000000000000000000000000000..132351d5fb9a34696a50c7af18971373851e88da --- /dev/null +++ b/Grinder/res/css/global.css @@ -0,0 +1,42 @@ +/* ToolButton */ + +QToolButton +{ + padding: 2px; +} + +/* DockWidget */ + +QDockWidget +{ + titlebar-normal-icon: url(:/icons/icons/maximize-size-option.png); + titlebar-close-icon: url(:/icons/icons/delete.png); +} + +QDockWidget::title +{ + border: 1px solid %BORDER%; + background: %LIGHTGRAY%; + text-align: center; + margin: 1px; + padding: 2px; +} + +QDockWidget::close-button, QDockWidget::float-button +{ + margin: 1px; + icon-size: 16px; + subcontrol-position: top right; + subcontrol-origin: margin; + position: absolute; + top: 0px; right: 2px; bottom: 0px; + width: 14px; +} + +/* StatusBar */ + +QStatusBar +{ + border: none; + border-top: 1px solid %BORDER%; +} diff --git a/Grinder/res/css/imageEditorDockWidget.css b/Grinder/res/css/imageEditorDockWidget.css new file mode 100644 index 0000000000000000000000000000000000000000..5612e1509ccb87661b8877e59f7e491d630df9bb --- /dev/null +++ b/Grinder/res/css/imageEditorDockWidget.css @@ -0,0 +1,29 @@ +QDockWidget::close-button +{ + margin: 1px; + icon-size: 16px; + subcontrol-position: top right; + subcontrol-origin: margin; + position: absolute; + top: 0px; right: 2px; bottom: 0px; + width: 14px; +} + +QDockWidget::float-button +{ + margin: 1px; + icon-size: 16px; + subcontrol-position: top right; + subcontrol-origin: margin; + position: absolute; + top: 0px; right: 19px; bottom: 0px; + width: 14px; +} + +/* Inner dock widgets */ + +QDockWidget QDockWidget::title +{ + border: 1px solid %LIGHTGRAY%; + background: %LIGHTGRAY%; +} diff --git a/Grinder/res/css/imageEditorProperties.css b/Grinder/res/css/imageEditorProperties.css new file mode 100644 index 0000000000000000000000000000000000000000..1bbd69059ff399dcffb0199b19867a60f291fd19 --- /dev/null +++ b/Grinder/res/css/imageEditorProperties.css @@ -0,0 +1,10 @@ +QScrollArea +{ + background: %LIGHTBG%; + border: 1px solid %BORDER%; +} + +QScrollArea #imageEditorProperties +{ + background: %LIGHTBG%; +} diff --git a/Grinder/res/css/propertyDesc.css b/Grinder/res/css/propertyDesc.css new file mode 100644 index 0000000000000000000000000000000000000000..55ea46cee8e335ab2f495555a50aa1c74b0e3120 --- /dev/null +++ b/Grinder/res/css/propertyDesc.css @@ -0,0 +1,10 @@ +QFrame +{ + background: %LIGHTBG%; + border: 1px solid %BORDER%; +} + +QFrame QLabel +{ + border: none; +} diff --git a/Grinder/res/cursors/box-cursor.png b/Grinder/res/cursors/box-cursor.png new file mode 100644 index 0000000000000000000000000000000000000000..3b10e5c4a6768a7d5ad35098a3f08144ee5cc8e2 Binary files /dev/null and b/Grinder/res/cursors/box-cursor.png differ diff --git a/Grinder/res/cursors/click.png b/Grinder/res/cursors/click.png new file mode 100644 index 0000000000000000000000000000000000000000..49845813b4f830cbf876cb7673a80ace6fb8a983 Binary files /dev/null and b/Grinder/res/cursors/click.png differ diff --git a/Grinder/res/cursors/cursor-1.png b/Grinder/res/cursors/cursor-1.png new file mode 100644 index 0000000000000000000000000000000000000000..cc561c47c229ae9624bf432bb07ec94489531504 Binary files /dev/null and b/Grinder/res/cursors/cursor-1.png differ diff --git a/Grinder/res/cursors/cursor-2.png b/Grinder/res/cursors/cursor-2.png new file mode 100644 index 0000000000000000000000000000000000000000..acc80a518346802fcb799247b2cb547d2e1cc7b0 Binary files /dev/null and b/Grinder/res/cursors/cursor-2.png differ diff --git a/Grinder/res/cursors/cursor-3.png b/Grinder/res/cursors/cursor-3.png new file mode 100644 index 0000000000000000000000000000000000000000..9b927db195661a6436786d593321f28f96b8fcab Binary files /dev/null and b/Grinder/res/cursors/cursor-3.png differ diff --git a/Grinder/res/cursors/cursor-4.png b/Grinder/res/cursors/cursor-4.png new file mode 100644 index 0000000000000000000000000000000000000000..5e1e7e13b8c990923602ed4248a9e06499968853 Binary files /dev/null and b/Grinder/res/cursors/cursor-4.png differ diff --git a/Grinder/res/cursors/cursor-with-question-mark.png b/Grinder/res/cursors/cursor-with-question-mark.png new file mode 100644 index 0000000000000000000000000000000000000000..cef08e15d14a41cc2d0512a2d2d353b21d600c74 Binary files /dev/null and b/Grinder/res/cursors/cursor-with-question-mark.png differ diff --git a/Grinder/res/cursors/cursor.png b/Grinder/res/cursors/cursor.png new file mode 100644 index 0000000000000000000000000000000000000000..1f81e3774f78a82e9cc2e4a39c5c0904d43853e7 Binary files /dev/null and b/Grinder/res/cursors/cursor.png differ diff --git a/Grinder/res/cursors/drag-1.png b/Grinder/res/cursors/drag-1.png new file mode 100644 index 0000000000000000000000000000000000000000..aa8a0805026ca750d1fdf72228c0e27f8e847296 Binary files /dev/null and b/Grinder/res/cursors/drag-1.png differ diff --git a/Grinder/res/cursors/drag.png b/Grinder/res/cursors/drag.png new file mode 100644 index 0000000000000000000000000000000000000000..5bd67330efc010822a68f42e66383a34428e3b84 Binary files /dev/null and b/Grinder/res/cursors/drag.png differ diff --git a/Grinder/res/cursors/forbidden-cursor.png b/Grinder/res/cursors/forbidden-cursor.png new file mode 100644 index 0000000000000000000000000000000000000000..6528e75f804c37874ad53a2fd45c41ae760651be Binary files /dev/null and b/Grinder/res/cursors/forbidden-cursor.png differ diff --git a/Grinder/res/cursors/hand-move.png b/Grinder/res/cursors/hand-move.png new file mode 100644 index 0000000000000000000000000000000000000000..ee933f278ebab5c5e85a57f21192200fa7a67e4e Binary files /dev/null and b/Grinder/res/cursors/hand-move.png differ diff --git a/Grinder/res/cursors/line-cursor.png b/Grinder/res/cursors/line-cursor.png new file mode 100644 index 0000000000000000000000000000000000000000..27c7e6ef2b283a61b45c59cfc7e5bbdbc7d389f2 Binary files /dev/null and b/Grinder/res/cursors/line-cursor.png differ diff --git a/Grinder/res/cursors/move-arrows.png b/Grinder/res/cursors/move-arrows.png new file mode 100644 index 0000000000000000000000000000000000000000..3e0d5368b00b164562e6899e2a3844f5ac4756ae Binary files /dev/null and b/Grinder/res/cursors/move-arrows.png differ diff --git a/Grinder/res/cursors/navigation-arrows.png b/Grinder/res/cursors/navigation-arrows.png new file mode 100644 index 0000000000000000000000000000000000000000..b39503f86b10c7b1128572ea5da1b94105f48e85 Binary files /dev/null and b/Grinder/res/cursors/navigation-arrows.png differ diff --git a/Grinder/res/cursors/picker-cursor.png b/Grinder/res/cursors/picker-cursor.png new file mode 100644 index 0000000000000000000000000000000000000000..cf2f622e24c7c9ee11306b4b58f097236704f8b2 Binary files /dev/null and b/Grinder/res/cursors/picker-cursor.png differ diff --git a/Grinder/res/cursors/scroll.png b/Grinder/res/cursors/scroll.png new file mode 100644 index 0000000000000000000000000000000000000000..74656f060e1e9f7793f5d53b60e0c8e7de2cc39f Binary files /dev/null and b/Grinder/res/cursors/scroll.png differ diff --git a/Grinder/res/cursors/type.png b/Grinder/res/cursors/type.png new file mode 100644 index 0000000000000000000000000000000000000000..eaa024f85d9b89ce0e17eac25e5883ecb73762ce Binary files /dev/null and b/Grinder/res/cursors/type.png differ diff --git a/Grinder/res/cursors/wait-cursor.png b/Grinder/res/cursors/wait-cursor.png new file mode 100644 index 0000000000000000000000000000000000000000..8f02a0dcaf126bfb9ccd3d0605e15a880a7fac74 Binary files /dev/null and b/Grinder/res/cursors/wait-cursor.png differ diff --git a/Grinder/res/cursors/wait.png b/Grinder/res/cursors/wait.png new file mode 100644 index 0000000000000000000000000000000000000000..d9612a042691399ca49fe82d7a94b2977052fe75 Binary files /dev/null and b/Grinder/res/cursors/wait.png differ diff --git a/Grinder/res/cursors/waiting.png b/Grinder/res/cursors/waiting.png new file mode 100644 index 0000000000000000000000000000000000000000..3311c4348a792c90ab7bed316c7f4e7fb610e502 Binary files /dev/null and b/Grinder/res/cursors/waiting.png differ diff --git a/Grinder/res/cursors/zoom-in.png b/Grinder/res/cursors/zoom-in.png new file mode 100644 index 0000000000000000000000000000000000000000..0e6f71d5def02f657b9db2e42419ca393a7cc174 Binary files /dev/null and b/Grinder/res/cursors/zoom-in.png differ diff --git a/Grinder/res/cursors/zoom-out.png b/Grinder/res/cursors/zoom-out.png new file mode 100644 index 0000000000000000000000000000000000000000..c0fcf78f475855fe5165d0104f198bff19b472bd Binary files /dev/null and b/Grinder/res/cursors/zoom-out.png differ diff --git a/Grinder/res/icons/add-button-with-plus-symbol-in-a-black-circle.png b/Grinder/res/icons/add-button-with-plus-symbol-in-a-black-circle.png new file mode 100644 index 0000000000000000000000000000000000000000..452bc8d5afc20ed8c0c36baaa75aae44ba190df7 Binary files /dev/null and b/Grinder/res/icons/add-button-with-plus-symbol-in-a-black-circle.png differ diff --git a/Grinder/res/icons/arrow-circle.png b/Grinder/res/icons/arrow-circle.png new file mode 100644 index 0000000000000000000000000000000000000000..656c9894540220fdb7746cc90522b53a83281329 Binary files /dev/null and b/Grinder/res/icons/arrow-circle.png differ diff --git a/Grinder/res/icons/arrow-curve-pointing-left.png b/Grinder/res/icons/arrow-curve-pointing-left.png new file mode 100644 index 0000000000000000000000000000000000000000..e42ead034a3e5a33b467c791713a1d7a25b81b5e Binary files /dev/null and b/Grinder/res/icons/arrow-curve-pointing-left.png differ diff --git a/Grinder/res/icons/arrow-curving-around-a-circle.png b/Grinder/res/icons/arrow-curving-around-a-circle.png new file mode 100644 index 0000000000000000000000000000000000000000..bc5240764c45e5b71f363e7533bdefd9e6da1ac8 Binary files /dev/null and b/Grinder/res/icons/arrow-curving-around-a-circle.png differ diff --git a/Grinder/res/icons/arrow-down-left.png b/Grinder/res/icons/arrow-down-left.png new file mode 100644 index 0000000000000000000000000000000000000000..8e03bf1d52a187b304ad53894aa2decd2e7c0796 Binary files /dev/null and b/Grinder/res/icons/arrow-down-left.png differ diff --git a/Grinder/res/icons/arrow-down-sign-to-navigate.png b/Grinder/res/icons/arrow-down-sign-to-navigate.png new file mode 100644 index 0000000000000000000000000000000000000000..b48d5343c44a7ac397833568efcd2441f2c1c044 Binary files /dev/null and b/Grinder/res/icons/arrow-down-sign-to-navigate.png differ diff --git a/Grinder/res/icons/arrow-down.png b/Grinder/res/icons/arrow-down.png new file mode 100644 index 0000000000000000000000000000000000000000..df963828e00c1a4545ae39380c749d183dd087ef Binary files /dev/null and b/Grinder/res/icons/arrow-down.png differ diff --git a/Grinder/res/icons/arrow-fork.png b/Grinder/res/icons/arrow-fork.png new file mode 100644 index 0000000000000000000000000000000000000000..1536d5acfdefc5b01bc4b6f70217f4e406e30ee6 Binary files /dev/null and b/Grinder/res/icons/arrow-fork.png differ diff --git a/Grinder/res/icons/arrow-from.png b/Grinder/res/icons/arrow-from.png new file mode 100644 index 0000000000000000000000000000000000000000..25b131284419365a231b2f9038d15557802192cb Binary files /dev/null and b/Grinder/res/icons/arrow-from.png differ diff --git a/Grinder/res/icons/arrow-in-u-shape-to-turn.png b/Grinder/res/icons/arrow-in-u-shape-to-turn.png new file mode 100644 index 0000000000000000000000000000000000000000..ca42585db92fd97b465696f3ee9af45e53e926b3 Binary files /dev/null and b/Grinder/res/icons/arrow-in-u-shape-to-turn.png differ diff --git a/Grinder/res/icons/arrow-into-drive-symbol.png b/Grinder/res/icons/arrow-into-drive-symbol.png new file mode 100644 index 0000000000000000000000000000000000000000..c5866569a373eb2dc4f9818b53851e3d4339a2f0 Binary files /dev/null and b/Grinder/res/icons/arrow-into-drive-symbol.png differ diff --git a/Grinder/res/icons/arrow-join.png b/Grinder/res/icons/arrow-join.png new file mode 100644 index 0000000000000000000000000000000000000000..3900b97fa341f00dc6192776cae38e397cda20d3 Binary files /dev/null and b/Grinder/res/icons/arrow-join.png differ diff --git a/Grinder/res/icons/arrow-junction-one-to-the-left.png b/Grinder/res/icons/arrow-junction-one-to-the-left.png new file mode 100644 index 0000000000000000000000000000000000000000..f060c3ed7dc49bfb2a1921dbd3e547000779cd78 Binary files /dev/null and b/Grinder/res/icons/arrow-junction-one-to-the-left.png differ diff --git a/Grinder/res/icons/arrow-loop-1.png b/Grinder/res/icons/arrow-loop-1.png new file mode 100644 index 0000000000000000000000000000000000000000..ab199541af226d3b894b1e43cb8c5e7492b0b8fd Binary files /dev/null and b/Grinder/res/icons/arrow-loop-1.png differ diff --git a/Grinder/res/icons/arrow-loop-symbol.png b/Grinder/res/icons/arrow-loop-symbol.png new file mode 100644 index 0000000000000000000000000000000000000000..7bd3f52a2e8070a056cc39ccdb57b0237b3a67b9 Binary files /dev/null and b/Grinder/res/icons/arrow-loop-symbol.png differ diff --git a/Grinder/res/icons/arrow-loop.png b/Grinder/res/icons/arrow-loop.png new file mode 100644 index 0000000000000000000000000000000000000000..7f9fd5965acc3d9dfeb143148ab389df92a75658 Binary files /dev/null and b/Grinder/res/icons/arrow-loop.png differ diff --git a/Grinder/res/icons/arrow-merge-symbol.png b/Grinder/res/icons/arrow-merge-symbol.png new file mode 100644 index 0000000000000000000000000000000000000000..44bb1377b41b83c9dd6f2d60b328df7de8fdb240 Binary files /dev/null and b/Grinder/res/icons/arrow-merge-symbol.png differ diff --git a/Grinder/res/icons/arrow-navigate-close.png b/Grinder/res/icons/arrow-navigate-close.png new file mode 100644 index 0000000000000000000000000000000000000000..62ff5256c932f588620c3002538a2379abdfde2e Binary files /dev/null and b/Grinder/res/icons/arrow-navigate-close.png differ diff --git a/Grinder/res/icons/arrow-of-large-size-turning-to-the-left.png b/Grinder/res/icons/arrow-of-large-size-turning-to-the-left.png new file mode 100644 index 0000000000000000000000000000000000000000..4fe7626562a9f920e1feabba18b32b72340ab109 Binary files /dev/null and b/Grinder/res/icons/arrow-of-large-size-turning-to-the-left.png differ diff --git a/Grinder/res/icons/arrow-out.png b/Grinder/res/icons/arrow-out.png new file mode 100644 index 0000000000000000000000000000000000000000..30f15c09305e8e79af8b69e9317f31355ba78908 Binary files /dev/null and b/Grinder/res/icons/arrow-out.png differ diff --git a/Grinder/res/icons/arrow-over-a-rectangular-element.png b/Grinder/res/icons/arrow-over-a-rectangular-element.png new file mode 100644 index 0000000000000000000000000000000000000000..025f7ba837b3b278aead349231c5f0006c6114a3 Binary files /dev/null and b/Grinder/res/icons/arrow-over-a-rectangular-element.png differ diff --git a/Grinder/res/icons/arrow-point-to-down.png b/Grinder/res/icons/arrow-point-to-down.png new file mode 100644 index 0000000000000000000000000000000000000000..70bdeba7a59dc521dd9def75199f800a787e312f Binary files /dev/null and b/Grinder/res/icons/arrow-point-to-down.png differ diff --git a/Grinder/res/icons/arrow-point-to-right.png b/Grinder/res/icons/arrow-point-to-right.png new file mode 100644 index 0000000000000000000000000000000000000000..53537958db76edc69b26584600ffbeaaee232d1a Binary files /dev/null and b/Grinder/res/icons/arrow-point-to-right.png differ diff --git a/Grinder/res/icons/arrow-pointing-down-right-in-a-circle.png b/Grinder/res/icons/arrow-pointing-down-right-in-a-circle.png new file mode 100644 index 0000000000000000000000000000000000000000..5c43ca757c5b1f1586fc2a5441dea87961bb7d58 Binary files /dev/null and b/Grinder/res/icons/arrow-pointing-down-right-in-a-circle.png differ diff --git a/Grinder/res/icons/arrow-pointing-right-down.png b/Grinder/res/icons/arrow-pointing-right-down.png new file mode 100644 index 0000000000000000000000000000000000000000..21e190b23ac35569b5f1839b117f397ca82273e3 Binary files /dev/null and b/Grinder/res/icons/arrow-pointing-right-down.png differ diff --git a/Grinder/res/icons/arrow-pointing-to-right.png b/Grinder/res/icons/arrow-pointing-to-right.png new file mode 100644 index 0000000000000000000000000000000000000000..2cd649398ca6fc33020a02afc7c8a013bd209270 Binary files /dev/null and b/Grinder/res/icons/arrow-pointing-to-right.png differ diff --git a/Grinder/res/icons/arrow-pointing-up-left.png b/Grinder/res/icons/arrow-pointing-up-left.png new file mode 100644 index 0000000000000000000000000000000000000000..4ccbf1bd6b5748efe472c87c00076d078795a656 Binary files /dev/null and b/Grinder/res/icons/arrow-pointing-up-left.png differ diff --git a/Grinder/res/icons/arrow-shuffle.png b/Grinder/res/icons/arrow-shuffle.png new file mode 100644 index 0000000000000000000000000000000000000000..74f974e179b1fb8727fce0f5bb5b8469d450b769 Binary files /dev/null and b/Grinder/res/icons/arrow-shuffle.png differ diff --git a/Grinder/res/icons/arrow-spread-symbol.png b/Grinder/res/icons/arrow-spread-symbol.png new file mode 100644 index 0000000000000000000000000000000000000000..0746e694e6d616ad1281163784cf84de7198c271 Binary files /dev/null and b/Grinder/res/icons/arrow-spread-symbol.png differ diff --git a/Grinder/res/icons/arrow-squiggly.png b/Grinder/res/icons/arrow-squiggly.png new file mode 100644 index 0000000000000000000000000000000000000000..a972632fe6823ae3ee6203dcbe9d08e1a015a0c5 Binary files /dev/null and b/Grinder/res/icons/arrow-squiggly.png differ diff --git a/Grinder/res/icons/arrow-through.png b/Grinder/res/icons/arrow-through.png new file mode 100644 index 0000000000000000000000000000000000000000..23fc8834dfba8999c62e5223248fc2c1d141a7c7 Binary files /dev/null and b/Grinder/res/icons/arrow-through.png differ diff --git a/Grinder/res/icons/arrow-to-the-left-silhouette.png b/Grinder/res/icons/arrow-to-the-left-silhouette.png new file mode 100644 index 0000000000000000000000000000000000000000..252f0509e35ccc8c8c32a55b69c3ac01116bbd00 Binary files /dev/null and b/Grinder/res/icons/arrow-to-the-left-silhouette.png differ diff --git a/Grinder/res/icons/arrow-to-the-right-navigation.png b/Grinder/res/icons/arrow-to-the-right-navigation.png new file mode 100644 index 0000000000000000000000000000000000000000..6ff683adbd20e1bc5f465f962da08861ae5ce493 Binary files /dev/null and b/Grinder/res/icons/arrow-to-the-right-navigation.png differ diff --git a/Grinder/res/icons/arrow-to.png b/Grinder/res/icons/arrow-to.png new file mode 100644 index 0000000000000000000000000000000000000000..ba46e853c2995bee8e666b7e068956f858e62d30 Binary files /dev/null and b/Grinder/res/icons/arrow-to.png differ diff --git a/Grinder/res/icons/arrow-up-in-a-circle.png b/Grinder/res/icons/arrow-up-in-a-circle.png new file mode 100644 index 0000000000000000000000000000000000000000..fa416c4c9dbde4d0dad1f9eb934478fdfe1e7cf9 Binary files /dev/null and b/Grinder/res/icons/arrow-up-in-a-circle.png differ diff --git a/Grinder/res/icons/arrow-up-right.png b/Grinder/res/icons/arrow-up-right.png new file mode 100644 index 0000000000000000000000000000000000000000..526f04400afdd42eb69a56d5cf3145706f86aaad Binary files /dev/null and b/Grinder/res/icons/arrow-up-right.png differ diff --git a/Grinder/res/icons/arrow-up.png b/Grinder/res/icons/arrow-up.png new file mode 100644 index 0000000000000000000000000000000000000000..f08776cb9d5048e547ecf9ef44eea31e3750a324 Binary files /dev/null and b/Grinder/res/icons/arrow-up.png differ diff --git a/Grinder/res/icons/arrow-upward-to-rectangle-shape.png b/Grinder/res/icons/arrow-upward-to-rectangle-shape.png new file mode 100644 index 0000000000000000000000000000000000000000..5105b110771efdba49c289ccc8cf82639b9a50a5 Binary files /dev/null and b/Grinder/res/icons/arrow-upward-to-rectangle-shape.png differ diff --git a/Grinder/res/icons/arrowhead-thin-outline-to-the-left.png b/Grinder/res/icons/arrowhead-thin-outline-to-the-left.png new file mode 100644 index 0000000000000000000000000000000000000000..1bab84b37403016c605e013326bc5a58f920ee5c Binary files /dev/null and b/Grinder/res/icons/arrowhead-thin-outline-to-the-left.png differ diff --git a/Grinder/res/icons/arrowheads-of-thin-outline-to-the-left.png b/Grinder/res/icons/arrowheads-of-thin-outline-to-the-left.png new file mode 100644 index 0000000000000000000000000000000000000000..054b282f20e4306c7da21352e7331721546f983a Binary files /dev/null and b/Grinder/res/icons/arrowheads-of-thin-outline-to-the-left.png differ diff --git a/Grinder/res/icons/arrows-circle.png b/Grinder/res/icons/arrows-circle.png new file mode 100644 index 0000000000000000000000000000000000000000..7a85e857748112e59cb75659c9762f9c77bf1a40 Binary files /dev/null and b/Grinder/res/icons/arrows-circle.png differ diff --git a/Grinder/res/icons/arrows-group-interface-symbol-to-expand.png b/Grinder/res/icons/arrows-group-interface-symbol-to-expand.png new file mode 100644 index 0000000000000000000000000000000000000000..aac515fa06793e0726d90efb3808443555c751c3 Binary files /dev/null and b/Grinder/res/icons/arrows-group-interface-symbol-to-expand.png differ diff --git a/Grinder/res/icons/arrows-merge-pointing-to-right.png b/Grinder/res/icons/arrows-merge-pointing-to-right.png new file mode 100644 index 0000000000000000000000000000000000000000..e2a62652912ff046113f8eab90f3e54dc4da696e Binary files /dev/null and b/Grinder/res/icons/arrows-merge-pointing-to-right.png differ diff --git a/Grinder/res/icons/arrows-mix.png b/Grinder/res/icons/arrows-mix.png new file mode 100644 index 0000000000000000000000000000000000000000..2040682afa85a7a3f81e55320b458e67c9c2ede9 Binary files /dev/null and b/Grinder/res/icons/arrows-mix.png differ diff --git a/Grinder/res/icons/back-navigational-arrow-button-pointing-to-left.png b/Grinder/res/icons/back-navigational-arrow-button-pointing-to-left.png new file mode 100644 index 0000000000000000000000000000000000000000..1f45a7f13578ad18c8f1e71e7928ee79e55d2ca7 Binary files /dev/null and b/Grinder/res/icons/back-navigational-arrow-button-pointing-to-left.png differ diff --git a/Grinder/res/icons/barrier-closed.png b/Grinder/res/icons/barrier-closed.png new file mode 100644 index 0000000000000000000000000000000000000000..421be351c033c999dc14ac0cab96b0a21c4e8562 Binary files /dev/null and b/Grinder/res/icons/barrier-closed.png differ diff --git a/Grinder/res/icons/barrier-open.png b/Grinder/res/icons/barrier-open.png new file mode 100644 index 0000000000000000000000000000000000000000..a98e97906595257ead627c22501ac5a02d7ba2bc Binary files /dev/null and b/Grinder/res/icons/barrier-open.png differ diff --git a/Grinder/res/icons/basic-text-format.png b/Grinder/res/icons/basic-text-format.png new file mode 100644 index 0000000000000000000000000000000000000000..8c632cc0cb5dacc6e1a8f1b7cb7eb072fa0dcc1f Binary files /dev/null and b/Grinder/res/icons/basic-text-format.png differ diff --git a/Grinder/res/icons/basic-window-appearance.png b/Grinder/res/icons/basic-window-appearance.png new file mode 100644 index 0000000000000000000000000000000000000000..6425c8f9b1a3bfd86bac0429798771e9c8e18a5b Binary files /dev/null and b/Grinder/res/icons/basic-window-appearance.png differ diff --git a/Grinder/res/icons/bell-black-shape.png b/Grinder/res/icons/bell-black-shape.png new file mode 100644 index 0000000000000000000000000000000000000000..450306db6fb85962684f2852a851bca855fa4df1 Binary files /dev/null and b/Grinder/res/icons/bell-black-shape.png differ diff --git a/Grinder/res/icons/biohazard-sign-inside-a-triangle-outline.png b/Grinder/res/icons/biohazard-sign-inside-a-triangle-outline.png new file mode 100644 index 0000000000000000000000000000000000000000..57caa01ee8bf451cf40f0e56523d2e12008ccd5b Binary files /dev/null and b/Grinder/res/icons/biohazard-sign-inside-a-triangle-outline.png differ diff --git a/Grinder/res/icons/blank-window-with-key.png b/Grinder/res/icons/blank-window-with-key.png new file mode 100644 index 0000000000000000000000000000000000000000..f1e445ee64f9c6e2afc4751e27ab4b9ff56e3dda Binary files /dev/null and b/Grinder/res/icons/blank-window-with-key.png differ diff --git a/Grinder/res/icons/block.png b/Grinder/res/icons/block.png new file mode 100644 index 0000000000000000000000000000000000000000..fcb668e90f3931cd6f360267ee7e3d6b386226b7 Binary files /dev/null and b/Grinder/res/icons/block.png differ diff --git a/Grinder/res/icons/book-closed-with-black-cover.png b/Grinder/res/icons/book-closed-with-black-cover.png new file mode 100644 index 0000000000000000000000000000000000000000..199a2991083bdb83358f796ef06673aa80120c7f Binary files /dev/null and b/Grinder/res/icons/book-closed-with-black-cover.png differ diff --git a/Grinder/res/icons/book-of-black-cover-closed.png b/Grinder/res/icons/book-of-black-cover-closed.png new file mode 100644 index 0000000000000000000000000000000000000000..f117f744d9d056f7be99c39aa8e140d340cd9148 Binary files /dev/null and b/Grinder/res/icons/book-of-black-cover-closed.png differ diff --git a/Grinder/res/icons/book-open-in-the-middle.png b/Grinder/res/icons/book-open-in-the-middle.png new file mode 100644 index 0000000000000000000000000000000000000000..b8cacd00030688c673accde74859c9040e33f0ec Binary files /dev/null and b/Grinder/res/icons/book-open-in-the-middle.png differ diff --git a/Grinder/res/icons/book-with-headphones-symbol.png b/Grinder/res/icons/book-with-headphones-symbol.png new file mode 100644 index 0000000000000000000000000000000000000000..5b1787b1635e14a242460a8a10cee3b03a882ed4 Binary files /dev/null and b/Grinder/res/icons/book-with-headphones-symbol.png differ diff --git a/Grinder/res/icons/book-with-white-bookmark.png b/Grinder/res/icons/book-with-white-bookmark.png new file mode 100644 index 0000000000000000000000000000000000000000..fbca9459c6c650adadd915a766671ff7a923a8f1 Binary files /dev/null and b/Grinder/res/icons/book-with-white-bookmark.png differ diff --git a/Grinder/res/icons/bookmark-silhouette-variant.png b/Grinder/res/icons/bookmark-silhouette-variant.png new file mode 100644 index 0000000000000000000000000000000000000000..380651f3f68bb7aa2edfe369d27c3f9a8831819c Binary files /dev/null and b/Grinder/res/icons/bookmark-silhouette-variant.png differ diff --git a/Grinder/res/icons/bookmarks.png b/Grinder/res/icons/bookmarks.png new file mode 100644 index 0000000000000000000000000000000000000000..c6f30ed807b28caa0cb1d414179c11d3bb9064cc Binary files /dev/null and b/Grinder/res/icons/bookmarks.png differ diff --git a/Grinder/res/icons/books-overlapping-arrangement.png b/Grinder/res/icons/books-overlapping-arrangement.png new file mode 100644 index 0000000000000000000000000000000000000000..ec24e8cf6892c38604e1ac8871fb84910cbf177d Binary files /dev/null and b/Grinder/res/icons/books-overlapping-arrangement.png differ diff --git a/Grinder/res/icons/braille-text.png b/Grinder/res/icons/braille-text.png new file mode 100644 index 0000000000000000000000000000000000000000..433d460fb5a9fc84aebab8e54d86c45707459f69 Binary files /dev/null and b/Grinder/res/icons/braille-text.png differ diff --git a/Grinder/res/icons/broken-heart-silhouette-shape.png b/Grinder/res/icons/broken-heart-silhouette-shape.png new file mode 100644 index 0000000000000000000000000000000000000000..bbb9c0c4d03589f174b84c83645f112e26dc7598 Binary files /dev/null and b/Grinder/res/icons/broken-heart-silhouette-shape.png differ diff --git a/Grinder/res/icons/buoy.png b/Grinder/res/icons/buoy.png new file mode 100644 index 0000000000000000000000000000000000000000..3cba764bacb7c3cc8372730ac59ee1aefaec575b Binary files /dev/null and b/Grinder/res/icons/buoy.png differ diff --git a/Grinder/res/icons/calibration-mark.png b/Grinder/res/icons/calibration-mark.png new file mode 100644 index 0000000000000000000000000000000000000000..1a03cd23edd1cc6b38c8c108505257c01f3870e6 Binary files /dev/null and b/Grinder/res/icons/calibration-mark.png differ diff --git a/Grinder/res/icons/candle-burning.png b/Grinder/res/icons/candle-burning.png new file mode 100644 index 0000000000000000000000000000000000000000..6ea5ae6e33f20353a1a3a1829bd2bf239b7013aa Binary files /dev/null and b/Grinder/res/icons/candle-burning.png differ diff --git a/Grinder/res/icons/candle-holder-with-candle.png b/Grinder/res/icons/candle-holder-with-candle.png new file mode 100644 index 0000000000000000000000000000000000000000..b363326d8d29d57dac947b9ffb2b0764056875a1 Binary files /dev/null and b/Grinder/res/icons/candle-holder-with-candle.png differ diff --git a/Grinder/res/icons/cd-burning-application.png b/Grinder/res/icons/cd-burning-application.png new file mode 100644 index 0000000000000000000000000000000000000000..94fe0a5790feb757724db8f9c40349098e8f94eb Binary files /dev/null and b/Grinder/res/icons/cd-burning-application.png differ diff --git a/Grinder/res/icons/cd-case.png b/Grinder/res/icons/cd-case.png new file mode 100644 index 0000000000000000000000000000000000000000..931be7d917a6fce0459d98fe28b02db1c963dc1a Binary files /dev/null and b/Grinder/res/icons/cd-case.png differ diff --git a/Grinder/res/icons/cd-drive.png b/Grinder/res/icons/cd-drive.png new file mode 100644 index 0000000000000000000000000000000000000000..bbcb1ffda3a085ff60ba17a6b418d791fab04df6 Binary files /dev/null and b/Grinder/res/icons/cd-drive.png differ diff --git a/Grinder/res/icons/cd-pirated-interface-symbol-for-piracy.png b/Grinder/res/icons/cd-pirated-interface-symbol-for-piracy.png new file mode 100644 index 0000000000000000000000000000000000000000..5486cae1fa20c2c773c9d43948abf6a54ca9678a Binary files /dev/null and b/Grinder/res/icons/cd-pirated-interface-symbol-for-piracy.png differ diff --git a/Grinder/res/icons/cd-window.png b/Grinder/res/icons/cd-window.png new file mode 100644 index 0000000000000000000000000000000000000000..0adb88e1cbafddcacd746d2e7f373394c00c969d Binary files /dev/null and b/Grinder/res/icons/cd-window.png differ diff --git a/Grinder/res/icons/cd-with-open-window.png b/Grinder/res/icons/cd-with-open-window.png new file mode 100644 index 0000000000000000000000000000000000000000..9c1b8505bbe828180b4988ed831e124e49dd6489 Binary files /dev/null and b/Grinder/res/icons/cd-with-open-window.png differ diff --git a/Grinder/res/icons/check-mark-black-outline.png b/Grinder/res/icons/check-mark-black-outline.png new file mode 100644 index 0000000000000000000000000000000000000000..db1dea847f46984aee2de4130a2c2ea634ad053f Binary files /dev/null and b/Grinder/res/icons/check-mark-black-outline.png differ diff --git a/Grinder/res/icons/check.png b/Grinder/res/icons/check.png new file mode 100644 index 0000000000000000000000000000000000000000..4eefbedd9438d6581db0fc0c3c09af75d39fc5a3 Binary files /dev/null and b/Grinder/res/icons/check.png differ diff --git a/Grinder/res/icons/checkered-racing-flag-variant.png b/Grinder/res/icons/checkered-racing-flag-variant.png new file mode 100644 index 0000000000000000000000000000000000000000..8b0a3608bcd499da8f5186a0f1c2fba786ff166d Binary files /dev/null and b/Grinder/res/icons/checkered-racing-flag-variant.png differ diff --git a/Grinder/res/icons/circle-outline-with-exclamation-point.png b/Grinder/res/icons/circle-outline-with-exclamation-point.png new file mode 100644 index 0000000000000000000000000000000000000000..703087867e58b12d9324f7ff64e23317b508905e Binary files /dev/null and b/Grinder/res/icons/circle-outline-with-exclamation-point.png differ diff --git a/Grinder/res/icons/circle-outline.png b/Grinder/res/icons/circle-outline.png new file mode 100644 index 0000000000000000000000000000000000000000..c8f2833c749e7b86b68e769ea402dd87ae10e188 Binary files /dev/null and b/Grinder/res/icons/circle-outline.png differ diff --git a/Grinder/res/icons/clipboard-empty-in-black.png b/Grinder/res/icons/clipboard-empty-in-black.png new file mode 100644 index 0000000000000000000000000000000000000000..24659fd9d59e955e4420b934065fdb9740a01f67 Binary files /dev/null and b/Grinder/res/icons/clipboard-empty-in-black.png differ diff --git a/Grinder/res/icons/clipboard-paste-option.png b/Grinder/res/icons/clipboard-paste-option.png new file mode 100644 index 0000000000000000000000000000000000000000..3f806b8375ef334f9c6484fcf822d3650f8cee2e Binary files /dev/null and b/Grinder/res/icons/clipboard-paste-option.png differ diff --git a/Grinder/res/icons/clipboard-paste-with-no-format.png b/Grinder/res/icons/clipboard-paste-with-no-format.png new file mode 100644 index 0000000000000000000000000000000000000000..c49e9168acfae187f9ce5d811012bd3de96534a0 Binary files /dev/null and b/Grinder/res/icons/clipboard-paste-with-no-format.png differ diff --git a/Grinder/res/icons/clipboard-variant-with-lists-and-checks.png b/Grinder/res/icons/clipboard-variant-with-lists-and-checks.png new file mode 100644 index 0000000000000000000000000000000000000000..319d98f06ca48a3cfbe06fa0ec4794e6969a328f Binary files /dev/null and b/Grinder/res/icons/clipboard-variant-with-lists-and-checks.png differ diff --git a/Grinder/res/icons/clipboard-variant-with-pencil-and-check-mark-variant.png b/Grinder/res/icons/clipboard-variant-with-pencil-and-check-mark-variant.png new file mode 100644 index 0000000000000000000000000000000000000000..0d3776b2a4ccfca3249136f116875c1649e1b0f6 Binary files /dev/null and b/Grinder/res/icons/clipboard-variant-with-pencil-and-check-mark-variant.png differ diff --git a/Grinder/res/icons/clipboard.png b/Grinder/res/icons/clipboard.png new file mode 100644 index 0000000000000000000000000000000000000000..fa1da1808c2500fab13af7f9c79025ed83b32283 Binary files /dev/null and b/Grinder/res/icons/clipboard.png differ diff --git a/Grinder/res/icons/close.png b/Grinder/res/icons/close.png new file mode 100644 index 0000000000000000000000000000000000000000..fe2aefc84df3cdb284b62b4261a8932e1b97bf95 Binary files /dev/null and b/Grinder/res/icons/close.png differ diff --git a/Grinder/res/icons/closed-door-with-border-silhouette.png b/Grinder/res/icons/closed-door-with-border-silhouette.png new file mode 100644 index 0000000000000000000000000000000000000000..fa5e9dc0a13c7f91ca4c94855b715b109ef4e1c1 Binary files /dev/null and b/Grinder/res/icons/closed-door-with-border-silhouette.png differ diff --git a/Grinder/res/icons/compact-disc-variant-with-border.png b/Grinder/res/icons/compact-disc-variant-with-border.png new file mode 100644 index 0000000000000000000000000000000000000000..2476aca0c833f4847d31d1804547a2ce997c94ef Binary files /dev/null and b/Grinder/res/icons/compact-disc-variant-with-border.png differ diff --git a/Grinder/res/icons/copy-documents-option.png b/Grinder/res/icons/copy-documents-option.png new file mode 100644 index 0000000000000000000000000000000000000000..0eb0af7437b252608bf00f056d8e20fd281da727 Binary files /dev/null and b/Grinder/res/icons/copy-documents-option.png differ diff --git a/Grinder/res/icons/cross-variant-with-arrow-edges.png b/Grinder/res/icons/cross-variant-with-arrow-edges.png new file mode 100644 index 0000000000000000000000000000000000000000..71cf0e1c041c71012f2cc207debed41830920ab0 Binary files /dev/null and b/Grinder/res/icons/cross-variant-with-arrow-edges.png differ diff --git a/Grinder/res/icons/crosshair-variant-with-navigation-arrows.png b/Grinder/res/icons/crosshair-variant-with-navigation-arrows.png new file mode 100644 index 0000000000000000000000000000000000000000..955bdc1218fb46bbf538ccf0d9ef1ac447677b9a Binary files /dev/null and b/Grinder/res/icons/crosshair-variant-with-navigation-arrows.png differ diff --git a/Grinder/res/icons/curved-arrow-to-the-right.png b/Grinder/res/icons/curved-arrow-to-the-right.png new file mode 100644 index 0000000000000000000000000000000000000000..dddfce7efdbf563b95dc747b770057af348de50c Binary files /dev/null and b/Grinder/res/icons/curved-arrow-to-the-right.png differ diff --git a/Grinder/res/icons/cut.png b/Grinder/res/icons/cut.png new file mode 100644 index 0000000000000000000000000000000000000000..a1146c814544bc28a1bc7a9de41d5ad5daf6c642 Binary files /dev/null and b/Grinder/res/icons/cut.png differ diff --git a/Grinder/res/icons/delete-sel-items.png b/Grinder/res/icons/delete-sel-items.png new file mode 100644 index 0000000000000000000000000000000000000000..a1285c18b11b643ffb83c596c142cb5454e8d1b9 Binary files /dev/null and b/Grinder/res/icons/delete-sel-items.png differ diff --git a/Grinder/res/icons/delete.png b/Grinder/res/icons/delete.png new file mode 100644 index 0000000000000000000000000000000000000000..4416877f859255330ee8b985f2adddbd014c74e9 Binary files /dev/null and b/Grinder/res/icons/delete.png differ diff --git a/Grinder/res/icons/dictionary-book-with-letters-a-to-z.png b/Grinder/res/icons/dictionary-book-with-letters-a-to-z.png new file mode 100644 index 0000000000000000000000000000000000000000..9495fd781a5d930a965ab3e8fce95f2dc947e2a7 Binary files /dev/null and b/Grinder/res/icons/dictionary-book-with-letters-a-to-z.png differ diff --git a/Grinder/res/icons/document-center.png b/Grinder/res/icons/document-center.png new file mode 100644 index 0000000000000000000000000000000000000000..3a301e968b9ba073da519a5200b299e8b8e0e99a Binary files /dev/null and b/Grinder/res/icons/document-center.png differ diff --git a/Grinder/res/icons/document-centering-horizontally.png b/Grinder/res/icons/document-centering-horizontally.png new file mode 100644 index 0000000000000000000000000000000000000000..22b06bee3d4760e9d1f70c0024a71176bb2bd8a2 Binary files /dev/null and b/Grinder/res/icons/document-centering-horizontally.png differ diff --git a/Grinder/res/icons/document-empty-horizontal-page.png b/Grinder/res/icons/document-empty-horizontal-page.png new file mode 100644 index 0000000000000000000000000000000000000000..cb7f699098ca2f5ddbc519939f8d32b5af163216 Binary files /dev/null and b/Grinder/res/icons/document-empty-horizontal-page.png differ diff --git a/Grinder/res/icons/document-empty.png b/Grinder/res/icons/document-empty.png new file mode 100644 index 0000000000000000000000000000000000000000..557509ce52c833bc278c13fc8d301a05b674d49e Binary files /dev/null and b/Grinder/res/icons/document-empty.png differ diff --git a/Grinder/res/icons/document-file-with-line.png b/Grinder/res/icons/document-file-with-line.png new file mode 100644 index 0000000000000000000000000000000000000000..9445dc4ce5bdb2512b8148bb4b52ad4695c96fc4 Binary files /dev/null and b/Grinder/res/icons/document-file-with-line.png differ diff --git a/Grinder/res/icons/document-footer.png b/Grinder/res/icons/document-footer.png new file mode 100644 index 0000000000000000000000000000000000000000..1ce335f9cea6fae4a1e654e6ca093c2efa5a05f7 Binary files /dev/null and b/Grinder/res/icons/document-footer.png differ diff --git a/Grinder/res/icons/document-header.png b/Grinder/res/icons/document-header.png new file mode 100644 index 0000000000000000000000000000000000000000..242071eb74b88607928981e8cb1a2a00487c2221 Binary files /dev/null and b/Grinder/res/icons/document-header.png differ diff --git a/Grinder/res/icons/document-height-adjustment.png b/Grinder/res/icons/document-height-adjustment.png new file mode 100644 index 0000000000000000000000000000000000000000..efffbc45eb082e75dcd987fb348e40437de068ca Binary files /dev/null and b/Grinder/res/icons/document-height-adjustment.png differ diff --git a/Grinder/res/icons/document-landscape-orientation.png b/Grinder/res/icons/document-landscape-orientation.png new file mode 100644 index 0000000000000000000000000000000000000000..9413e3b57c31eb02f4244211c73ff1eac9a7978f Binary files /dev/null and b/Grinder/res/icons/document-landscape-orientation.png differ diff --git a/Grinder/res/icons/document-page-number.png b/Grinder/res/icons/document-page-number.png new file mode 100644 index 0000000000000000000000000000000000000000..e1324f1c670552e475c95cb6e2398ce2c22b8212 Binary files /dev/null and b/Grinder/res/icons/document-page-number.png differ diff --git a/Grinder/res/icons/document-pinned.png b/Grinder/res/icons/document-pinned.png new file mode 100644 index 0000000000000000000000000000000000000000..55d010d2d12cd4d716bc914d7ed31cf7040c1b4e Binary files /dev/null and b/Grinder/res/icons/document-pinned.png differ diff --git a/Grinder/res/icons/document-size.png b/Grinder/res/icons/document-size.png new file mode 100644 index 0000000000000000000000000000000000000000..beaf6ae0ecf01ea175193b730c1e150baeb6ef0a Binary files /dev/null and b/Grinder/res/icons/document-size.png differ diff --git a/Grinder/res/icons/document-tag-interface-symbol.png b/Grinder/res/icons/document-tag-interface-symbol.png new file mode 100644 index 0000000000000000000000000000000000000000..fbda34dd5e3ddaf953cc3b772b1b2043d45a2cc4 Binary files /dev/null and b/Grinder/res/icons/document-tag-interface-symbol.png differ diff --git a/Grinder/res/icons/document-vertical-center-alignment.png b/Grinder/res/icons/document-vertical-center-alignment.png new file mode 100644 index 0000000000000000000000000000000000000000..8baec09342013a7f883ae9b73c30829e9ffbc933 Binary files /dev/null and b/Grinder/res/icons/document-vertical-center-alignment.png differ diff --git a/Grinder/res/icons/document-width.png b/Grinder/res/icons/document-width.png new file mode 100644 index 0000000000000000000000000000000000000000..cf2d99150580d168cc0a80d16feca9a2691c8034 Binary files /dev/null and b/Grinder/res/icons/document-width.png differ diff --git a/Grinder/res/icons/document-with-a-cup.png b/Grinder/res/icons/document-with-a-cup.png new file mode 100644 index 0000000000000000000000000000000000000000..280e27018718a549cf77662490317c1db3150678 Binary files /dev/null and b/Grinder/res/icons/document-with-a-cup.png differ diff --git a/Grinder/res/icons/document-with-gear.png b/Grinder/res/icons/document-with-gear.png new file mode 100644 index 0000000000000000000000000000000000000000..d778b9d92f16d80aa957a5566a374d0d3bf5467b Binary files /dev/null and b/Grinder/res/icons/document-with-gear.png differ diff --git a/Grinder/res/icons/document-with-irregular-line.png b/Grinder/res/icons/document-with-irregular-line.png new file mode 100644 index 0000000000000000000000000000000000000000..6dd4ec8506d71bf63f50b3534cb48fef33bf56ea Binary files /dev/null and b/Grinder/res/icons/document-with-irregular-line.png differ diff --git a/Grinder/res/icons/document-with-line-chart.png b/Grinder/res/icons/document-with-line-chart.png new file mode 100644 index 0000000000000000000000000000000000000000..c2cd75be922f0e8e71d5a31c72b335f939864889 Binary files /dev/null and b/Grinder/res/icons/document-with-line-chart.png differ diff --git a/Grinder/res/icons/document-with-paper-clip.png b/Grinder/res/icons/document-with-paper-clip.png new file mode 100644 index 0000000000000000000000000000000000000000..044e2bbebe81db35f035cdf5717c0b7651f3d125 Binary files /dev/null and b/Grinder/res/icons/document-with-paper-clip.png differ diff --git a/Grinder/res/icons/document-with-selection-box.png b/Grinder/res/icons/document-with-selection-box.png new file mode 100644 index 0000000000000000000000000000000000000000..8ed3bff15fa74349a591a5f550dd33a10b56c19b Binary files /dev/null and b/Grinder/res/icons/document-with-selection-box.png differ diff --git a/Grinder/res/icons/document-with-speaker.png b/Grinder/res/icons/document-with-speaker.png new file mode 100644 index 0000000000000000000000000000000000000000..26eddb061b71773a4e1fffb398c5d05253c14e56 Binary files /dev/null and b/Grinder/res/icons/document-with-speaker.png differ diff --git a/Grinder/res/icons/documents-empty.png b/Grinder/res/icons/documents-empty.png new file mode 100644 index 0000000000000000000000000000000000000000..d1cebaea87ac9cece55c93067bf7d0309a6cfa25 Binary files /dev/null and b/Grinder/res/icons/documents-empty.png differ diff --git a/Grinder/res/icons/documents-exchange.png b/Grinder/res/icons/documents-exchange.png new file mode 100644 index 0000000000000000000000000000000000000000..013639b86ef6e77d521ef943fe2d484f6468ff5d Binary files /dev/null and b/Grinder/res/icons/documents-exchange.png differ diff --git a/Grinder/res/icons/documents-with-a-heart-symbol.png b/Grinder/res/icons/documents-with-a-heart-symbol.png new file mode 100644 index 0000000000000000000000000000000000000000..e60d5a9254fea6efbb308a61613da36a33b71925 Binary files /dev/null and b/Grinder/res/icons/documents-with-a-heart-symbol.png differ diff --git a/Grinder/res/icons/door-exit.png b/Grinder/res/icons/door-exit.png new file mode 100644 index 0000000000000000000000000000000000000000..e99db497fe6f9d86d01b348e354c603cd18320ef Binary files /dev/null and b/Grinder/res/icons/door-exit.png differ diff --git a/Grinder/res/icons/double-curve-arrow-to-the-right.png b/Grinder/res/icons/double-curve-arrow-to-the-right.png new file mode 100644 index 0000000000000000000000000000000000000000..281f2deb00d41bd417c6535ec03d4a2e00e51258 Binary files /dev/null and b/Grinder/res/icons/double-curve-arrow-to-the-right.png differ diff --git a/Grinder/res/icons/drag.png b/Grinder/res/icons/drag.png new file mode 100644 index 0000000000000000000000000000000000000000..af20596adae17c564274d4c6a44710810762902d Binary files /dev/null and b/Grinder/res/icons/drag.png differ diff --git a/Grinder/res/icons/edit.png b/Grinder/res/icons/edit.png new file mode 100644 index 0000000000000000000000000000000000000000..050e2fd65b0a1d8367b083640a6215ecef561ccc Binary files /dev/null and b/Grinder/res/icons/edit.png differ diff --git a/Grinder/res/icons/equalizer-tool-on-open-window.png b/Grinder/res/icons/equalizer-tool-on-open-window.png new file mode 100644 index 0000000000000000000000000000000000000000..e8b08ba874c75cfc37cb4a313f9f6fb61d080398 Binary files /dev/null and b/Grinder/res/icons/equalizer-tool-on-open-window.png differ diff --git a/Grinder/res/icons/escalator-down.png b/Grinder/res/icons/escalator-down.png new file mode 100644 index 0000000000000000000000000000000000000000..4eaff082292e51a17696c3cfc8e0ebfe68f5e824 Binary files /dev/null and b/Grinder/res/icons/escalator-down.png differ diff --git a/Grinder/res/icons/escalator-silhouette-symbol.png b/Grinder/res/icons/escalator-silhouette-symbol.png new file mode 100644 index 0000000000000000000000000000000000000000..c0971d97711b4fdaa5d2f1a898c3966e577dde71 Binary files /dev/null and b/Grinder/res/icons/escalator-silhouette-symbol.png differ diff --git a/Grinder/res/icons/escalator-up-sign.png b/Grinder/res/icons/escalator-up-sign.png new file mode 100644 index 0000000000000000000000000000000000000000..e78075f68df46c57ca17a7be2bcd6f5020f6cc4a Binary files /dev/null and b/Grinder/res/icons/escalator-up-sign.png differ diff --git a/Grinder/res/icons/female-symbol.png b/Grinder/res/icons/female-symbol.png new file mode 100644 index 0000000000000000000000000000000000000000..2015503fe9f5868ddf9487acb1d6a1662446f748 Binary files /dev/null and b/Grinder/res/icons/female-symbol.png differ diff --git a/Grinder/res/icons/find-again-option.png b/Grinder/res/icons/find-again-option.png new file mode 100644 index 0000000000000000000000000000000000000000..c3c5b0530b41348b7b6066e01798ec119d1f3039 Binary files /dev/null and b/Grinder/res/icons/find-again-option.png differ diff --git a/Grinder/res/icons/find-and-replace.png b/Grinder/res/icons/find-and-replace.png new file mode 100644 index 0000000000000000000000000000000000000000..ef2ac692ee22cff281bcb0f9cc2757f5b0b2a673 Binary files /dev/null and b/Grinder/res/icons/find-and-replace.png differ diff --git a/Grinder/res/icons/find-text.png b/Grinder/res/icons/find-text.png new file mode 100644 index 0000000000000000000000000000000000000000..831680af82b2430b744914bbf3d7cb0be4ac1c48 Binary files /dev/null and b/Grinder/res/icons/find-text.png differ diff --git a/Grinder/res/icons/first-aid-cross-in-black-inside-a-circle.png b/Grinder/res/icons/first-aid-cross-in-black-inside-a-circle.png new file mode 100644 index 0000000000000000000000000000000000000000..2b27381c440a682a9d0aa20d71d6efeb9fc1c955 Binary files /dev/null and b/Grinder/res/icons/first-aid-cross-in-black-inside-a-circle.png differ diff --git a/Grinder/res/icons/fit-to-height.png b/Grinder/res/icons/fit-to-height.png new file mode 100644 index 0000000000000000000000000000000000000000..2e1a91769f15580af426c174be2e3db4b37322f9 Binary files /dev/null and b/Grinder/res/icons/fit-to-height.png differ diff --git a/Grinder/res/icons/fit-to-width.png b/Grinder/res/icons/fit-to-width.png new file mode 100644 index 0000000000000000000000000000000000000000..415481a2152beb7e9f2129a18322095b594a00ba Binary files /dev/null and b/Grinder/res/icons/fit-to-width.png differ diff --git a/Grinder/res/icons/flag-signal-in-black-cloth-on-a-pole.png b/Grinder/res/icons/flag-signal-in-black-cloth-on-a-pole.png new file mode 100644 index 0000000000000000000000000000000000000000..79304f9f0f57b95d3ac4c5d93e0a0d6ec85bf229 Binary files /dev/null and b/Grinder/res/icons/flag-signal-in-black-cloth-on-a-pole.png differ diff --git a/Grinder/res/icons/floppy-disk-digital-data-storage-or-save-interface-symbol.png b/Grinder/res/icons/floppy-disk-digital-data-storage-or-save-interface-symbol.png new file mode 100644 index 0000000000000000000000000000000000000000..928eda26eae59c990ca36fe75a8b77260fa1904c Binary files /dev/null and b/Grinder/res/icons/floppy-disk-digital-data-storage-or-save-interface-symbol.png differ diff --git a/Grinder/res/icons/floppy-diskette-with-open-window.png b/Grinder/res/icons/floppy-diskette-with-open-window.png new file mode 100644 index 0000000000000000000000000000000000000000..5b7552ee43bb5dad54036683beb6d11c6de518cc Binary files /dev/null and b/Grinder/res/icons/floppy-diskette-with-open-window.png differ diff --git a/Grinder/res/icons/floppy-diskette-with-pen.png b/Grinder/res/icons/floppy-diskette-with-pen.png new file mode 100644 index 0000000000000000000000000000000000000000..4d585524e2b2d6e1e5249fd0e374ab895b0de34d Binary files /dev/null and b/Grinder/res/icons/floppy-diskette-with-pen.png differ diff --git a/Grinder/res/icons/floppy-disks-pair.png b/Grinder/res/icons/floppy-disks-pair.png new file mode 100644 index 0000000000000000000000000000000000000000..deaf267bd4d4444302d855b2e99d88d90e0feccb Binary files /dev/null and b/Grinder/res/icons/floppy-disks-pair.png differ diff --git a/Grinder/res/icons/floppy-drive.png b/Grinder/res/icons/floppy-drive.png new file mode 100644 index 0000000000000000000000000000000000000000..3080b8949c57bf11969665c9d76ad7a7979ad77b Binary files /dev/null and b/Grinder/res/icons/floppy-drive.png differ diff --git a/Grinder/res/icons/folder-and-window.png b/Grinder/res/icons/folder-and-window.png new file mode 100644 index 0000000000000000000000000000000000000000..62f53c24e0efa75db86f70141714a14906cc72ad Binary files /dev/null and b/Grinder/res/icons/folder-and-window.png differ diff --git a/Grinder/res/icons/folder-into.png b/Grinder/res/icons/folder-into.png new file mode 100644 index 0000000000000000000000000000000000000000..0811c34f96672e11bfb84be18b9960b756657f99 Binary files /dev/null and b/Grinder/res/icons/folder-into.png differ diff --git a/Grinder/res/icons/folder-network-option.png b/Grinder/res/icons/folder-network-option.png new file mode 100644 index 0000000000000000000000000000000000000000..e10c4ccb2ccf54dd4d15b61cf2ad66c25e5f41d9 Binary files /dev/null and b/Grinder/res/icons/folder-network-option.png differ diff --git a/Grinder/res/icons/folder-out-interface-symbol.png b/Grinder/res/icons/folder-out-interface-symbol.png new file mode 100644 index 0000000000000000000000000000000000000000..a3170b667dc3dfc1792e50640841f0e98ad9a5d5 Binary files /dev/null and b/Grinder/res/icons/folder-out-interface-symbol.png differ diff --git a/Grinder/res/icons/folder-shared.png b/Grinder/res/icons/folder-shared.png new file mode 100644 index 0000000000000000000000000000000000000000..9fe27144755e6d848181f7e25403226b5c1c9ea2 Binary files /dev/null and b/Grinder/res/icons/folder-shared.png differ diff --git a/Grinder/res/icons/folder-with-a-document-page.png b/Grinder/res/icons/folder-with-a-document-page.png new file mode 100644 index 0000000000000000000000000000000000000000..1cad7473f52bec7e96f9c057a31171d4d496e830 Binary files /dev/null and b/Grinder/res/icons/folder-with-a-document-page.png differ diff --git a/Grinder/res/icons/folder-with-information.png b/Grinder/res/icons/folder-with-information.png new file mode 100644 index 0000000000000000000000000000000000000000..0d4d5917befff22fba9f4900d2d559937eaf991d Binary files /dev/null and b/Grinder/res/icons/folder-with-information.png differ diff --git a/Grinder/res/icons/folder-with-label-of-large-size.png b/Grinder/res/icons/folder-with-label-of-large-size.png new file mode 100644 index 0000000000000000000000000000000000000000..506a169d39bd1e8221067f6a7422f585dc047304 Binary files /dev/null and b/Grinder/res/icons/folder-with-label-of-large-size.png differ diff --git a/Grinder/res/icons/folder-zip.png b/Grinder/res/icons/folder-zip.png new file mode 100644 index 0000000000000000000000000000000000000000..985c2c031bd47b5666f93b27b98d5509a957b80a Binary files /dev/null and b/Grinder/res/icons/folder-zip.png differ diff --git a/Grinder/res/icons/folders-of-large-size-arranged.png b/Grinder/res/icons/folders-of-large-size-arranged.png new file mode 100644 index 0000000000000000000000000000000000000000..b4c7ec7f75f868484179600b34bbef61e6260edf Binary files /dev/null and b/Grinder/res/icons/folders-of-large-size-arranged.png differ diff --git a/Grinder/res/icons/folders-with-information.png b/Grinder/res/icons/folders-with-information.png new file mode 100644 index 0000000000000000000000000000000000000000..e643a8e5c2eec9826de0b50e5a25088ac0ce5380 Binary files /dev/null and b/Grinder/res/icons/folders-with-information.png differ diff --git a/Grinder/res/icons/font-style-bold.png b/Grinder/res/icons/font-style-bold.png new file mode 100644 index 0000000000000000000000000000000000000000..d45dff1622dc5e4cee0174f9088d8056e6de0c5b Binary files /dev/null and b/Grinder/res/icons/font-style-bold.png differ diff --git a/Grinder/res/icons/font-style-subscript.png b/Grinder/res/icons/font-style-subscript.png new file mode 100644 index 0000000000000000000000000000000000000000..832e9ba64a946788720caee9bdd49b3157dccef1 Binary files /dev/null and b/Grinder/res/icons/font-style-subscript.png differ diff --git a/Grinder/res/icons/font-style-superscript.png b/Grinder/res/icons/font-style-superscript.png new file mode 100644 index 0000000000000000000000000000000000000000..8456aa6b35d97949202542a0cc2b64589205b863 Binary files /dev/null and b/Grinder/res/icons/font-style-superscript.png differ diff --git a/Grinder/res/icons/font-style-underline.png b/Grinder/res/icons/font-style-underline.png new file mode 100644 index 0000000000000000000000000000000000000000..8f1c38a57d83dcbd53a58297108dc2be627b2e2f Binary files /dev/null and b/Grinder/res/icons/font-style-underline.png differ diff --git a/Grinder/res/icons/font-window.png b/Grinder/res/icons/font-window.png new file mode 100644 index 0000000000000000000000000000000000000000..c2db2d9a0d9b2118ddbc23047ae882ed64407c55 Binary files /dev/null and b/Grinder/res/icons/font-window.png differ diff --git a/Grinder/res/icons/font.png b/Grinder/res/icons/font.png new file mode 100644 index 0000000000000000000000000000000000000000..08701af8b2810afd799f60e1813e30df5ce6d5f9 Binary files /dev/null and b/Grinder/res/icons/font.png differ diff --git a/Grinder/res/icons/forbidden-sign.png b/Grinder/res/icons/forbidden-sign.png new file mode 100644 index 0000000000000000000000000000000000000000..f7ad0eb6a1edd2801c282bb063210f4573395fe6 Binary files /dev/null and b/Grinder/res/icons/forbidden-sign.png differ diff --git a/Grinder/res/icons/four-arrows-meeting-at-the-center.png b/Grinder/res/icons/four-arrows-meeting-at-the-center.png new file mode 100644 index 0000000000000000000000000000000000000000..28bf5c72fcf97e72dd24cd281f4fd7875b5ee9f5 Binary files /dev/null and b/Grinder/res/icons/four-arrows-meeting-at-the-center.png differ diff --git a/Grinder/res/icons/garbage-can-half-full-with-recycle-symbol.png b/Grinder/res/icons/garbage-can-half-full-with-recycle-symbol.png new file mode 100644 index 0000000000000000000000000000000000000000..21eebb5d8edefd65df18746d3b820ce1d256f7d6 Binary files /dev/null and b/Grinder/res/icons/garbage-can-half-full-with-recycle-symbol.png differ diff --git a/Grinder/res/icons/garbage-can-with-recycling-symbol.png b/Grinder/res/icons/garbage-can-with-recycling-symbol.png new file mode 100644 index 0000000000000000000000000000000000000000..8ce4469124b131f944dba3903cc48787afcecc48 Binary files /dev/null and b/Grinder/res/icons/garbage-can-with-recycling-symbol.png differ diff --git a/Grinder/res/icons/garbage-can.png b/Grinder/res/icons/garbage-can.png new file mode 100644 index 0000000000000000000000000000000000000000..aecc7144a2c6fee6a6ae291fbad8111b1019f464 Binary files /dev/null and b/Grinder/res/icons/garbage-can.png differ diff --git a/Grinder/res/icons/garbage-container.png b/Grinder/res/icons/garbage-container.png new file mode 100644 index 0000000000000000000000000000000000000000..37917cf01591c6832784b034e44365bc357838ce Binary files /dev/null and b/Grinder/res/icons/garbage-container.png differ diff --git a/Grinder/res/icons/garbage-full.png b/Grinder/res/icons/garbage-full.png new file mode 100644 index 0000000000000000000000000000000000000000..70105997cd3a9c3f6a98c1ec93c3135c6370341c Binary files /dev/null and b/Grinder/res/icons/garbage-full.png differ diff --git a/Grinder/res/icons/garbage-with-recycle-sign-overflowing-with-trash.png b/Grinder/res/icons/garbage-with-recycle-sign-overflowing-with-trash.png new file mode 100644 index 0000000000000000000000000000000000000000..87556948ab058d5d2aefcf2681a131bf6887aca7 Binary files /dev/null and b/Grinder/res/icons/garbage-with-recycle-sign-overflowing-with-trash.png differ diff --git a/Grinder/res/icons/hard-drive-computer-part.png b/Grinder/res/icons/hard-drive-computer-part.png new file mode 100644 index 0000000000000000000000000000000000000000..39ef5ecde6fc383291e407779b5c1e78184e25bc Binary files /dev/null and b/Grinder/res/icons/hard-drive-computer-part.png differ diff --git a/Grinder/res/icons/hard-drive-network.png b/Grinder/res/icons/hard-drive-network.png new file mode 100644 index 0000000000000000000000000000000000000000..00b8faeda5a3feaa367a89bf503d5c9fe0aa34af Binary files /dev/null and b/Grinder/res/icons/hard-drive-network.png differ diff --git a/Grinder/res/icons/heart-simple-shape-silhouette.png b/Grinder/res/icons/heart-simple-shape-silhouette.png new file mode 100644 index 0000000000000000000000000000000000000000..61970cd9f15ecef41148e0d06c59ee1d5e07c05c Binary files /dev/null and b/Grinder/res/icons/heart-simple-shape-silhouette.png differ diff --git a/Grinder/res/icons/history.png b/Grinder/res/icons/history.png new file mode 100644 index 0000000000000000000000000000000000000000..61fadde48b1d2ac71878916d9b5040294ad99bc0 Binary files /dev/null and b/Grinder/res/icons/history.png differ diff --git a/Grinder/res/icons/indent-decrease-symbol.png b/Grinder/res/icons/indent-decrease-symbol.png new file mode 100644 index 0000000000000000000000000000000000000000..286cb08f8dca3b57ad29ef51e6fdc86bd7d7d00a Binary files /dev/null and b/Grinder/res/icons/indent-decrease-symbol.png differ diff --git a/Grinder/res/icons/indent-increase-interface-symbol.png b/Grinder/res/icons/indent-increase-interface-symbol.png new file mode 100644 index 0000000000000000000000000000000000000000..1b535a132da464c03c430be4e4fe0f013b012041 Binary files /dev/null and b/Grinder/res/icons/indent-increase-interface-symbol.png differ diff --git a/Grinder/res/icons/information.png b/Grinder/res/icons/information.png new file mode 100644 index 0000000000000000000000000000000000000000..7670c919740148339e2e425be384baf695b70267 Binary files /dev/null and b/Grinder/res/icons/information.png differ diff --git a/Grinder/res/icons/italics-font-style-variant.png b/Grinder/res/icons/italics-font-style-variant.png new file mode 100644 index 0000000000000000000000000000000000000000..e5fe168cd06b13118f6fe31fc4054704c7208309 Binary files /dev/null and b/Grinder/res/icons/italics-font-style-variant.png differ diff --git a/Grinder/res/icons/justified-text-alignment.png b/Grinder/res/icons/justified-text-alignment.png new file mode 100644 index 0000000000000000000000000000000000000000..48f20015042e278b9b94213259a68a36037c2fbb Binary files /dev/null and b/Grinder/res/icons/justified-text-alignment.png differ diff --git a/Grinder/res/icons/key-with-cross-sign.png b/Grinder/res/icons/key-with-cross-sign.png new file mode 100644 index 0000000000000000000000000000000000000000..504199f0be95441cdd6e30d6fb3a5482ad81459c Binary files /dev/null and b/Grinder/res/icons/key-with-cross-sign.png differ diff --git a/Grinder/res/icons/layout.png b/Grinder/res/icons/layout.png new file mode 100644 index 0000000000000000000000000000000000000000..e17d2ff920a9f2aed8ba76b7f1940894cd1232b9 Binary files /dev/null and b/Grinder/res/icons/layout.png differ diff --git a/Grinder/res/icons/led-light-lamp.png b/Grinder/res/icons/led-light-lamp.png new file mode 100644 index 0000000000000000000000000000000000000000..4fc48531bb0330bf155c5d96b06a6ee6bb3402a7 Binary files /dev/null and b/Grinder/res/icons/led-light-lamp.png differ diff --git a/Grinder/res/icons/letter-n-lower-case-font.png b/Grinder/res/icons/letter-n-lower-case-font.png new file mode 100644 index 0000000000000000000000000000000000000000..6ae0ef8df7ede0b5310113e29dc15e1fca71d8ae Binary files /dev/null and b/Grinder/res/icons/letter-n-lower-case-font.png differ diff --git a/Grinder/res/icons/letters-abc-in-pixelated-form.png b/Grinder/res/icons/letters-abc-in-pixelated-form.png new file mode 100644 index 0000000000000000000000000000000000000000..fe8ee889ed049b7abfe4e9075d3b26bf36ee6046 Binary files /dev/null and b/Grinder/res/icons/letters-abc-in-pixelated-form.png differ diff --git a/Grinder/res/icons/lifebelt.png b/Grinder/res/icons/lifebelt.png new file mode 100644 index 0000000000000000000000000000000000000000..292ecd55cece412394518995df15e4a066e80c58 Binary files /dev/null and b/Grinder/res/icons/lifebelt.png differ diff --git a/Grinder/res/icons/light-bulb-off.png b/Grinder/res/icons/light-bulb-off.png new file mode 100644 index 0000000000000000000000000000000000000000..bacd7817fd1bdd8e7a95f002714c49cac4e77608 Binary files /dev/null and b/Grinder/res/icons/light-bulb-off.png differ diff --git a/Grinder/res/icons/lightbulb-on.png b/Grinder/res/icons/lightbulb-on.png new file mode 100644 index 0000000000000000000000000000000000000000..b3ce1b576a18c4eb6f4ab0a5611039afd0204017 Binary files /dev/null and b/Grinder/res/icons/lightbulb-on.png differ diff --git a/Grinder/res/icons/lighthouse-with-flashing-lights.png b/Grinder/res/icons/lighthouse-with-flashing-lights.png new file mode 100644 index 0000000000000000000000000000000000000000..1551a45a3fc246cd3992013f884e40646190fdeb Binary files /dev/null and b/Grinder/res/icons/lighthouse-with-flashing-lights.png differ diff --git a/Grinder/res/icons/line-break-symbol.png b/Grinder/res/icons/line-break-symbol.png new file mode 100644 index 0000000000000000000000000000000000000000..349ee589b44164e8f93fec408e02bba0df1fdde1 Binary files /dev/null and b/Grinder/res/icons/line-break-symbol.png differ diff --git a/Grinder/res/icons/line-spacing-adjustment-in-a-paragraph.png b/Grinder/res/icons/line-spacing-adjustment-in-a-paragraph.png new file mode 100644 index 0000000000000000000000000000000000000000..aa58f030a0037dc3c1f2e94d8fa271af1cb8063c Binary files /dev/null and b/Grinder/res/icons/line-spacing-adjustment-in-a-paragraph.png differ diff --git a/Grinder/res/icons/line-spacing-text.png b/Grinder/res/icons/line-spacing-text.png new file mode 100644 index 0000000000000000000000000000000000000000..c5d9e20884696e8c054d67e4873d58dc8cf82c0f Binary files /dev/null and b/Grinder/res/icons/line-spacing-text.png differ diff --git a/Grinder/res/icons/list-in-bullet-form.png b/Grinder/res/icons/list-in-bullet-form.png new file mode 100644 index 0000000000000000000000000000000000000000..dbadcc7a7fcea334e9fb005a93f7ab307bfa9296 Binary files /dev/null and b/Grinder/res/icons/list-in-bullet-form.png differ diff --git a/Grinder/res/icons/list-roman-style-numbers.png b/Grinder/res/icons/list-roman-style-numbers.png new file mode 100644 index 0000000000000000000000000000000000000000..749dbb89f1c9a3059915b4c8d51c64bcfa3c5e36 Binary files /dev/null and b/Grinder/res/icons/list-roman-style-numbers.png differ diff --git a/Grinder/res/icons/locator-on-an-open-folded-map.png b/Grinder/res/icons/locator-on-an-open-folded-map.png new file mode 100644 index 0000000000000000000000000000000000000000..e9cfea6abf0e8acbcfe0dd7e72018228c98a2901 Binary files /dev/null and b/Grinder/res/icons/locator-on-an-open-folded-map.png differ diff --git a/Grinder/res/icons/log-in.png b/Grinder/res/icons/log-in.png new file mode 100644 index 0000000000000000000000000000000000000000..c4c411bf52315dab458fd13db1453cbf3c597e31 Binary files /dev/null and b/Grinder/res/icons/log-in.png differ diff --git a/Grinder/res/icons/magazine-folder.png b/Grinder/res/icons/magazine-folder.png new file mode 100644 index 0000000000000000000000000000000000000000..d44c39f3a182626657f0f18b33c8cbea617664bc Binary files /dev/null and b/Grinder/res/icons/magazine-folder.png differ diff --git a/Grinder/res/icons/magic-wand.png b/Grinder/res/icons/magic-wand.png new file mode 100644 index 0000000000000000000000000000000000000000..dcaa33e11e17ad03be40e8a96bad69a6038058ba Binary files /dev/null and b/Grinder/res/icons/magic-wand.png differ diff --git a/Grinder/res/icons/male-gender-symbol-variant.png b/Grinder/res/icons/male-gender-symbol-variant.png new file mode 100644 index 0000000000000000000000000000000000000000..1a5285e7b61328bc0b9ae7577946a74e83dce0bd Binary files /dev/null and b/Grinder/res/icons/male-gender-symbol-variant.png differ diff --git a/Grinder/res/icons/map-location.png b/Grinder/res/icons/map-location.png new file mode 100644 index 0000000000000000000000000000000000000000..95e57c58c615e81c31624ed1b2f9b5916de66508 Binary files /dev/null and b/Grinder/res/icons/map-location.png differ diff --git a/Grinder/res/icons/map-of-roads.png b/Grinder/res/icons/map-of-roads.png new file mode 100644 index 0000000000000000000000000000000000000000..21b6d369bab17af36cfcd8b971cf3a8dc1e9014d Binary files /dev/null and b/Grinder/res/icons/map-of-roads.png differ diff --git a/Grinder/res/icons/map-route.png b/Grinder/res/icons/map-route.png new file mode 100644 index 0000000000000000000000000000000000000000..3a76ff6bb05f57adcdffb4a7d04caec2b9fed675 Binary files /dev/null and b/Grinder/res/icons/map-route.png differ diff --git a/Grinder/res/icons/map.png b/Grinder/res/icons/map.png new file mode 100644 index 0000000000000000000000000000000000000000..19f6167d2c0f8ad139de51bfc6aa3fd887967a05 Binary files /dev/null and b/Grinder/res/icons/map.png differ diff --git a/Grinder/res/icons/maximize-size-option.png b/Grinder/res/icons/maximize-size-option.png new file mode 100644 index 0000000000000000000000000000000000000000..05fe1629eada2c97f0cb78dd18177dd662fa01f6 Binary files /dev/null and b/Grinder/res/icons/maximize-size-option.png differ diff --git a/Grinder/res/icons/microsoft-word-document-file.png b/Grinder/res/icons/microsoft-word-document-file.png new file mode 100644 index 0000000000000000000000000000000000000000..7df66cd32f31ed9b94b3ebeac95ddd85769daa09 Binary files /dev/null and b/Grinder/res/icons/microsoft-word-document-file.png differ diff --git a/Grinder/res/icons/minimize.png b/Grinder/res/icons/minimize.png new file mode 100644 index 0000000000000000000000000000000000000000..253ce5fa7ff85c91d399172d2da56d1cba0c3cbf Binary files /dev/null and b/Grinder/res/icons/minimize.png differ diff --git a/Grinder/res/icons/minus-sign-of-a-line-in-horizontal-position.png b/Grinder/res/icons/minus-sign-of-a-line-in-horizontal-position.png new file mode 100644 index 0000000000000000000000000000000000000000..b76f59b13363de88a21f58c0095157d06b926513 Binary files /dev/null and b/Grinder/res/icons/minus-sign-of-a-line-in-horizontal-position.png differ diff --git a/Grinder/res/icons/minus-sign.png b/Grinder/res/icons/minus-sign.png new file mode 100644 index 0000000000000000000000000000000000000000..935633d63df8284a0b9b523f3f573e3063e89dfb Binary files /dev/null and b/Grinder/res/icons/minus-sign.png differ diff --git a/Grinder/res/icons/music-cd.png b/Grinder/res/icons/music-cd.png new file mode 100644 index 0000000000000000000000000000000000000000..6b169808df957a429b68ae75d1b9f74ce4202918 Binary files /dev/null and b/Grinder/res/icons/music-cd.png differ diff --git a/Grinder/res/icons/music-document.png b/Grinder/res/icons/music-document.png new file mode 100644 index 0000000000000000000000000000000000000000..4b286dfbead32de3fc115bf6b046270e5b8865b9 Binary files /dev/null and b/Grinder/res/icons/music-document.png differ diff --git a/Grinder/res/icons/music-folder.png b/Grinder/res/icons/music-folder.png new file mode 100644 index 0000000000000000000000000000000000000000..b8c4563a36715ec4c5d049a631ba67ebde86a222 Binary files /dev/null and b/Grinder/res/icons/music-folder.png differ diff --git a/Grinder/res/icons/navigate-arrow-button-to-beginning.png b/Grinder/res/icons/navigate-arrow-button-to-beginning.png new file mode 100644 index 0000000000000000000000000000000000000000..2265db9e1fa7ef722dcc58aa8c0d7335fa411381 Binary files /dev/null and b/Grinder/res/icons/navigate-arrow-button-to-beginning.png differ diff --git a/Grinder/res/icons/navigate-arrows-pointing-to-down.png b/Grinder/res/icons/navigate-arrows-pointing-to-down.png new file mode 100644 index 0000000000000000000000000000000000000000..a295efe8b8c27f74aa936ec7e006f2fe0d93579f Binary files /dev/null and b/Grinder/res/icons/navigate-arrows-pointing-to-down.png differ diff --git a/Grinder/res/icons/navigate-to-end.png b/Grinder/res/icons/navigate-to-end.png new file mode 100644 index 0000000000000000000000000000000000000000..e3bb0039a612ede48198df9469ed3b8339911435 Binary files /dev/null and b/Grinder/res/icons/navigate-to-end.png differ diff --git a/Grinder/res/icons/navigate-up-arrow.png b/Grinder/res/icons/navigate-up-arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..15e4f9e77f5ef609b23d843686e6947641027de9 Binary files /dev/null and b/Grinder/res/icons/navigate-up-arrow.png differ diff --git a/Grinder/res/icons/navigate-up-arrows.png b/Grinder/res/icons/navigate-up-arrows.png new file mode 100644 index 0000000000000000000000000000000000000000..83c44a5f3c472bff35222c6cebea5fa615bf073a Binary files /dev/null and b/Grinder/res/icons/navigate-up-arrows.png differ diff --git a/Grinder/res/icons/navigation-arrow-down-button.png b/Grinder/res/icons/navigation-arrow-down-button.png new file mode 100644 index 0000000000000000000000000000000000000000..73f68e2fb8867138e15251f4202cae7781cfe28a Binary files /dev/null and b/Grinder/res/icons/navigation-arrow-down-button.png differ diff --git a/Grinder/res/icons/navigation-history-interface-symbol-of-a-clock-with-an-arrow.png b/Grinder/res/icons/navigation-history-interface-symbol-of-a-clock-with-an-arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..6dd46664968ac0e8da0a0188d6a45ed82846994f Binary files /dev/null and b/Grinder/res/icons/navigation-history-interface-symbol-of-a-clock-with-an-arrow.png differ diff --git a/Grinder/res/icons/navigation-up-arrow-to-the-left.png b/Grinder/res/icons/navigation-up-arrow-to-the-left.png new file mode 100644 index 0000000000000000000000000000000000000000..4cf98fc5ea3b7514738c87be6a4dbc814445bc01 Binary files /dev/null and b/Grinder/res/icons/navigation-up-arrow-to-the-left.png differ diff --git a/Grinder/res/icons/navigational-arrow-pointing-down-left-in-a-circle.png b/Grinder/res/icons/navigational-arrow-pointing-down-left-in-a-circle.png new file mode 100644 index 0000000000000000000000000000000000000000..53eeb525b2de10127d7aeeaf88af5452bb201e28 Binary files /dev/null and b/Grinder/res/icons/navigational-arrow-pointing-down-left-in-a-circle.png differ diff --git a/Grinder/res/icons/numbered-list-style.png b/Grinder/res/icons/numbered-list-style.png new file mode 100644 index 0000000000000000000000000000000000000000..c95f8ca4e5b3b0c632ac71aeefe474e76464ed5b Binary files /dev/null and b/Grinder/res/icons/numbered-list-style.png differ diff --git a/Grinder/res/icons/open-door-with-border.png b/Grinder/res/icons/open-door-with-border.png new file mode 100644 index 0000000000000000000000000000000000000000..145104dd3f20bde07bbf077a2e1b0597a5e98c8e Binary files /dev/null and b/Grinder/res/icons/open-door-with-border.png differ diff --git a/Grinder/res/icons/open-folder-black-and-white-variant.png b/Grinder/res/icons/open-folder-black-and-white-variant.png new file mode 100644 index 0000000000000000000000000000000000000000..6b3ed313c2cc3342962acb78185905235a48754e Binary files /dev/null and b/Grinder/res/icons/open-folder-black-and-white-variant.png differ diff --git a/Grinder/res/icons/open-folder-with-document.png b/Grinder/res/icons/open-folder-with-document.png new file mode 100644 index 0000000000000000000000000000000000000000..fcffdd7ece110b5c1b885cd5eaeb7143ccad0938 Binary files /dev/null and b/Grinder/res/icons/open-folder-with-document.png differ diff --git a/Grinder/res/icons/open-scroll-outline.png b/Grinder/res/icons/open-scroll-outline.png new file mode 100644 index 0000000000000000000000000000000000000000..b9992c7abdd2ef27137bb9cd7edd73e342925f91 Binary files /dev/null and b/Grinder/res/icons/open-scroll-outline.png differ diff --git a/Grinder/res/icons/open-window-with-gear-sign.png b/Grinder/res/icons/open-window-with-gear-sign.png new file mode 100644 index 0000000000000000000000000000000000000000..0b05922b02586806db99628d629ba2ef49520647 Binary files /dev/null and b/Grinder/res/icons/open-window-with-gear-sign.png differ diff --git a/Grinder/res/icons/open-window-with-height-adjustment.png b/Grinder/res/icons/open-window-with-height-adjustment.png new file mode 100644 index 0000000000000000000000000000000000000000..d21f1bf27ef8a27ee023cd84b5a26407678329fb Binary files /dev/null and b/Grinder/res/icons/open-window-with-height-adjustment.png differ diff --git a/Grinder/res/icons/open-window-with-star-symbol.png b/Grinder/res/icons/open-window-with-star-symbol.png new file mode 100644 index 0000000000000000000000000000000000000000..81828761de7b317e2fd01fae50f8ae002e450b4e Binary files /dev/null and b/Grinder/res/icons/open-window-with-star-symbol.png differ diff --git a/Grinder/res/icons/open-window-with-timer.png b/Grinder/res/icons/open-window-with-timer.png new file mode 100644 index 0000000000000000000000000000000000000000..218918c203c554cc0a03b6f799fde603781745c4 Binary files /dev/null and b/Grinder/res/icons/open-window-with-timer.png differ diff --git a/Grinder/res/icons/painting/051-calculator.png b/Grinder/res/icons/painting/051-calculator.png new file mode 100644 index 0000000000000000000000000000000000000000..3b95a17644e5e82c376cc24bef127b21dbe8796c Binary files /dev/null and b/Grinder/res/icons/painting/051-calculator.png differ diff --git a/Grinder/res/icons/painting/051-chalk.png b/Grinder/res/icons/painting/051-chalk.png new file mode 100644 index 0000000000000000000000000000000000000000..a0184d3971b63cdf570dc37e4c700cfce6485a37 Binary files /dev/null and b/Grinder/res/icons/painting/051-chalk.png differ diff --git a/Grinder/res/icons/painting/051-compass.png b/Grinder/res/icons/painting/051-compass.png new file mode 100644 index 0000000000000000000000000000000000000000..07fa462c7993276a74571e0f0bdf677c9446d388 Binary files /dev/null and b/Grinder/res/icons/painting/051-compass.png differ diff --git a/Grinder/res/icons/painting/051-crayon.png b/Grinder/res/icons/painting/051-crayon.png new file mode 100644 index 0000000000000000000000000000000000000000..7629955133553e0d2edc3372c67d77238f030c01 Binary files /dev/null and b/Grinder/res/icons/painting/051-crayon.png differ diff --git a/Grinder/res/icons/painting/051-cutter.png b/Grinder/res/icons/painting/051-cutter.png new file mode 100644 index 0000000000000000000000000000000000000000..742c7bf8447168251317459bdc7027d6d5453079 Binary files /dev/null and b/Grinder/res/icons/painting/051-cutter.png differ diff --git a/Grinder/res/icons/painting/051-desk-lamp.png b/Grinder/res/icons/painting/051-desk-lamp.png new file mode 100644 index 0000000000000000000000000000000000000000..6c5d4d48bb2aa1e3fa7359836340b6d029e0a4a1 Binary files /dev/null and b/Grinder/res/icons/painting/051-desk-lamp.png differ diff --git a/Grinder/res/icons/painting/051-dropper-1.png b/Grinder/res/icons/painting/051-dropper-1.png new file mode 100644 index 0000000000000000000000000000000000000000..6f33d8714b070dd63408d834daba66d123d93a86 Binary files /dev/null and b/Grinder/res/icons/painting/051-dropper-1.png differ diff --git a/Grinder/res/icons/painting/051-dropper.png b/Grinder/res/icons/painting/051-dropper.png new file mode 100644 index 0000000000000000000000000000000000000000..c43e5830e3d0cce7ea1fa3a5276f4345a69e74ab Binary files /dev/null and b/Grinder/res/icons/painting/051-dropper.png differ diff --git a/Grinder/res/icons/painting/051-easel.png b/Grinder/res/icons/painting/051-easel.png new file mode 100644 index 0000000000000000000000000000000000000000..64cfe5377cf25b2e6ee33288c86fab83efa67d71 Binary files /dev/null and b/Grinder/res/icons/painting/051-easel.png differ diff --git a/Grinder/res/icons/painting/051-eraser.png b/Grinder/res/icons/painting/051-eraser.png new file mode 100644 index 0000000000000000000000000000000000000000..ab8f1485cbce806e0819c308b18ece0a6481071d Binary files /dev/null and b/Grinder/res/icons/painting/051-eraser.png differ diff --git a/Grinder/res/icons/painting/051-file-1.png b/Grinder/res/icons/painting/051-file-1.png new file mode 100644 index 0000000000000000000000000000000000000000..22da46f02eecae56e8cc63c13f1a764fb555f513 Binary files /dev/null and b/Grinder/res/icons/painting/051-file-1.png differ diff --git a/Grinder/res/icons/painting/051-file-2.png b/Grinder/res/icons/painting/051-file-2.png new file mode 100644 index 0000000000000000000000000000000000000000..a11b7ea86d7dd863ad082d352f4f0e188d6c3358 Binary files /dev/null and b/Grinder/res/icons/painting/051-file-2.png differ diff --git a/Grinder/res/icons/painting/051-file-3.png b/Grinder/res/icons/painting/051-file-3.png new file mode 100644 index 0000000000000000000000000000000000000000..83ddd18fd3eb2494827607e37216d07d39dc372d Binary files /dev/null and b/Grinder/res/icons/painting/051-file-3.png differ diff --git a/Grinder/res/icons/painting/051-file.png b/Grinder/res/icons/painting/051-file.png new file mode 100644 index 0000000000000000000000000000000000000000..479440db38f0b332e845c1b7d6066a1667c248de Binary files /dev/null and b/Grinder/res/icons/painting/051-file.png differ diff --git a/Grinder/res/icons/painting/051-glue.png b/Grinder/res/icons/painting/051-glue.png new file mode 100644 index 0000000000000000000000000000000000000000..12c84b982004a6aa1a48de0a9e8ff16632d7a6f5 Binary files /dev/null and b/Grinder/res/icons/painting/051-glue.png differ diff --git a/Grinder/res/icons/painting/051-highlighter.png b/Grinder/res/icons/painting/051-highlighter.png new file mode 100644 index 0000000000000000000000000000000000000000..3be149d1d1cf527d1e18fb6aead23cb6906350b8 Binary files /dev/null and b/Grinder/res/icons/painting/051-highlighter.png differ diff --git a/Grinder/res/icons/painting/051-idea.png b/Grinder/res/icons/painting/051-idea.png new file mode 100644 index 0000000000000000000000000000000000000000..7530f912791ce98828a61408b737e90c9e0f470c Binary files /dev/null and b/Grinder/res/icons/painting/051-idea.png differ diff --git a/Grinder/res/icons/painting/051-ink.png b/Grinder/res/icons/painting/051-ink.png new file mode 100644 index 0000000000000000000000000000000000000000..ed4e730e8b1333f475d316e5eb7553928dfa77c3 Binary files /dev/null and b/Grinder/res/icons/painting/051-ink.png differ diff --git a/Grinder/res/icons/painting/051-monitor.png b/Grinder/res/icons/painting/051-monitor.png new file mode 100644 index 0000000000000000000000000000000000000000..d4e8dbd4bb22cae737744a031fd3086b68f8b013 Binary files /dev/null and b/Grinder/res/icons/painting/051-monitor.png differ diff --git a/Grinder/res/icons/painting/051-mouse.png b/Grinder/res/icons/painting/051-mouse.png new file mode 100644 index 0000000000000000000000000000000000000000..e0783912d936bb727e176a991a6622988db3359b Binary files /dev/null and b/Grinder/res/icons/painting/051-mouse.png differ diff --git a/Grinder/res/icons/painting/051-notebook.png b/Grinder/res/icons/painting/051-notebook.png new file mode 100644 index 0000000000000000000000000000000000000000..d813279305684420667dac651edf76f53e86d82d Binary files /dev/null and b/Grinder/res/icons/painting/051-notebook.png differ diff --git a/Grinder/res/icons/painting/051-paint-brush-1.png b/Grinder/res/icons/painting/051-paint-brush-1.png new file mode 100644 index 0000000000000000000000000000000000000000..492ed6a18bebaa138ed4c8b07fd1eaeb70598ca4 Binary files /dev/null and b/Grinder/res/icons/painting/051-paint-brush-1.png differ diff --git a/Grinder/res/icons/painting/051-paint-brush-2.png b/Grinder/res/icons/painting/051-paint-brush-2.png new file mode 100644 index 0000000000000000000000000000000000000000..8002df8993d088f84c9c26e8694b20ced6b14c07 Binary files /dev/null and b/Grinder/res/icons/painting/051-paint-brush-2.png differ diff --git a/Grinder/res/icons/painting/051-paint-brush.png b/Grinder/res/icons/painting/051-paint-brush.png new file mode 100644 index 0000000000000000000000000000000000000000..4f3fdab1c99568a79b6467a7b0765c8dd162c2c3 Binary files /dev/null and b/Grinder/res/icons/painting/051-paint-brush.png differ diff --git a/Grinder/res/icons/painting/051-paint-bucket.png b/Grinder/res/icons/painting/051-paint-bucket.png new file mode 100644 index 0000000000000000000000000000000000000000..df676ccfb635eb169dbacdfeaab4e0af7b262156 Binary files /dev/null and b/Grinder/res/icons/painting/051-paint-bucket.png differ diff --git a/Grinder/res/icons/painting/051-paint-palette-1.png b/Grinder/res/icons/painting/051-paint-palette-1.png new file mode 100644 index 0000000000000000000000000000000000000000..5756b141a370f02aba8d0e91ef4ebb121cc4d68b Binary files /dev/null and b/Grinder/res/icons/painting/051-paint-palette-1.png differ diff --git a/Grinder/res/icons/painting/051-paint-palette.png b/Grinder/res/icons/painting/051-paint-palette.png new file mode 100644 index 0000000000000000000000000000000000000000..07bcfdc98ca89b67e609b7e53945be1c6d8ba1d9 Binary files /dev/null and b/Grinder/res/icons/painting/051-paint-palette.png differ diff --git a/Grinder/res/icons/painting/051-paint-roller.png b/Grinder/res/icons/painting/051-paint-roller.png new file mode 100644 index 0000000000000000000000000000000000000000..f8a836e219f196763dbb8881fefa79494b9f57ac Binary files /dev/null and b/Grinder/res/icons/painting/051-paint-roller.png differ diff --git a/Grinder/res/icons/painting/051-paint-tube.png b/Grinder/res/icons/painting/051-paint-tube.png new file mode 100644 index 0000000000000000000000000000000000000000..cd39f7cd3e633c6420d1fc3261528cedadcdc662 Binary files /dev/null and b/Grinder/res/icons/painting/051-paint-tube.png differ diff --git a/Grinder/res/icons/painting/051-paperclip.png b/Grinder/res/icons/painting/051-paperclip.png new file mode 100644 index 0000000000000000000000000000000000000000..855cce53fcb8953a9cba09dab4c1a276874b400b Binary files /dev/null and b/Grinder/res/icons/painting/051-paperclip.png differ diff --git a/Grinder/res/icons/painting/051-pen-1.png b/Grinder/res/icons/painting/051-pen-1.png new file mode 100644 index 0000000000000000000000000000000000000000..848a93299454b6264d06e94acee4c7fe1ce49d15 Binary files /dev/null and b/Grinder/res/icons/painting/051-pen-1.png differ diff --git a/Grinder/res/icons/painting/051-pen-2.png b/Grinder/res/icons/painting/051-pen-2.png new file mode 100644 index 0000000000000000000000000000000000000000..96ebe101958c5c58395fccecdb92389d3e8e5a74 Binary files /dev/null and b/Grinder/res/icons/painting/051-pen-2.png differ diff --git a/Grinder/res/icons/painting/051-pen-3.png b/Grinder/res/icons/painting/051-pen-3.png new file mode 100644 index 0000000000000000000000000000000000000000..16748e46c93658e0ebb4dc7bec23f958ae42e897 Binary files /dev/null and b/Grinder/res/icons/painting/051-pen-3.png differ diff --git a/Grinder/res/icons/painting/051-pen.png b/Grinder/res/icons/painting/051-pen.png new file mode 100644 index 0000000000000000000000000000000000000000..3a322f98ae2e1fc089e92c0fa694aa532323fba5 Binary files /dev/null and b/Grinder/res/icons/painting/051-pen.png differ diff --git a/Grinder/res/icons/painting/051-pencil.png b/Grinder/res/icons/painting/051-pencil.png new file mode 100644 index 0000000000000000000000000000000000000000..de24bf7b112e995372eac4cce237972d40b67116 Binary files /dev/null and b/Grinder/res/icons/painting/051-pencil.png differ diff --git a/Grinder/res/icons/painting/051-photo-camera.png b/Grinder/res/icons/painting/051-photo-camera.png new file mode 100644 index 0000000000000000000000000000000000000000..1cc80d3b70331ad92f8ef4b4b83f4b56eef5ddaf Binary files /dev/null and b/Grinder/res/icons/painting/051-photo-camera.png differ diff --git a/Grinder/res/icons/painting/051-quill-1.png b/Grinder/res/icons/painting/051-quill-1.png new file mode 100644 index 0000000000000000000000000000000000000000..09861cebde035126381f5e22327aac2df30acda7 Binary files /dev/null and b/Grinder/res/icons/painting/051-quill-1.png differ diff --git a/Grinder/res/icons/painting/051-quill-2.png b/Grinder/res/icons/painting/051-quill-2.png new file mode 100644 index 0000000000000000000000000000000000000000..148f405358e407f10f5676cefb1cc0791a67b178 Binary files /dev/null and b/Grinder/res/icons/painting/051-quill-2.png differ diff --git a/Grinder/res/icons/painting/051-quill.png b/Grinder/res/icons/painting/051-quill.png new file mode 100644 index 0000000000000000000000000000000000000000..ef5c07ff9d8964a36aedc771fdc4f8b1eabbfa60 Binary files /dev/null and b/Grinder/res/icons/painting/051-quill.png differ diff --git a/Grinder/res/icons/painting/051-ruler-1.png b/Grinder/res/icons/painting/051-ruler-1.png new file mode 100644 index 0000000000000000000000000000000000000000..796a96838a976893a846c84d066bd115c5d70838 Binary files /dev/null and b/Grinder/res/icons/painting/051-ruler-1.png differ diff --git a/Grinder/res/icons/painting/051-ruler.png b/Grinder/res/icons/painting/051-ruler.png new file mode 100644 index 0000000000000000000000000000000000000000..e0e8ffb68839b493d567ad25832226c8ecd20e47 Binary files /dev/null and b/Grinder/res/icons/painting/051-ruler.png differ diff --git a/Grinder/res/icons/painting/051-scissors.png b/Grinder/res/icons/painting/051-scissors.png new file mode 100644 index 0000000000000000000000000000000000000000..e358decbc054348a07250caca4c94b63b3797806 Binary files /dev/null and b/Grinder/res/icons/painting/051-scissors.png differ diff --git a/Grinder/res/icons/painting/051-set-square-1.png b/Grinder/res/icons/painting/051-set-square-1.png new file mode 100644 index 0000000000000000000000000000000000000000..2016d9097898065d339139e7e497382a14dc3778 Binary files /dev/null and b/Grinder/res/icons/painting/051-set-square-1.png differ diff --git a/Grinder/res/icons/painting/051-set-square.png b/Grinder/res/icons/painting/051-set-square.png new file mode 100644 index 0000000000000000000000000000000000000000..fa4661704196464f58f31d60193eeac5570d57d7 Binary files /dev/null and b/Grinder/res/icons/painting/051-set-square.png differ diff --git a/Grinder/res/icons/painting/051-sharpener.png b/Grinder/res/icons/painting/051-sharpener.png new file mode 100644 index 0000000000000000000000000000000000000000..34016b3ac52f808abdc4b29268effe640a51ff3d Binary files /dev/null and b/Grinder/res/icons/painting/051-sharpener.png differ diff --git a/Grinder/res/icons/painting/051-spray-paint.png b/Grinder/res/icons/painting/051-spray-paint.png new file mode 100644 index 0000000000000000000000000000000000000000..38504bbafebbc3678414a9d8af5effd402a2d145 Binary files /dev/null and b/Grinder/res/icons/painting/051-spray-paint.png differ diff --git a/Grinder/res/icons/painting/051-transform.png b/Grinder/res/icons/painting/051-transform.png new file mode 100644 index 0000000000000000000000000000000000000000..8c576a49e4d8540a3b740325dd9dfca7dd9d0e5f Binary files /dev/null and b/Grinder/res/icons/painting/051-transform.png differ diff --git a/Grinder/res/icons/painting/051-vector-1.png b/Grinder/res/icons/painting/051-vector-1.png new file mode 100644 index 0000000000000000000000000000000000000000..9f6e05cb3f5d36ad4f3ad01b5b948bcd93aeee93 Binary files /dev/null and b/Grinder/res/icons/painting/051-vector-1.png differ diff --git a/Grinder/res/icons/painting/051-vector.png b/Grinder/res/icons/painting/051-vector.png new file mode 100644 index 0000000000000000000000000000000000000000..20968e82a5cc7f0584db2b35beb93fe5754ccead Binary files /dev/null and b/Grinder/res/icons/painting/051-vector.png differ diff --git a/Grinder/res/icons/painting/051-watercolor.png b/Grinder/res/icons/painting/051-watercolor.png new file mode 100644 index 0000000000000000000000000000000000000000..6f15885a7b4eccfde79e125bb1a53df4b3d72f73 Binary files /dev/null and b/Grinder/res/icons/painting/051-watercolor.png differ diff --git a/Grinder/res/icons/painting/art-board.png b/Grinder/res/icons/painting/art-board.png new file mode 100644 index 0000000000000000000000000000000000000000..1250eebaf4f8fd3f1b3bd3a5bcf9d1266c426645 Binary files /dev/null and b/Grinder/res/icons/painting/art-board.png differ diff --git a/Grinder/res/icons/painting/art-palette.png b/Grinder/res/icons/painting/art-palette.png new file mode 100644 index 0000000000000000000000000000000000000000..710cbc8be0f941f6d8e3ca63a48565663f7e7ee9 Binary files /dev/null and b/Grinder/res/icons/painting/art-palette.png differ diff --git a/Grinder/res/icons/painting/barrett.png b/Grinder/res/icons/painting/barrett.png new file mode 100644 index 0000000000000000000000000000000000000000..8498d502b3f2b5b4286c899ad458ccac6e2063e5 Binary files /dev/null and b/Grinder/res/icons/painting/barrett.png differ diff --git a/Grinder/res/icons/painting/calculator.png b/Grinder/res/icons/painting/calculator.png new file mode 100644 index 0000000000000000000000000000000000000000..205748233e8ca9aef3b8aacd57e893ccacbc1047 Binary files /dev/null and b/Grinder/res/icons/painting/calculator.png differ diff --git a/Grinder/res/icons/painting/chalk.png b/Grinder/res/icons/painting/chalk.png new file mode 100644 index 0000000000000000000000000000000000000000..40cc311b1c35233874c587d1df3187fdec3eecfd Binary files /dev/null and b/Grinder/res/icons/painting/chalk.png differ diff --git a/Grinder/res/icons/painting/circle-ruler.png b/Grinder/res/icons/painting/circle-ruler.png new file mode 100644 index 0000000000000000000000000000000000000000..9e3a30048582d776b53d050a4d545783b973e9fc Binary files /dev/null and b/Grinder/res/icons/painting/circle-ruler.png differ diff --git a/Grinder/res/icons/painting/compass.png b/Grinder/res/icons/painting/compass.png new file mode 100644 index 0000000000000000000000000000000000000000..6719b55d6d8ccb00fd3c2d936c3dc70a9e679ae9 Binary files /dev/null and b/Grinder/res/icons/painting/compass.png differ diff --git a/Grinder/res/icons/painting/computer-mouse.png b/Grinder/res/icons/painting/computer-mouse.png new file mode 100644 index 0000000000000000000000000000000000000000..be73856ed39ea6ce94986a15d2773ed6157265f8 Binary files /dev/null and b/Grinder/res/icons/painting/computer-mouse.png differ diff --git a/Grinder/res/icons/painting/computer-screen.png b/Grinder/res/icons/painting/computer-screen.png new file mode 100644 index 0000000000000000000000000000000000000000..3e34a1413a0fabf51f060fd7dc1484538ff2ce51 Binary files /dev/null and b/Grinder/res/icons/painting/computer-screen.png differ diff --git a/Grinder/res/icons/painting/crayola.png b/Grinder/res/icons/painting/crayola.png new file mode 100644 index 0000000000000000000000000000000000000000..8b0a1918dc996187a4ebc9809fa48bc6fd819418 Binary files /dev/null and b/Grinder/res/icons/painting/crayola.png differ diff --git a/Grinder/res/icons/painting/curve.png b/Grinder/res/icons/painting/curve.png new file mode 100644 index 0000000000000000000000000000000000000000..26706b679d297ff08752bfedb3244f9a26642051 Binary files /dev/null and b/Grinder/res/icons/painting/curve.png differ diff --git a/Grinder/res/icons/painting/cutter-knife.png b/Grinder/res/icons/painting/cutter-knife.png new file mode 100644 index 0000000000000000000000000000000000000000..90937981de41ab77d853778e0b357e803753df16 Binary files /dev/null and b/Grinder/res/icons/painting/cutter-knife.png differ diff --git a/Grinder/res/icons/painting/desk-lamp.png b/Grinder/res/icons/painting/desk-lamp.png new file mode 100644 index 0000000000000000000000000000000000000000..0d780c1ee291cf46c9c39f9183ef19b62f4f4d69 Binary files /dev/null and b/Grinder/res/icons/painting/desk-lamp.png differ diff --git a/Grinder/res/icons/painting/eraser.png b/Grinder/res/icons/painting/eraser.png new file mode 100644 index 0000000000000000000000000000000000000000..c02328e75076f4eb0a6c92fa93eba37575b94ed1 Binary files /dev/null and b/Grinder/res/icons/painting/eraser.png differ diff --git a/Grinder/res/icons/painting/eyedropper-1.png b/Grinder/res/icons/painting/eyedropper-1.png new file mode 100644 index 0000000000000000000000000000000000000000..99051ea79d42b701ca14b61f19cfee4901f2fd8b Binary files /dev/null and b/Grinder/res/icons/painting/eyedropper-1.png differ diff --git a/Grinder/res/icons/painting/eyedropper.png b/Grinder/res/icons/painting/eyedropper.png new file mode 100644 index 0000000000000000000000000000000000000000..567d53354f7d5957edfad28f43b4f9406c9d5113 Binary files /dev/null and b/Grinder/res/icons/painting/eyedropper.png differ diff --git a/Grinder/res/icons/painting/feather-and-ink.png b/Grinder/res/icons/painting/feather-and-ink.png new file mode 100644 index 0000000000000000000000000000000000000000..3fc153119a5b098145723fe41085662fac9e9d15 Binary files /dev/null and b/Grinder/res/icons/painting/feather-and-ink.png differ diff --git a/Grinder/res/icons/painting/feather.png b/Grinder/res/icons/painting/feather.png new file mode 100644 index 0000000000000000000000000000000000000000..8a19e0b2a4d1320454adf558f2e6a4e53e6a07fa Binary files /dev/null and b/Grinder/res/icons/painting/feather.png differ diff --git a/Grinder/res/icons/painting/glue.png b/Grinder/res/icons/painting/glue.png new file mode 100644 index 0000000000000000000000000000000000000000..e15df887c94720950b7e9a0e1c169dce97feb335 Binary files /dev/null and b/Grinder/res/icons/painting/glue.png differ diff --git a/Grinder/res/icons/painting/idea.png b/Grinder/res/icons/painting/idea.png new file mode 100644 index 0000000000000000000000000000000000000000..be0854d181280941ca9ac8d1d5cc6144ddd8be42 Binary files /dev/null and b/Grinder/res/icons/painting/idea.png differ diff --git a/Grinder/res/icons/painting/ink.png b/Grinder/res/icons/painting/ink.png new file mode 100644 index 0000000000000000000000000000000000000000..d65e588001061e54c8a70e394a28d1a29a6497c0 Binary files /dev/null and b/Grinder/res/icons/painting/ink.png differ diff --git a/Grinder/res/icons/painting/line.png b/Grinder/res/icons/painting/line.png new file mode 100644 index 0000000000000000000000000000000000000000..e0917178e22bb0d7145e4a1e616ef854942d299c Binary files /dev/null and b/Grinder/res/icons/painting/line.png differ diff --git a/Grinder/res/icons/painting/marker.png b/Grinder/res/icons/painting/marker.png new file mode 100644 index 0000000000000000000000000000000000000000..9bb68cbce27ff7fee91c77929a79c5bae69bd584 Binary files /dev/null and b/Grinder/res/icons/painting/marker.png differ diff --git a/Grinder/res/icons/painting/paint-brush-1.png b/Grinder/res/icons/painting/paint-brush-1.png new file mode 100644 index 0000000000000000000000000000000000000000..5c801ea19d25d0acd134f5d0aeba0e9e52926732 Binary files /dev/null and b/Grinder/res/icons/painting/paint-brush-1.png differ diff --git a/Grinder/res/icons/painting/paint-brush.png b/Grinder/res/icons/painting/paint-brush.png new file mode 100644 index 0000000000000000000000000000000000000000..8e8394f3ea22cc7358051465eeee52b83626a680 Binary files /dev/null and b/Grinder/res/icons/painting/paint-brush.png differ diff --git a/Grinder/res/icons/painting/paint-can.png b/Grinder/res/icons/painting/paint-can.png new file mode 100644 index 0000000000000000000000000000000000000000..26f05075aef094856e622c06142c609a1141c49f Binary files /dev/null and b/Grinder/res/icons/painting/paint-can.png differ diff --git a/Grinder/res/icons/painting/paint-palette.png b/Grinder/res/icons/painting/paint-palette.png new file mode 100644 index 0000000000000000000000000000000000000000..04c2587810f1da893ba25e6bb3ec5c684049f01b Binary files /dev/null and b/Grinder/res/icons/painting/paint-palette.png differ diff --git a/Grinder/res/icons/painting/paint-roller.png b/Grinder/res/icons/painting/paint-roller.png new file mode 100644 index 0000000000000000000000000000000000000000..33f426c82946d5462be8a454c6c7828b347bb88d Binary files /dev/null and b/Grinder/res/icons/painting/paint-roller.png differ diff --git a/Grinder/res/icons/painting/paint-tube.png b/Grinder/res/icons/painting/paint-tube.png new file mode 100644 index 0000000000000000000000000000000000000000..bb8386a70d022a16a28b00923e5ae55cee24c5a9 Binary files /dev/null and b/Grinder/res/icons/painting/paint-tube.png differ diff --git a/Grinder/res/icons/painting/paintbrush.png b/Grinder/res/icons/painting/paintbrush.png new file mode 100644 index 0000000000000000000000000000000000000000..79d2c4e9397cc6f8c3615bbe6c4dffab92f70639 Binary files /dev/null and b/Grinder/res/icons/painting/paintbrush.png differ diff --git a/Grinder/res/icons/painting/paper-1.png b/Grinder/res/icons/painting/paper-1.png new file mode 100644 index 0000000000000000000000000000000000000000..33b50be30a62d52cf136740e78e08b4caaa928f3 Binary files /dev/null and b/Grinder/res/icons/painting/paper-1.png differ diff --git a/Grinder/res/icons/painting/paper-and-feather.png b/Grinder/res/icons/painting/paper-and-feather.png new file mode 100644 index 0000000000000000000000000000000000000000..228bca38990f912ec4edeb1ed619ab7a5a1274bc Binary files /dev/null and b/Grinder/res/icons/painting/paper-and-feather.png differ diff --git a/Grinder/res/icons/painting/paper-and-pencil.png b/Grinder/res/icons/painting/paper-and-pencil.png new file mode 100644 index 0000000000000000000000000000000000000000..7fcbf28a2f32b6cecc7371c475359ae4a88ed881 Binary files /dev/null and b/Grinder/res/icons/painting/paper-and-pencil.png differ diff --git a/Grinder/res/icons/painting/paper-clip.png b/Grinder/res/icons/painting/paper-clip.png new file mode 100644 index 0000000000000000000000000000000000000000..50bad49f4ffc49ef942139a0fb7d8dbfdbda32ce Binary files /dev/null and b/Grinder/res/icons/painting/paper-clip.png differ diff --git a/Grinder/res/icons/painting/paper-with-text.png b/Grinder/res/icons/painting/paper-with-text.png new file mode 100644 index 0000000000000000000000000000000000000000..86ed02b9c436799dc53183bae2c6a16dc4049d93 Binary files /dev/null and b/Grinder/res/icons/painting/paper-with-text.png differ diff --git a/Grinder/res/icons/painting/paper.png b/Grinder/res/icons/painting/paper.png new file mode 100644 index 0000000000000000000000000000000000000000..fb3706cd5712ca24cfbda746a380c1071ec4d23b Binary files /dev/null and b/Grinder/res/icons/painting/paper.png differ diff --git a/Grinder/res/icons/painting/pen-1.png b/Grinder/res/icons/painting/pen-1.png new file mode 100644 index 0000000000000000000000000000000000000000..1212bee1b3db81ba4da29c2134cb09359c00d2a6 Binary files /dev/null and b/Grinder/res/icons/painting/pen-1.png differ diff --git a/Grinder/res/icons/painting/pen-tool.png b/Grinder/res/icons/painting/pen-tool.png new file mode 100644 index 0000000000000000000000000000000000000000..a61a20957b09865436795e31d87c869d31381376 Binary files /dev/null and b/Grinder/res/icons/painting/pen-tool.png differ diff --git a/Grinder/res/icons/painting/pen.png b/Grinder/res/icons/painting/pen.png new file mode 100644 index 0000000000000000000000000000000000000000..b3c95dd1b31226ebd77fd760ab9e1bf4daadbf59 Binary files /dev/null and b/Grinder/res/icons/painting/pen.png differ diff --git a/Grinder/res/icons/painting/pencil-1.png b/Grinder/res/icons/painting/pencil-1.png new file mode 100644 index 0000000000000000000000000000000000000000..3720bd3ffab277a20d7911f8570c90cc237bdffc Binary files /dev/null and b/Grinder/res/icons/painting/pencil-1.png differ diff --git a/Grinder/res/icons/painting/pencil.png b/Grinder/res/icons/painting/pencil.png new file mode 100644 index 0000000000000000000000000000000000000000..f8c653050539f3c0a2009b8e422eeb703062838f Binary files /dev/null and b/Grinder/res/icons/painting/pencil.png differ diff --git a/Grinder/res/icons/painting/photo-camera.png b/Grinder/res/icons/painting/photo-camera.png new file mode 100644 index 0000000000000000000000000000000000000000..24d2bd15cfe3ed8a63a67b00bc8c7630472972ca Binary files /dev/null and b/Grinder/res/icons/painting/photo-camera.png differ diff --git a/Grinder/res/icons/painting/ruler.png b/Grinder/res/icons/painting/ruler.png new file mode 100644 index 0000000000000000000000000000000000000000..2c4d484d0bcd4a4154a003f89ec1517882f10139 Binary files /dev/null and b/Grinder/res/icons/painting/ruler.png differ diff --git a/Grinder/res/icons/painting/s-curve.png b/Grinder/res/icons/painting/s-curve.png new file mode 100644 index 0000000000000000000000000000000000000000..e76a2f0a8bbb0118b43c3e9fb55a50f498832417 Binary files /dev/null and b/Grinder/res/icons/painting/s-curve.png differ diff --git a/Grinder/res/icons/painting/scissors.png b/Grinder/res/icons/painting/scissors.png new file mode 100644 index 0000000000000000000000000000000000000000..7cbdbc2c1c194064b39a1021cc6087bbf714aa5d Binary files /dev/null and b/Grinder/res/icons/painting/scissors.png differ diff --git a/Grinder/res/icons/painting/sharpener.png b/Grinder/res/icons/painting/sharpener.png new file mode 100644 index 0000000000000000000000000000000000000000..00879f9dcd5d5a65086e0d17a48aa98e226e5411 Binary files /dev/null and b/Grinder/res/icons/painting/sharpener.png differ diff --git a/Grinder/res/icons/painting/sketchbook.png b/Grinder/res/icons/painting/sketchbook.png new file mode 100644 index 0000000000000000000000000000000000000000..ce78b64be53349928bba995ed32821eda31ecbab Binary files /dev/null and b/Grinder/res/icons/painting/sketchbook.png differ diff --git a/Grinder/res/icons/painting/spray.png b/Grinder/res/icons/painting/spray.png new file mode 100644 index 0000000000000000000000000000000000000000..b15f16923347ac27fad32a58b9ec9c3c538586d6 Binary files /dev/null and b/Grinder/res/icons/painting/spray.png differ diff --git a/Grinder/res/icons/painting/transform-box.png b/Grinder/res/icons/painting/transform-box.png new file mode 100644 index 0000000000000000000000000000000000000000..5bf8a255bf918704b02c23a7b43e6ad4ed4f5ec9 Binary files /dev/null and b/Grinder/res/icons/painting/transform-box.png differ diff --git a/Grinder/res/icons/painting/triangle-ruler.png b/Grinder/res/icons/painting/triangle-ruler.png new file mode 100644 index 0000000000000000000000000000000000000000..c1501595bd87328023bb4a3a8e9aef63a075a743 Binary files /dev/null and b/Grinder/res/icons/painting/triangle-ruler.png differ diff --git a/Grinder/res/icons/painting/watercolor-palette.png b/Grinder/res/icons/painting/watercolor-palette.png new file mode 100644 index 0000000000000000000000000000000000000000..f85585bfb71b24c9321335c8ebe9c5326de48a28 Binary files /dev/null and b/Grinder/res/icons/painting/watercolor-palette.png differ diff --git a/Grinder/res/icons/plus-sign.png b/Grinder/res/icons/plus-sign.png new file mode 100644 index 0000000000000000000000000000000000000000..55c7c826b2592b79be3e610eef34ad8e4550cdb2 Binary files /dev/null and b/Grinder/res/icons/plus-sign.png differ diff --git a/Grinder/res/icons/plus-symbol-button.png b/Grinder/res/icons/plus-symbol-button.png new file mode 100644 index 0000000000000000000000000000000000000000..609fb590dc44f04d617093d0a1706477e658daae Binary files /dev/null and b/Grinder/res/icons/plus-symbol-button.png differ diff --git a/Grinder/res/icons/portfolio-folder-with-button-lock.png b/Grinder/res/icons/portfolio-folder-with-button-lock.png new file mode 100644 index 0000000000000000000000000000000000000000..991524b46cc7fe14241cb5f591fe706401bfb470 Binary files /dev/null and b/Grinder/res/icons/portfolio-folder-with-button-lock.png differ diff --git a/Grinder/res/icons/printer-of-network.png b/Grinder/res/icons/printer-of-network.png new file mode 100644 index 0000000000000000000000000000000000000000..50df5f63c45af9b1af7a798251364730f4e17cc8 Binary files /dev/null and b/Grinder/res/icons/printer-of-network.png differ diff --git a/Grinder/res/icons/printer-with-document-coming-out-of-machine.png b/Grinder/res/icons/printer-with-document-coming-out-of-machine.png new file mode 100644 index 0000000000000000000000000000000000000000..c807448076c08a4c6c5c7551f992286444e927c1 Binary files /dev/null and b/Grinder/res/icons/printer-with-document-coming-out-of-machine.png differ diff --git a/Grinder/res/icons/question-mark-in-a-circle.png b/Grinder/res/icons/question-mark-in-a-circle.png new file mode 100644 index 0000000000000000000000000000000000000000..c52555083b0fb82fde4f85d133d616495a511ee1 Binary files /dev/null and b/Grinder/res/icons/question-mark-in-a-circle.png differ diff --git a/Grinder/res/icons/quotation-mark.png b/Grinder/res/icons/quotation-mark.png new file mode 100644 index 0000000000000000000000000000000000000000..513cfb2b2dca89314363a3d1e41cca9b43586d58 Binary files /dev/null and b/Grinder/res/icons/quotation-mark.png differ diff --git a/Grinder/res/icons/quotation-marks.png b/Grinder/res/icons/quotation-marks.png new file mode 100644 index 0000000000000000000000000000000000000000..6e872539191bb569e91c2a3ac979b30d39aee8b0 Binary files /dev/null and b/Grinder/res/icons/quotation-marks.png differ diff --git a/Grinder/res/icons/quotation-right-mark.png b/Grinder/res/icons/quotation-right-mark.png new file mode 100644 index 0000000000000000000000000000000000000000..903d7e9dc72b4e347e7679a8b3f86b49f18d8f2c Binary files /dev/null and b/Grinder/res/icons/quotation-right-mark.png differ diff --git a/Grinder/res/icons/radiation-warning-sign.png b/Grinder/res/icons/radiation-warning-sign.png new file mode 100644 index 0000000000000000000000000000000000000000..2c108c47dad92db0f401e689e55b7ef27181e074 Binary files /dev/null and b/Grinder/res/icons/radiation-warning-sign.png differ diff --git a/Grinder/res/icons/recycle-symbol-made-of-arrows-with-selection-box.png b/Grinder/res/icons/recycle-symbol-made-of-arrows-with-selection-box.png new file mode 100644 index 0000000000000000000000000000000000000000..f610ca8f79294a9e688eba3af339071739243728 Binary files /dev/null and b/Grinder/res/icons/recycle-symbol-made-of-arrows-with-selection-box.png differ diff --git a/Grinder/res/icons/redo-navigational-arrow-in-a-circle.png b/Grinder/res/icons/redo-navigational-arrow-in-a-circle.png new file mode 100644 index 0000000000000000000000000000000000000000..c9e91654cbe0b1510e97f0730ff0c368cc4a641f Binary files /dev/null and b/Grinder/res/icons/redo-navigational-arrow-in-a-circle.png differ diff --git a/Grinder/res/icons/refresh-arrows-couple.png b/Grinder/res/icons/refresh-arrows-couple.png new file mode 100644 index 0000000000000000000000000000000000000000..48ffa35848fbb6defb914986a913a0cc9212dffd Binary files /dev/null and b/Grinder/res/icons/refresh-arrows-couple.png differ diff --git a/Grinder/res/icons/refresh-navigational-arrows-interface-symbol-inside-a-circle.png b/Grinder/res/icons/refresh-navigational-arrows-interface-symbol-inside-a-circle.png new file mode 100644 index 0000000000000000000000000000000000000000..574986a8c84b2eb2d83e57534792eeb10dda27c0 Binary files /dev/null and b/Grinder/res/icons/refresh-navigational-arrows-interface-symbol-inside-a-circle.png differ diff --git a/Grinder/res/icons/resize-arrow-inside-a-square-interface-symbol.png b/Grinder/res/icons/resize-arrow-inside-a-square-interface-symbol.png new file mode 100644 index 0000000000000000000000000000000000000000..4e76e6253bff4d91ad92e39a3d130ceaae740957 Binary files /dev/null and b/Grinder/res/icons/resize-arrow-inside-a-square-interface-symbol.png differ diff --git a/Grinder/res/icons/right-text-alignment-option.png b/Grinder/res/icons/right-text-alignment-option.png new file mode 100644 index 0000000000000000000000000000000000000000..327f0f253d7d001825bc12012fb5a58dd6895a1f Binary files /dev/null and b/Grinder/res/icons/right-text-alignment-option.png differ diff --git a/Grinder/res/icons/right-thin-arrowheads.png b/Grinder/res/icons/right-thin-arrowheads.png new file mode 100644 index 0000000000000000000000000000000000000000..0c3db37398d3e729a01039a5d561821732408699 Binary files /dev/null and b/Grinder/res/icons/right-thin-arrowheads.png differ diff --git a/Grinder/res/icons/route-sign.png b/Grinder/res/icons/route-sign.png new file mode 100644 index 0000000000000000000000000000000000000000..b5e5607b8285783a3e93faa70230435b693c8f55 Binary files /dev/null and b/Grinder/res/icons/route-sign.png differ diff --git a/Grinder/res/icons/scroll-variant-with-seal.png b/Grinder/res/icons/scroll-variant-with-seal.png new file mode 100644 index 0000000000000000000000000000000000000000..a53fa60656e383a472a98e0565e1019305ee98a7 Binary files /dev/null and b/Grinder/res/icons/scroll-variant-with-seal.png differ diff --git a/Grinder/res/icons/select-all.png b/Grinder/res/icons/select-all.png new file mode 100644 index 0000000000000000000000000000000000000000..07dac5053d857d20a268cab15f0dd1bd09e87c61 Binary files /dev/null and b/Grinder/res/icons/select-all.png differ diff --git a/Grinder/res/icons/selection-find-interface-symbol-of-binoculars-with-eyes-in-a-square-of-broken-line.png b/Grinder/res/icons/selection-find-interface-symbol-of-binoculars-with-eyes-in-a-square-of-broken-line.png new file mode 100644 index 0000000000000000000000000000000000000000..30b0e4755737584959b708217875763b32743655 Binary files /dev/null and b/Grinder/res/icons/selection-find-interface-symbol-of-binoculars-with-eyes-in-a-square-of-broken-line.png differ diff --git a/Grinder/res/icons/service-bell.png b/Grinder/res/icons/service-bell.png new file mode 100644 index 0000000000000000000000000000000000000000..a2cc8a62331abfc13f836f85039d3c217d4fd01d Binary files /dev/null and b/Grinder/res/icons/service-bell.png differ diff --git a/Grinder/res/icons/ships-wheel.png b/Grinder/res/icons/ships-wheel.png new file mode 100644 index 0000000000000000000000000000000000000000..e4d7006e24cae1b7b20b3287ee7ea4688020f0d0 Binary files /dev/null and b/Grinder/res/icons/ships-wheel.png differ diff --git a/Grinder/res/icons/show-arrows.png b/Grinder/res/icons/show-arrows.png new file mode 100644 index 0000000000000000000000000000000000000000..3b4128297334757c2168d2cac82bb010ab026d99 Binary files /dev/null and b/Grinder/res/icons/show-arrows.png differ diff --git a/Grinder/res/icons/show-tags.png b/Grinder/res/icons/show-tags.png new file mode 100644 index 0000000000000000000000000000000000000000..e8ca441b3fe23bb516cc742bd132f8a949f238a4 Binary files /dev/null and b/Grinder/res/icons/show-tags.png differ diff --git a/Grinder/res/icons/signaling-disc-tool.png b/Grinder/res/icons/signaling-disc-tool.png new file mode 100644 index 0000000000000000000000000000000000000000..d65dd36b78209c9e31c05bad98c5279e43f156be Binary files /dev/null and b/Grinder/res/icons/signaling-disc-tool.png differ diff --git a/Grinder/res/icons/signpost-1.png b/Grinder/res/icons/signpost-1.png new file mode 100644 index 0000000000000000000000000000000000000000..ca362cba40a9c718587fb8545077e6ffefae7947 Binary files /dev/null and b/Grinder/res/icons/signpost-1.png differ diff --git a/Grinder/res/icons/signpost-with-three-arrows.png b/Grinder/res/icons/signpost-with-three-arrows.png new file mode 100644 index 0000000000000000000000000000000000000000..184289f80665f32b6a8103639caa075c7cd1c6f0 Binary files /dev/null and b/Grinder/res/icons/signpost-with-three-arrows.png differ diff --git a/Grinder/res/icons/signpost.png b/Grinder/res/icons/signpost.png new file mode 100644 index 0000000000000000000000000000000000000000..64903e93cf2cc650f9e7ead2cd6f04af71514b4b Binary files /dev/null and b/Grinder/res/icons/signpost.png differ diff --git a/Grinder/res/icons/speech-balloon-outline-for-conversation.png b/Grinder/res/icons/speech-balloon-outline-for-conversation.png new file mode 100644 index 0000000000000000000000000000000000000000..bf0c1ebbf2070bdc4e7ec8f0aa193052ec833281 Binary files /dev/null and b/Grinder/res/icons/speech-balloon-outline-for-conversation.png differ diff --git a/Grinder/res/icons/speech-balloon-outline-with-exclamation-mark.png b/Grinder/res/icons/speech-balloon-outline-with-exclamation-mark.png new file mode 100644 index 0000000000000000000000000000000000000000..6b1ba06507819c8e30d8084576cab8a02ba465c2 Binary files /dev/null and b/Grinder/res/icons/speech-balloon-outline-with-exclamation-mark.png differ diff --git a/Grinder/res/icons/speech-balloon-outline-with-question-mark.png b/Grinder/res/icons/speech-balloon-outline-with-question-mark.png new file mode 100644 index 0000000000000000000000000000000000000000..7d1b35658200db8f73179678249d932646848785 Binary files /dev/null and b/Grinder/res/icons/speech-balloon-outline-with-question-mark.png differ diff --git a/Grinder/res/icons/spell-check-interface-symbol.png b/Grinder/res/icons/spell-check-interface-symbol.png new file mode 100644 index 0000000000000000000000000000000000000000..23f11ce7efaf336890aa45b4a00792abed5f3bbf Binary files /dev/null and b/Grinder/res/icons/spell-check-interface-symbol.png differ diff --git a/Grinder/res/icons/split-type-window.png b/Grinder/res/icons/split-type-window.png new file mode 100644 index 0000000000000000000000000000000000000000..b8a4b7cbf8a85da8ab3b76b15ea4318cacf380fb Binary files /dev/null and b/Grinder/res/icons/split-type-window.png differ diff --git a/Grinder/res/icons/square-root-of-x-math-formula.png b/Grinder/res/icons/square-root-of-x-math-formula.png new file mode 100644 index 0000000000000000000000000000000000000000..84282ad44d2a1cf165e7534db5952b523cf4b526 Binary files /dev/null and b/Grinder/res/icons/square-root-of-x-math-formula.png differ diff --git a/Grinder/res/icons/ssd-drive-part.png b/Grinder/res/icons/ssd-drive-part.png new file mode 100644 index 0000000000000000000000000000000000000000..1ff62df7248fe7862d4887c188bef85c5ea965bf Binary files /dev/null and b/Grinder/res/icons/ssd-drive-part.png differ diff --git a/Grinder/res/icons/stairs-down.png b/Grinder/res/icons/stairs-down.png new file mode 100644 index 0000000000000000000000000000000000000000..93f1740f1d70f096c101efaa32f2ef9d2ec36643 Binary files /dev/null and b/Grinder/res/icons/stairs-down.png differ diff --git a/Grinder/res/icons/stairs-up.png b/Grinder/res/icons/stairs-up.png new file mode 100644 index 0000000000000000000000000000000000000000..f6f79fb8d9bb112366376a2e5bc350fe70c67edf Binary files /dev/null and b/Grinder/res/icons/stairs-up.png differ diff --git a/Grinder/res/icons/star-in-black-of-five-points-shape.png b/Grinder/res/icons/star-in-black-of-five-points-shape.png new file mode 100644 index 0000000000000000000000000000000000000000..f9b2a350f17caa7b21b2f5a02d7fe1d5c93dda1d Binary files /dev/null and b/Grinder/res/icons/star-in-black-of-five-points-shape.png differ diff --git a/Grinder/res/icons/star-outline.png b/Grinder/res/icons/star-outline.png new file mode 100644 index 0000000000000000000000000000000000000000..f96b6dacc5c52a3649a418ae22227e328a09c9ab Binary files /dev/null and b/Grinder/res/icons/star-outline.png differ diff --git a/Grinder/res/icons/start.png b/Grinder/res/icons/start.png new file mode 100644 index 0000000000000000000000000000000000000000..b46ccf151ce08b384380b5bd31fd9e5d7b164893 Binary files /dev/null and b/Grinder/res/icons/start.png differ diff --git a/Grinder/res/icons/stop-sign-variant.png b/Grinder/res/icons/stop-sign-variant.png new file mode 100644 index 0000000000000000000000000000000000000000..d12fbfb0b613587eb99efac1b160986e9096d0a9 Binary files /dev/null and b/Grinder/res/icons/stop-sign-variant.png differ diff --git a/Grinder/res/icons/strikethrough-font-variant.png b/Grinder/res/icons/strikethrough-font-variant.png new file mode 100644 index 0000000000000000000000000000000000000000..209dc2b939852df7d34393505198a0ecd55b66f7 Binary files /dev/null and b/Grinder/res/icons/strikethrough-font-variant.png differ diff --git a/Grinder/res/icons/sum-sign.png b/Grinder/res/icons/sum-sign.png new file mode 100644 index 0000000000000000000000000000000000000000..0d0103e6fc61e6c7c65cdd518c3c5f64affaa68d Binary files /dev/null and b/Grinder/res/icons/sum-sign.png differ diff --git a/Grinder/res/icons/tag-black.png b/Grinder/res/icons/tag-black.png new file mode 100644 index 0000000000000000000000000000000000000000..65ece66db8b6d01678aa5f0a15a94aa672081d6a Binary files /dev/null and b/Grinder/res/icons/tag-black.png differ diff --git a/Grinder/res/icons/tags-black-couple-with-rings.png b/Grinder/res/icons/tags-black-couple-with-rings.png new file mode 100644 index 0000000000000000000000000000000000000000..4b6bce5af4b61dee24796980af1cc181b0838068 Binary files /dev/null and b/Grinder/res/icons/tags-black-couple-with-rings.png differ diff --git a/Grinder/res/icons/text-align-left.png b/Grinder/res/icons/text-align-left.png new file mode 100644 index 0000000000000000000000000000000000000000..79fdd6c1ce3e7a74a31e01230a1730abc225dfef Binary files /dev/null and b/Grinder/res/icons/text-align-left.png differ diff --git a/Grinder/res/icons/text-alignment-at-the-center.png b/Grinder/res/icons/text-alignment-at-the-center.png new file mode 100644 index 0000000000000000000000000000000000000000..0b8a3ef5df07a26465aa3cc6c8e47c7467901f24 Binary files /dev/null and b/Grinder/res/icons/text-alignment-at-the-center.png differ diff --git a/Grinder/res/icons/text-document.png b/Grinder/res/icons/text-document.png new file mode 100644 index 0000000000000000000000000000000000000000..c2192d50b51e73080c624c09a58f01c49e105873 Binary files /dev/null and b/Grinder/res/icons/text-document.png differ diff --git a/Grinder/res/icons/ticket.png b/Grinder/res/icons/ticket.png new file mode 100644 index 0000000000000000000000000000000000000000..be33c49540dbc7165211141a390d4885aec96856 Binary files /dev/null and b/Grinder/res/icons/ticket.png differ diff --git a/Grinder/res/icons/toxic-warning-sign.png b/Grinder/res/icons/toxic-warning-sign.png new file mode 100644 index 0000000000000000000000000000000000000000..7515328d638cf54eab476dd3d415306d3d186b52 Binary files /dev/null and b/Grinder/res/icons/toxic-warning-sign.png differ diff --git a/Grinder/res/icons/traffic-light-in-red-signal.png b/Grinder/res/icons/traffic-light-in-red-signal.png new file mode 100644 index 0000000000000000000000000000000000000000..cc898200e460d2273159338dc3b3941794ae3a55 Binary files /dev/null and b/Grinder/res/icons/traffic-light-in-red-signal.png differ diff --git a/Grinder/res/icons/traffic-light-silhouette-variant.png b/Grinder/res/icons/traffic-light-silhouette-variant.png new file mode 100644 index 0000000000000000000000000000000000000000..2d05aa14dbaea5b638a0f845d899d356ce64d837 Binary files /dev/null and b/Grinder/res/icons/traffic-light-silhouette-variant.png differ diff --git a/Grinder/res/icons/traffic-light.png b/Grinder/res/icons/traffic-light.png new file mode 100644 index 0000000000000000000000000000000000000000..513ba96552a5582b2d6a90e9bb98f04299d4f9b4 Binary files /dev/null and b/Grinder/res/icons/traffic-light.png differ diff --git a/Grinder/res/icons/trafficlight-in-green.png b/Grinder/res/icons/trafficlight-in-green.png new file mode 100644 index 0000000000000000000000000000000000000000..64d84a0182f297dbdba8d02721abd0dc719d394a Binary files /dev/null and b/Grinder/res/icons/trafficlight-in-green.png differ diff --git a/Grinder/res/icons/trafficlight-in-yellow.png b/Grinder/res/icons/trafficlight-in-yellow.png new file mode 100644 index 0000000000000000000000000000000000000000..f074f605f051c9147cd9dd39814e74a74727886f Binary files /dev/null and b/Grinder/res/icons/trafficlight-in-yellow.png differ diff --git a/Grinder/res/icons/trafficlight-off.png b/Grinder/res/icons/trafficlight-off.png new file mode 100644 index 0000000000000000000000000000000000000000..a0d1b7e9cf6327800e5a8dc4c686e730d72d48cc Binary files /dev/null and b/Grinder/res/icons/trafficlight-off.png differ diff --git a/Grinder/res/icons/triangular-warning-sign.png b/Grinder/res/icons/triangular-warning-sign.png new file mode 100644 index 0000000000000000000000000000000000000000..220cb1f680ae900836dd10ee9004875b0921b2d1 Binary files /dev/null and b/Grinder/res/icons/triangular-warning-sign.png differ diff --git a/Grinder/res/icons/triple-arrow-merging-to-one.png b/Grinder/res/icons/triple-arrow-merging-to-one.png new file mode 100644 index 0000000000000000000000000000000000000000..cfb595dba064c3c7b2f8b36477797b292c14b247 Binary files /dev/null and b/Grinder/res/icons/triple-arrow-merging-to-one.png differ diff --git a/Grinder/res/icons/undo-arrow.png b/Grinder/res/icons/undo-arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..a4672b08e10aa08d1913821a0ccdb4c94bc9c42c Binary files /dev/null and b/Grinder/res/icons/undo-arrow.png differ diff --git a/Grinder/res/icons/undo-navigational-arrow-in-a-circle.png b/Grinder/res/icons/undo-navigational-arrow-in-a-circle.png new file mode 100644 index 0000000000000000000000000000000000000000..168319e8fedef471b4f79170beb87650a1a853d5 Binary files /dev/null and b/Grinder/res/icons/undo-navigational-arrow-in-a-circle.png differ diff --git a/Grinder/res/icons/up-right-arrow-in-a-circle.png b/Grinder/res/icons/up-right-arrow-in-a-circle.png new file mode 100644 index 0000000000000000000000000000000000000000..222cc04e5b6137eb67e521662b41b0ffc81a22fa Binary files /dev/null and b/Grinder/res/icons/up-right-arrow-in-a-circle.png differ diff --git a/Grinder/res/icons/warning-flammable-sign.png b/Grinder/res/icons/warning-flammable-sign.png new file mode 100644 index 0000000000000000000000000000000000000000..f191eac47b0f67e473da716b73ad9994c9037a90 Binary files /dev/null and b/Grinder/res/icons/warning-flammable-sign.png differ diff --git a/Grinder/res/icons/warning-harmful-sign.png b/Grinder/res/icons/warning-harmful-sign.png new file mode 100644 index 0000000000000000000000000000000000000000..571dff333129416f190dc8e7a8d1f3f6c770803a Binary files /dev/null and b/Grinder/res/icons/warning-harmful-sign.png differ diff --git a/Grinder/res/icons/warning-voltage-sign-of-a-bolt-inside-a-triangle.png b/Grinder/res/icons/warning-voltage-sign-of-a-bolt-inside-a-triangle.png new file mode 100644 index 0000000000000000000000000000000000000000..61a2e627963f9a3697398180c0adc7adcbc53118 Binary files /dev/null and b/Grinder/res/icons/warning-voltage-sign-of-a-bolt-inside-a-triangle.png differ diff --git a/Grinder/res/icons/warning-window-with-exclamation-sign-inside-a-triangle.png b/Grinder/res/icons/warning-window-with-exclamation-sign-inside-a-triangle.png new file mode 100644 index 0000000000000000000000000000000000000000..f79ba64666276bdb921b1e319b870786b5187a57 Binary files /dev/null and b/Grinder/res/icons/warning-window-with-exclamation-sign-inside-a-triangle.png differ diff --git a/Grinder/res/icons/window-close.png b/Grinder/res/icons/window-close.png new file mode 100644 index 0000000000000000000000000000000000000000..d158881e5259d02e7e8951f3397d8c4cdf5c5442 Binary files /dev/null and b/Grinder/res/icons/window-close.png differ diff --git a/Grinder/res/icons/window-networking-option.png b/Grinder/res/icons/window-networking-option.png new file mode 100644 index 0000000000000000000000000000000000000000..0810d8a2f56545967ea0efa4a66a59da2ff4546c Binary files /dev/null and b/Grinder/res/icons/window-networking-option.png differ diff --git a/Grinder/res/icons/window-of-dialog.png b/Grinder/res/icons/window-of-dialog.png new file mode 100644 index 0000000000000000000000000000000000000000..a80513bed048fca684dbce57cc6c65710b58ec1e Binary files /dev/null and b/Grinder/res/icons/window-of-dialog.png differ diff --git a/Grinder/res/icons/window-of-test-card.png b/Grinder/res/icons/window-of-test-card.png new file mode 100644 index 0000000000000000000000000000000000000000..17923cd61e6abf2d1dd5ddcc03fc7ccde1f6afe3 Binary files /dev/null and b/Grinder/res/icons/window-of-test-card.png differ diff --git a/Grinder/res/icons/window-size.png b/Grinder/res/icons/window-size.png new file mode 100644 index 0000000000000000000000000000000000000000..58c90c68d40d5c67a356a155c8f93d19dfc786fd Binary files /dev/null and b/Grinder/res/icons/window-size.png differ diff --git a/Grinder/res/icons/window-width.png b/Grinder/res/icons/window-width.png new file mode 100644 index 0000000000000000000000000000000000000000..431fe387088be547966354969d71b30caccc2193 Binary files /dev/null and b/Grinder/res/icons/window-width.png differ diff --git a/Grinder/res/icons/window-with-earth-image.png b/Grinder/res/icons/window-with-earth-image.png new file mode 100644 index 0000000000000000000000000000000000000000..f54ed3990be5802c59f5f997c591981c27e58984 Binary files /dev/null and b/Grinder/res/icons/window-with-earth-image.png differ diff --git a/Grinder/res/icons/window-with-side-bar-selection.png b/Grinder/res/icons/window-with-side-bar-selection.png new file mode 100644 index 0000000000000000000000000000000000000000..d147c7d2ec4e1657e6391af4d265079cecd834cb Binary files /dev/null and b/Grinder/res/icons/window-with-side-bar-selection.png differ diff --git a/Grinder/res/icons/windows-close.png b/Grinder/res/icons/windows-close.png new file mode 100644 index 0000000000000000000000000000000000000000..b9c028c061c73474f317fb8092f256135f2857a3 Binary files /dev/null and b/Grinder/res/icons/windows-close.png differ diff --git a/Grinder/res/icons/windows-couple.png b/Grinder/res/icons/windows-couple.png new file mode 100644 index 0000000000000000000000000000000000000000..ad02b6679c7aa09a47c1161338721e64e70e14d5 Binary files /dev/null and b/Grinder/res/icons/windows-couple.png differ diff --git a/Grinder/res/icons/yield-sign.png b/Grinder/res/icons/yield-sign.png new file mode 100644 index 0000000000000000000000000000000000000000..7e7bf7f4f5250c2a3f8d45f8cfce49ee3d00f089 Binary files /dev/null and b/Grinder/res/icons/yield-sign.png differ diff --git a/Grinder/res/icons/zip-file-document-variant.png b/Grinder/res/icons/zip-file-document-variant.png new file mode 100644 index 0000000000000000000000000000000000000000..809490b1da4cfc442002948d51e8f06d84da4a39 Binary files /dev/null and b/Grinder/res/icons/zip-file-document-variant.png differ diff --git a/Grinder/res/icons/zoom-fit.png b/Grinder/res/icons/zoom-fit.png new file mode 100644 index 0000000000000000000000000000000000000000..8361e6f095b050b3234ca3fb91e0b9bbc286e5b9 Binary files /dev/null and b/Grinder/res/icons/zoom-fit.png differ diff --git a/Grinder/res/icons/zoom-in.png b/Grinder/res/icons/zoom-in.png new file mode 100644 index 0000000000000000000000000000000000000000..d09026423382c44cfa86b8ea64ba8a6d85055e15 Binary files /dev/null and b/Grinder/res/icons/zoom-in.png differ diff --git a/Grinder/res/icons/zoom-orig.png b/Grinder/res/icons/zoom-orig.png new file mode 100644 index 0000000000000000000000000000000000000000..b22143236f34bd022189b16c64bc0426f824198c Binary files /dev/null and b/Grinder/res/icons/zoom-orig.png differ diff --git a/Grinder/res/icons/zoom-out.png b/Grinder/res/icons/zoom-out.png new file mode 100644 index 0000000000000000000000000000000000000000..304b0dcb76b3be71c913b08b5c8e9155e576fa54 Binary files /dev/null and b/Grinder/res/icons/zoom-out.png differ diff --git a/Grinder/res/misc/checkerboard.png b/Grinder/res/misc/checkerboard.png new file mode 100644 index 0000000000000000000000000000000000000000..4a5b02465dd67226e8898b9f2892bedb1cdffd23 Binary files /dev/null and b/Grinder/res/misc/checkerboard.png differ diff --git a/Grinder/ui/StyleSheet.cpp b/Grinder/ui/StyleSheet.cpp new file mode 100644 index 0000000000000000000000000000000000000000..f7b2f20398faed56586216a9157c7fb443464f60 --- /dev/null +++ b/Grinder/ui/StyleSheet.cpp @@ -0,0 +1,43 @@ +/****************************************************************************** + * File: StyleSheet.cpp + * Date: 02.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "StyleSheet.h" + +QString StyleSheet::loadStyleSheet(QString name) +{ + QFile file(":/css/" + name); + + if (file.open(QIODevice::ReadOnly|QIODevice::Text)) + { + QTextStream stream(&file); + QString sheet = stream.readAll(); + + replacePlaceholders(sheet); + return sheet; + } + else + return ""; +} + +void StyleSheet::replacePlaceholders(QString& styleSheet) +{ + QColor clrBorder = QPalette{}.color(QPalette::Dark); + QColor clrBase = QPalette{}.color(QPalette::Base); + QColor clrLightBg = QPalette{}.color(QPalette::Button).lighter(104); + QColor clrLightGray = QPalette{}.color(QPalette::Dark).lighter(120); + QColor clrHighlight = QPalette{}.color(QPalette::Highlight); + + replaceColorPlaceholder(styleSheet, "%BORDER%", clrBorder); + replaceColorPlaceholder(styleSheet, "%BASE%", clrBase); + replaceColorPlaceholder(styleSheet, "%LIGHTBG%", clrLightBg); + replaceColorPlaceholder(styleSheet, "%LIGHTGRAY%", clrLightGray); + replaceColorPlaceholder(styleSheet, "%HIGHLIGHT%", clrHighlight); +} + +void StyleSheet::replaceColorPlaceholder(QString& styleSheet, QString name, QColor color) +{ + styleSheet.replace(name, color.name()); +} diff --git a/Grinder/ui/StyleSheet.h b/Grinder/ui/StyleSheet.h new file mode 100644 index 0000000000000000000000000000000000000000..ca78ff9fdd07e21c07fe396917e478c125d3beca --- /dev/null +++ b/Grinder/ui/StyleSheet.h @@ -0,0 +1,28 @@ +/****************************************************************************** + * File: StyleSheet.h + * Date: 02.2.2018 + *****************************************************************************/ + +#ifndef GLOBALSTYLESHEET_H +#define GLOBALSTYLESHEET_H + +#include <QString> +#include <QColor> + +namespace grndr +{ + class StyleSheet final + { + public: + static QString loadStyleSheet(QString name); + + private: + StyleSheet() { } + + private: + static void replacePlaceholders(QString& styleSheet); + static void replaceColorPlaceholder(QString& styleSheet, QString name, QColor color); + }; +} + +#endif diff --git a/Grinder/ui/dlg/OptionsDialog.cpp b/Grinder/ui/dlg/OptionsDialog.cpp new file mode 100644 index 0000000000000000000000000000000000000000..655ba9df15c0925d88916d80bc08be21d466a73c --- /dev/null +++ b/Grinder/ui/dlg/OptionsDialog.cpp @@ -0,0 +1,95 @@ +/****************************************************************************** + * File: OptionsDialog.cpp + * Date: 02.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "OptionsDialog.h" +#include "ui_OptionsDialog.h" +#include "core/GrinderApplication.h" + +OptionsDialog::OptionsDialog(QWidget *parent) : QDialog(parent, Qt::Dialog|Qt::WindowTitleHint|Qt::WindowCloseButtonHint), + ui{new Ui::OptionsDialog} +{ + _generalOptions = grinder()->settings().generalOptions(); + _startupOptions = grinder()->settings().startupOptions(); + _imageEditorOptions = grinder()->settings().imageEditorOptions(); + + setupUi(); +} + +OptionsDialog::~OptionsDialog() +{ + delete ui; +} + +void grndr::OptionsDialog::setupUi() +{ + ui->setupUi(this); + + applyGeneralOptions(false); + applyStartupOptions(false); + applyImageEditorOptions(false); +} + +void OptionsDialog::applyGeneralOptions(bool save) +{ + if (save) + _generalOptions.askOnUnsavedChanges = ui->chkAskOnUnsavedChanges->isChecked(); + else + ui->chkAskOnUnsavedChanges->setChecked(_generalOptions.askOnUnsavedChanges); +} + +void OptionsDialog::applyStartupOptions(bool save) +{ + if (save) + _startupOptions.loadLastProject = ui->chkLoadLastProject->isChecked(); + else + ui->chkLoadLastProject->setChecked(_startupOptions.loadLastProject); +} + +void OptionsDialog::applyImageEditorOptions(bool save) +{ + if (save) + { + _imageEditorOptions.groupIntoTabs = ui->chkGroupIntoTabs->isChecked(); + _imageEditorOptions.startUndocked = ui->chkStartUndocked->isChecked(); + _imageEditorOptions.autoFitToWindow = ui->chkAutoFitToWindow->isChecked(); + } + else + { + ui->chkGroupIntoTabs->setChecked(_imageEditorOptions.groupIntoTabs); + ui->chkStartUndocked->setChecked(_imageEditorOptions.startUndocked); + ui->chkAutoFitToWindow->setChecked(_imageEditorOptions.autoFitToWindow); + } +} + +void OptionsDialog::accept() +{ + applyGeneralOptions(true); + applyStartupOptions(true); + applyImageEditorOptions(true); + + grinder()->settings().generalOptions() = _generalOptions; + grinder()->settings().startupOptions() = _startupOptions; + grinder()->settings().imageEditorOptions() = _imageEditorOptions; + grinder()->settings().saveSettings(); + + QDialog::accept(); +} + +void OptionsDialog::on_chkGroupIntoTabs_toggled(bool checked) +{ + ui->chkStartUndocked->setEnabled(!checked); + + if (checked) + ui->chkStartUndocked->setChecked(false); +} + +void OptionsDialog::on_chkStartUndocked_toggled(bool checked) +{ + ui->chkGroupIntoTabs->setEnabled(!checked); + + if (checked) + ui->chkGroupIntoTabs->setChecked(false); +} diff --git a/Grinder/ui/dlg/OptionsDialog.h b/Grinder/ui/dlg/OptionsDialog.h new file mode 100644 index 0000000000000000000000000000000000000000..9811b785c1fc33fc0e4b77e03d87f50b82d91d87 --- /dev/null +++ b/Grinder/ui/dlg/OptionsDialog.h @@ -0,0 +1,51 @@ +/****************************************************************************** + * File: OptionsDialog.h + * Date: 02.3.2018 + *****************************************************************************/ + +#ifndef OPTIONSDIALOG_H +#define OPTIONSDIALOG_H + +#include <QDialog> + +#include "core/GrinderSettings.h" + +namespace Ui +{ + class OptionsDialog; +} + +namespace grndr +{ + class OptionsDialog : public QDialog + { + Q_OBJECT + + public: + OptionsDialog(QWidget *parent = nullptr); + ~OptionsDialog(); + + public slots: + virtual void accept() override; + + private slots: + void on_chkGroupIntoTabs_toggled(bool checked); + void on_chkStartUndocked_toggled(bool checked); + + private: + void setupUi(); + Ui::OptionsDialog *ui; + + private: + void applyGeneralOptions(bool save); + void applyStartupOptions(bool save); + void applyImageEditorOptions(bool save); + + private: + GrinderSettings::GeneralOptions _generalOptions; + GrinderSettings::StartupOptions _startupOptions; + GrinderSettings::ImageEditorOptions _imageEditorOptions; + }; +} + +#endif diff --git a/Grinder/ui/dlg/OptionsDialog.ui b/Grinder/ui/dlg/OptionsDialog.ui new file mode 100644 index 0000000000000000000000000000000000000000..3d593fd182387338242bdb9f8166965dd769987e --- /dev/null +++ b/Grinder/ui/dlg/OptionsDialog.ui @@ -0,0 +1,168 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>OptionsDialog</class> + <widget class="QDialog" name="OptionsDialog"> + <property name="windowModality"> + <enum>Qt::ApplicationModal</enum> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>326</width> + <height>297</height> + </rect> + </property> + <property name="windowTitle"> + <string>Options</string> + </property> + <property name="modal"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <property name="sizeConstraint"> + <enum>QLayout::SetFixedSize</enum> + </property> + <item> + <widget class="QGroupBox" name="groupBox_2"> + <property name="title"> + <string>General options</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="QCheckBox" name="chkAskOnUnsavedChanges"> + <property name="text"> + <string>&Ask on unsaved project changes</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox"> + <property name="title"> + <string>Startup options</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QCheckBox" name="chkLoadLastProject"> + <property name="text"> + <string>&Load last project on startup</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox_3"> + <property name="title"> + <string>Image editor options</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_4"> + <item> + <widget class="QCheckBox" name="chkGroupIntoTabs"> + <property name="text"> + <string>&Group editors into tabs</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="chkStartUndocked"> + <property name="text"> + <string>Start new editors &undocked</string> + </property> + </widget> + </item> + <item> + <widget class="Line" name="line"> + <property name="styleSheet"> + <string notr="true">color: lightgray;</string> + </property> + <property name="frameShadow"> + <enum>QFrame::Plain</enum> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="chkAutoFitToWindow"> + <property name="text"> + <string>Automatically &fit images to window</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::MinimumExpanding</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>0</width> + <height>15</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>chkAskOnUnsavedChanges</tabstop> + <tabstop>chkLoadLastProject</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>OptionsDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>257</x> + <y>208</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>217</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>OptionsDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>208</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>217</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/Grinder/ui/graph/GraphBlockList.cpp b/Grinder/ui/graph/GraphBlockList.cpp new file mode 100644 index 0000000000000000000000000000000000000000..ab01f8265c3b305921a27c17c5303fdd8e307734 --- /dev/null +++ b/Grinder/ui/graph/GraphBlockList.cpp @@ -0,0 +1,178 @@ +/****************************************************************************** + * File: GraphBlockList.cpp + * Date: 03.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "GraphBlockList.h" +#include "core/GrinderApplication.h" +#include "pipeline/PipelineManager.h" +#include "pipeline/BlockCatalog.h" +#include "ui/StyleSheet.h" +#include "ui/graph/GraphView.h" +#include "ui/graph/GraphBlockNode.h" +#include "res/Resources.h" + +#define BLOCKTYPEBUTTON_TYPE_PROPERTY "blockType" + +GraphBlockList::GraphBlockList(QWidget* parent) : QFrame(parent), + _layout{new QVBoxLayout{this}} +{ + setObjectName("BlockStockpileCtrl"); + setStyleSheet(StyleSheet::loadStyleSheet(FILE_STYLESHEET_BLOCKLIST)); + + _layout->setContentsMargins(2, 2, 2, 2); + + _dragInfo.dragImage = QImage{FILE_ICON_BLOCK}.scaled(32, 32, Qt::KeepAspectRatio); + + createBlockList(); + + // Listen for pipeline switching in order to update the button states + connect(&grinder()->pipelineController(), &PipelineController::pipelineSwitched, this, &GraphBlockList::pipelineSwitched); +} + +bool GraphBlockList::eventFilter(QObject* obj, QEvent* event) +{ + auto button = dynamic_cast<QToolButton*>(obj); + + if (button && button->isEnabled() && event->type() == QEvent::MouseButtonPress) // Got a click on a type button + { + QMouseEvent* mouseEvent = static_cast<QMouseEvent*>(event); + + if (mouseEvent->buttons().testFlag(Qt::LeftButton)) + { + _dragInfo.dragInitiated = true; + _dragInfo.mousePressPos = mouseEvent->pos(); + _dragInfo.blockType = button->property(BLOCKTYPEBUTTON_TYPE_PROPERTY).toString(); + } + } + + return QFrame::eventFilter(obj, event); +} + +void GraphBlockList::mouseMoveEvent(QMouseEvent* event) +{ + // If a drag has been initiated and the mouse has been moved far enough, start a drag + if (_dragInfo.dragInitiated) + { + if (event->buttons().testFlag(Qt::LeftButton)) + { + if ((event->pos() - _dragInfo.mousePressPos).manhattanLength() >= QApplication::startDragDistance()) + { + startBlockTypeDrag(); + _dragInfo.dragInitiated = false; + } + } + else + _dragInfo.dragInitiated = false; + } + + QFrame::mouseMoveEvent(event); +} + +void GraphBlockList::createBlockList() +{ + // Add block categories and their associated types + for (auto category : BlockCatalog::getCategories()) + createBlockCategoryEntry(category); + + _layout->addStretch(); + + updateBlockTypeButtonStates(false); +} + +void GraphBlockList::createBlockCategoryEntry(BlockCategory category) +{ + auto catLabel = new QLabel{category}; + catLabel->setAlignment(Qt::AlignHCenter); + catLabel->setFont(GrinderApplication::boldFont(catLabel)); + + // Take fill & border color from the graph style and apply it using CSS + GraphStyle graphStyle; + QColor borderColor = graphStyle.getBlockNodeStyle().getBlockCategoryColor(category); + QColor fillColor = borderColor; + + fillColor.setHsl(fillColor.hslHue(), fillColor.hslSaturation(), 255 - (255 - fillColor.lightness()) * 0.4); + + QString css = QString{"QLabel { border: 1px solid %1; background-color: %2; }"}.arg(borderColor.name()).arg(fillColor.name()); + catLabel->setStyleSheet(css); + + _layout->addWidget(catLabel); + + // Create all types under this category + for (auto type : BlockCatalog::getTypes(category)) + createBlockTypeEntry(type); +} + +void GraphBlockList::createBlockTypeEntry(BlockType type) +{ + auto typeButton = new QToolButton{}; + + // Get the block's description + QString description = BlockCatalog::getDescription(type); + + if (description.isEmpty()) + description = "No description available"; + + typeButton->setText(type); + typeButton->setToolTip(description); + typeButton->setStatusTip(QString{"Create a new %1 block"}.arg(type)); + typeButton->setIcon(QIcon{FILE_ICON_BLOCK}); + typeButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + typeButton->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred); + typeButton->setAutoRaise(true); + typeButton->setProperty(BLOCKTYPEBUTTON_TYPE_PROPERTY, type); + + typeButton->connect(typeButton, SIGNAL(clicked(bool)), this, SLOT(blockTypeButtonPressed())); + typeButton->installEventFilter(this); // Need to capture mouse events of the button + + _layout->addWidget(typeButton); +} + +void GraphBlockList::blockTypeButtonPressed() +{ + BlockType type = sender()->property(BLOCKTYPEBUTTON_TYPE_PROPERTY).toString(); + + if (type != BlockType::Undefined) + { + if (auto pipeline = grinder()->pipelineController().activePipeline()) + { + QString newBlockName = StringUtils::generateUniqueItemName(pipeline->blocks(), type, &Block::getName); + auto block = grinder()->pipelineController().createBlock(type, newBlockName); + + if (auto scene = grinder()->pipelineController().activeScene()) + { + // Find the just-created block node and center on it + auto blockNode = scene->findBlockNode(block.get()); + + if (blockNode) + { + scene->view()->centerOn(blockNode); + + scene->clearSelection(); + blockNode->setSelected(true); + blockNode->setFocus(); + } + } + } + } +} + +void GraphBlockList::updateBlockTypeButtonStates(bool enable) const +{ + for (auto button : findChildren<QToolButton*>()) + button->setEnabled(enable); +} + +void GraphBlockList::startBlockTypeDrag() +{ + // Set the block type as the mime data + QMimeData* mimeData = new QMimeData{}; + mimeData->setData(GRAPHBLOCKLIST_DRAG_MIMETYPE, _dragInfo.blockType.toLatin1()); + + QDrag* drag = new QDrag{this}; + drag->setMimeData(mimeData); + drag->setPixmap(QPixmap::fromImage(_dragInfo.dragImage)); + + drag->exec(Qt::CopyAction, Qt::CopyAction); +} diff --git a/Grinder/ui/graph/GraphBlockList.h b/Grinder/ui/graph/GraphBlockList.h new file mode 100644 index 0000000000000000000000000000000000000000..a1b5f168405a697d2bdb7810a2e37dec6ba0b311 --- /dev/null +++ b/Grinder/ui/graph/GraphBlockList.h @@ -0,0 +1,64 @@ +/****************************************************************************** + * File: GraphBlockList.h + * Date: 03.2.2018 + *****************************************************************************/ + +#ifndef GRAPHBLOCKLIST_H +#define GRAPHBLOCKLIST_H + +#include <QFrame> +#include <QLabel> +#include <QLayout> + +#include "pipeline/BlockCategory.h" +#include "pipeline/BlockType.h" + +#define GRAPHBLOCKLIST_DRAG_MIMETYPE "application/x-blocktype" + +namespace grndr +{ + class PipelineManager; + class Pipeline; + + class GraphBlockList : public QFrame + { + Q_OBJECT + + public: + GraphBlockList(QWidget* parent = nullptr); + + protected: + virtual bool eventFilter(QObject *obj, QEvent* event) override; + + virtual void mouseMoveEvent(QMouseEvent *event) override; + + private: + void createBlockList(); + void createBlockCategoryEntry(BlockCategory category); + void createBlockTypeEntry(BlockType type); + + private slots: + void pipelineSwitched(Pipeline* pipeline) const { updateBlockTypeButtonStates(pipeline != nullptr); } + + void blockTypeButtonPressed(); + + private: + void updateBlockTypeButtonStates(bool enable) const; + + void startBlockTypeDrag(); + + private: + QVBoxLayout* _layout{nullptr}; + + struct + { + bool dragInitiated{false}; + QPoint mousePressPos; + BlockType blockType{BlockType::Undefined}; + + QImage dragImage; + } _dragInfo; + }; +} + +#endif diff --git a/Grinder/ui/graph/GraphBlockNode.cpp b/Grinder/ui/graph/GraphBlockNode.cpp new file mode 100644 index 0000000000000000000000000000000000000000..207defbab22c653fcfba7e397c5dd1fb4d59cbb0 --- /dev/null +++ b/Grinder/ui/graph/GraphBlockNode.cpp @@ -0,0 +1,460 @@ +/****************************************************************************** + * File: GraphBlockNode.cpp + * Date: 22.1.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "GraphBlockNode.h" +#include "GraphStyle.h" +#include "GraphScene.h" +#include "GraphPortNode.h" +#include "pipeline/Block.h" +#include "pipeline/BlockCatalog.h" +#include "core/GrinderApplication.h" +#include "controller/PipelineController.h" +#include "res/Resources.h" + +#include <QFontMetricsF> + +GraphBlockNode::GraphBlockNode(grndr::GraphScene* scene, const std::shared_ptr<Block>& block, QGraphicsItem* parent) : GraphNode(scene, parent), + _block{block}, _blockStyle{_style.getBlockNodeStyle()}, _nameEdit{new QLineEdit{}} +{ + if (!block) + throw std::invalid_argument{_EXCPT("block may not be null")}; + + setFlag(ItemIsMovable); + + // Add all ports (in & out) as node children; must be created before updating the geometry + createPorts(); + + updateGeometry(); + + _nameEdit->setStyleSheet("QLineEdit { background: transparent; border: none; }"); + _nameEdit->setAlignment(Qt::AlignCenter); + _nameEdit->setFont(_blockStyle.nameFont); + _nameEdit->setContextMenuPolicy(Qt::NoContextMenu); + _nameEdit->setVisible(false); + + _nameEditWidget = new QGraphicsProxyWidget{this}; + _nameEditWidget->setWidget(_nameEdit); + + // Create node actions + _renameAction = createNodeAction("&Rename block", FILE_ICON_EDIT, SLOT(beginRenameBlock()), "Rename the current block", "F2"); + _deleteAction->setText("&Delete block"); + + // Set the tooltip to the block's description + QString description = BlockCatalog::getDescription(block->getType()); + setToolTip(!description.isEmpty() ? description : "No description available"); +} + +void GraphBlockNode::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) +{ + Q_UNUSED(option); + Q_UNUSED(widget); + + painter->setRenderHints(QPainter::Antialiasing|QPainter::TextAntialiasing|QPainter::HighQualityAntialiasing|QPainter::SmoothPixmapTransform); + + // Draw the node background (including outer glow if selected) + if (isSelected()) + { + bool inactive = !_scene->hasFocus(); + + painter->save(); + painter->setPen(QPen{Qt::NoPen}); + painter->setBrush(inactive ? _blockStyle.selectionColorInactive : _blockStyle.selectionColor); + painter->setOpacity(_blockStyle.selectionOpacity); + painter->drawRoundedRect(_nodeRectSelected, _blockStyle.borderRadius, _blockStyle.borderRadius); + painter->restore(); + } + + painter->fillRect(_nodeRect, _blockStyle.lightBackgroundColor); + + // Draw node contents + if (auto block = _block.lock()) // Make sure that the underlying block still exists + drawHeader(painter, block.get()); + + // Draw node borders + auto borderWidth = _blockStyle.borderWidth; + auto borderWidthHalf = borderWidth * 0.5; + + QPen pen{_blockStyle.borderColor, borderWidth}; + pen.setCapStyle(Qt::RoundCap); + pen.setJoinStyle(Qt::RoundJoin); + + painter->setPen(pen); + painter->drawRoundedRect(_nodeRect, _blockStyle.borderRadius, _blockStyle.borderRadius); + + qreal y = _nodeRect.top() + _geometry.headerRect.height() - borderWidthHalf; + painter->drawLine(_nodeRect.left() + borderWidthHalf, y, _nodeRect.right() - borderWidthHalf, y); +} + +void GraphBlockNode::beginRenameBlock() +{ + if (_isEditingName) + return; + + if (auto block = _block.lock()) // Make sure that the underlying block still exists + { + // Show in-place edit field + connect(_nameEdit, &QLineEdit::editingFinished, this, &GraphBlockNode::endRenameBlock); + + auto headerRect = _nodeRect; + headerRect.setHeight(_geometry.headerNameSize.height()); + + _nameEditWidget->setGeometry(headerRect); + _nameEditWidget->setVisible(true); + + _nameEdit->setText(block->getName()); + _nameEdit->selectAll(); + _nameEdit->setFocus(); + + _isEditingName = true; + + update(); + } +} + +void GraphBlockNode::endRenameBlock() +{ + if (!_isEditingName) + return; + + disconnect(_nameEdit, nullptr, this, nullptr); + + if (auto block = _block.lock()) // Make sure that the underlying block still exists + { + // Hide in-place edit field and commit new name + auto name = _nameEdit->text().trimmed(); + _nameEditWidget->setVisible(false); + + if (!name.isEmpty()) + { + grinder()->pipelineController().renameBlock(block.get(), name); + + prepareGeometryChange(); + updateGeometry(); + } + } + + _isEditingName = false; + + update(); + setFocus(); +} + +void GraphBlockNode::cancelRenameBlock() +{ + if (!_isEditingName) + return; + + // Hide in-place edit field w/o committing new name + disconnect(_nameEdit, nullptr, this, nullptr); + + _nameEditWidget->setVisible(false); + + _isEditingName = false; + + update(); + setFocus(); +} + +void GraphBlockNode::keyReleaseEvent(QKeyEvent* event) +{ + if (event->modifiers() == Qt::NoModifier) + { + if (event->key() == Qt::Key_F2 && !_isEditingName) // Begin rename + { + beginRenameBlock(); + return; + } + else if (event->key() == Qt::Key_Escape) + { + if (_isEditingName) // Cancel name edit + cancelRenameBlock(); + else if (_connectionBlueprint) // Cancel connection initiation + endConnectionInitiation(false); + + return; + } + } + + GraphNode::keyReleaseEvent(event); +} + +void GraphBlockNode::mousePressEvent(QGraphicsSceneMouseEvent* event) +{ + if (event->button() == Qt::LeftButton) + { + if (!_connectionBlueprint) // Check if a new connection initiation must be started + { + // See if a port rect has been clicked; if so, initiate a new connection + if (auto portNode = findPortByPosition(event->pos())) + beginConnectionInitiation(portNode); + + updateConnectionInitiation(event->scenePos()); + } + } + + if (!_connectionBlueprint) + GraphNode::mousePressEvent(event); +} + +void GraphBlockNode::mouseMoveEvent(QGraphicsSceneMouseEvent* event) +{ + if (_connectionBlueprint) + updateConnectionInitiation(event->scenePos()); + else + GraphNode::mouseMoveEvent(event); +} + +void GraphBlockNode::mouseReleaseEvent(QGraphicsSceneMouseEvent* event) +{ + // Mouse-button has been released after a drag operation, so end any connection initiation + if (_connectionBlueprint) + endConnectionInitiation(); + + GraphNode::mouseReleaseEvent(event); +} + +void GraphBlockNode::mouseDoubleClickEvent(QGraphicsSceneMouseEvent* event) +{ + // If the header is double-click, initiate rename + if (_geometry.headerRect.contains(event->pos())) + beginRenameBlock(); +} + +void GraphBlockNode::updateGeometry() +{ + if (auto block = _block.lock()) // Make sure that the underlying block still exists + { + _geometry.headerNameSize = calcHeaderNameSize(block.get()); + _geometry.headerTypeSize = calcHeaderTypeSize(block.get()); + _geometry.inPortsSize = calcPortsSize(_inPortNodes); + _geometry.outPortsSize = calcPortsSize(_outPortNodes); + + auto totalWidth = std::max({_geometry.headerNameSize.width(), _geometry.headerTypeSize.width(), _geometry.inPortsSize.width() + _geometry.outPortsSize.width()}); + auto totalHeight = _geometry.headerNameSize.height() + _geometry.headerTypeSize.height() + std::max(_geometry.inPortsSize.height(), _geometry.outPortsSize.height()); + + auto minSize = _blockStyle.minimumSize; + _nodeRect = QRectF{0, 0, std::max(totalWidth, minSize.width()), std::max(totalHeight, minSize.height())}; + _nodeRectSelected = _nodeRect + QMarginsF{_blockStyle.selectionMargin, _blockStyle.selectionMargin, _blockStyle.selectionMargin, _blockStyle.selectionMargin}; + + _geometry.headerRect.setRect(_nodeRect.left(), _nodeRect.top(), _nodeRect.width(), _geometry.headerNameSize.height() + _geometry.headerTypeSize.height()); + _geometry.portsRect.setRect(_nodeRect.left(), _geometry.headerRect.bottom(), _nodeRect.width(), std::max(_geometry.inPortsSize.height(), _geometry.outPortsSize.height())); + } + + // The geometry has been updated to make enough room for all ports, so bring them into layout + layoutPorts(_inPortNodes, false); + layoutPorts(_outPortNodes, true); + + GraphNode::updateGeometry(); +} + +std::vector<QAction*> GraphBlockNode::getNodeActions(QMenu& menu) const +{ + std::vector<QAction*> actions = GraphNode::getNodeActions(menu); + actions.insert(actions.begin(), _renameAction); + return actions; +} + +QSizeF GraphBlockNode::calcHeaderNameSize(const Block* block) const +{ + auto marginsX = _blockStyle.textMargins.left() + _blockStyle.textMargins.right(); + auto marginsY = _blockStyle.textMargins.top() + _blockStyle.textMargins.bottom(); + + QFontMetricsF fontNameMetrics{_blockStyle.nameFont}; + auto nameWidth = fontNameMetrics.width(block->getName()) + marginsX; + auto nameHeight = fontNameMetrics.height() + marginsY; + + return QSizeF{nameWidth, nameHeight}; +} + +QSizeF GraphBlockNode::calcHeaderTypeSize(const Block* block) const +{ + auto marginsX = _blockStyle.textMargins.left() + _blockStyle.textMargins.right(); + auto marginsY = _blockStyle.textMargins.top() + _blockStyle.textMargins.bottom(); + + QFontMetricsF fontTypeMetrics{_blockStyle.typeFont}; + auto typeWidth = fontTypeMetrics.width(block->getType()) + marginsX; + auto typeHeight = fontTypeMetrics.height() + marginsY; + + return QSizeF{typeWidth, typeHeight}; +} + +QSizeF GraphBlockNode::calcPortsSize(const std::vector<GraphPortNode*>& ports) const +{ + QSizeF size{0.0, 0.0}; + + for (auto portNode : ports) + { + auto rect = portNode->boundingRect() + _blockStyle.portMargins; + + size.setWidth(std::max(size.width(), rect.width())); + size.setHeight(size.height() + rect.height()); + } + + return size; +} + +void GraphBlockNode::drawHeader(QPainter* painter, const Block* block) +{ + painter->setPen(_blockStyle.textColor); + + auto rect = _geometry.headerRect; + + // Draw block name + rect.setBottom(rect.top() + _geometry.headerNameSize.height()); + + QLinearGradient grad{0.0, rect.top(), 0.0, rect.bottom()}; + grad.setColorAt(0.0, _blockStyle.getBlockCategoryColor(block->getCategory())); + grad.setColorAt(1.0, _blockStyle.lightBackgroundColor); + + painter->fillRect(rect, QBrush{grad}); + + if (!_isEditingName) + { + painter->setFont(_blockStyle.nameFont); + painter->drawText(rect, Qt::AlignHCenter|Qt::AlignVCenter, block->getName()); + } + + // Draw block type + rect.setTop(rect.bottom()); + rect.setBottom(rect.top() + _geometry.headerTypeSize.height()); + + painter->setFont(_blockStyle.typeFont); + painter->drawText(rect, Qt::AlignHCenter|Qt::AlignTop, block->getType()); +} + +void GraphBlockNode::createPorts() +{ + if (auto block = _block.lock()) // Make sure that the underlying block still exists + { + // Add incoming ports + auto inPorts = block->ports().selectByDirection(Port::Direction::In); + + for (const auto& port : inPorts) + { + GraphPortNode* portNode = new GraphPortNode{this, _scene, port, this}; + _inPortNodes.push_back(portNode); + } + + // Add outgoing ports + auto outPorts = block->ports().selectByDirection(Port::Direction::Out); + + for (const auto& port : outPorts) + { + GraphPortNode* portNode = new GraphPortNode{this, _scene, port, this}; + _outPortNodes.push_back(portNode); + } + } +} + +void GraphBlockNode::layoutPorts(const std::vector<grndr::GraphPortNode*>& ports, bool alignRight) +{ + QPointF pos{alignRight ? _geometry.portsRect.topRight() : _geometry.portsRect.topLeft()}; + + // Move all ports into their proper place; in-ports on the left, out-ports on the right + for (const auto& portNode : ports) + { + auto rect = portNode->boundingRect(); + auto x = alignRight ? pos.x() - rect.width() - _blockStyle.portMargins.right() : pos.x() + _blockStyle.portMargins.left(); + auto y = pos.y() + _blockStyle.portMargins.top(); + + portNode->setPos(x, y); + pos.setY(pos.y() + rect.height() + _blockStyle.portMargins.top() + _blockStyle.portMargins.bottom()); + } +} + +GraphPortNode* GraphBlockNode::findPortByPosition(QPointF pos) +{ + // See if there is a port at position pos + auto findPort = [pos](const auto& nodes) -> GraphPortNode* { + for (const auto& node : nodes) + { + auto rect = node->mapRectToParent(node->getPortRect()); + + if (rect.contains(pos)) + return node; + } + + return nullptr; + }; + + auto port = findPort(_inPortNodes); + + if (!port) + port = findPort(_outPortNodes); + + return port; +} + +void GraphBlockNode::beginConnectionInitiation(GraphPortNode* portNode) +{ + if (!_connectionBlueprint) + { + _connectionBlueprint = std::make_unique<GraphConnectionBlueprint>(_scene, portNode); + + _scene->addItem(_connectionBlueprint.get()); + _scene->update(); + } +} + +void GraphBlockNode::updateConnectionInitiation(QPointF mousePos) +{ + if (_connectionBlueprint) + { + _connectionBlueprint->setTargetPos(mousePos); + + // See if there is a block node under the cursor + bool lockOnPort = false; + + for (auto item : _scene->items(mousePos)) + { + if (auto blockNode = dynamic_cast<GraphBlockNode*>(item)) + { + if (blockNode) + { + // We're above a block, so let's see if we are also above a port + if (auto portNode = blockNode->findPortByPosition(blockNode->mapFromScene(mousePos))) + { + _connectionBlueprint->setTargetPortNode(portNode); + + lockOnPort = true; + break; + } + } + } + } + + if (!lockOnPort) + _connectionBlueprint->setTargetPortNode(nullptr); + + _scene->update(); + } +} + +void GraphBlockNode::endConnectionInitiation(bool createConnection) +{ + if (_connectionBlueprint) + { + if (createConnection) + { + auto sourcePort = _connectionBlueprint->connectionPort(Port::Direction::Out); + auto destPort = _connectionBlueprint->connectionPort(Port::Direction::In); + + try { + grinder()->pipelineController().validateConnection(sourcePort, destPort); + + // We're fine, the connection is valid + grinder()->pipelineController().createConnection(sourcePort, destPort); + } catch (...) { + // Ignore any exceptions (simply don't add the invalid connection) + } + } + + _scene->removeItem(_connectionBlueprint.get()); + _scene->update(); + + _connectionBlueprint.reset(nullptr); + } +} diff --git a/Grinder/ui/graph/GraphBlockNode.h b/Grinder/ui/graph/GraphBlockNode.h new file mode 100644 index 0000000000000000000000000000000000000000..8d6e0d61f9ea7f6b989050447e8ee5db99ad7f02 --- /dev/null +++ b/Grinder/ui/graph/GraphBlockNode.h @@ -0,0 +1,109 @@ +/****************************************************************************** + * File: GraphBlockNode.h + * Date: 22.1.2018 + *****************************************************************************/ + +#ifndef GRAPHBLOCKNODE_H +#define GRAPHBLOCKNODE_H + +#include <memory> +#include <QLineEdit> +#include <QGraphicsProxyWidget> + +#include "GraphNode.h" +#include "GraphStyle.h" +#include "GraphConnectionBlueprint.h" + +namespace grndr +{ + class Block; + class GraphPortNode; + class GraphConnectionBlueprint; + + class GraphBlockNode : public GraphNode + { + Q_OBJECT + + friend class GraphLayout; + + public: + GraphBlockNode(GraphScene* scene, const std::shared_ptr<Block>& block, QGraphicsItem* parent = nullptr); + + public: + std::weak_ptr<Block>& block() { return _block; } + const std::weak_ptr<Block>& block() const { return _block; } + + const std::vector<GraphPortNode*>& inPortNodes() const { return _inPortNodes; } + const std::vector<GraphPortNode*>& outPortNodes() const { return _outPortNodes; } + + public: + virtual void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) override; + + public slots: + void beginRenameBlock(); + void endRenameBlock(); + void cancelRenameBlock(); + + protected: + virtual void keyReleaseEvent(QKeyEvent* event) override; + + virtual void mousePressEvent(QGraphicsSceneMouseEvent* event) override; + virtual void mouseMoveEvent(QGraphicsSceneMouseEvent* event) override; + virtual void mouseReleaseEvent(QGraphicsSceneMouseEvent* event) override; + virtual void mouseDoubleClickEvent(QGraphicsSceneMouseEvent* event) override; + + protected: + virtual void updateGeometry() override; + + virtual std::vector<QAction*> getNodeActions(QMenu& menu) const override; + + private: + QSizeF calcHeaderNameSize(const Block* block) const; + QSizeF calcHeaderTypeSize(const Block* block) const; + QSizeF calcPortsSize(const std::vector<GraphPortNode*>& ports) const; + + struct + { + QRectF headerRect; + QSizeF headerNameSize; + QSizeF headerTypeSize; + + QRectF portsRect; + QSizeF inPortsSize; + QSizeF outPortsSize; + } _geometry; + + private: + void drawHeader(QPainter* painter, const Block* block); + + private: + void createPorts(); + void layoutPorts(const std::vector<GraphPortNode*>& ports, bool alignRight); + + GraphPortNode* findPortByPosition(QPointF pos); + + protected: + std::weak_ptr<Block> _block; + + const GraphStyle::BlockNodeStyle& _blockStyle; + + std::vector<GraphPortNode*> _inPortNodes; + std::vector<GraphPortNode*> _outPortNodes; + + private: + QLineEdit* _nameEdit{nullptr}; + QGraphicsProxyWidget* _nameEditWidget{nullptr}; + bool _isEditingName{false}; + + QAction* _renameAction; + + private: + void beginConnectionInitiation(GraphPortNode* portNode); + void updateConnectionInitiation(QPointF mousePos); + void endConnectionInitiation(bool createConnection = true); + + std::unique_ptr<GraphConnectionBlueprint> _connectionBlueprint; + }; +} + +#endif diff --git a/Grinder/ui/graph/GraphConnectionBase.cpp b/Grinder/ui/graph/GraphConnectionBase.cpp new file mode 100644 index 0000000000000000000000000000000000000000..1380a4d8969aa9e7464af96dabae5c2096eae373 --- /dev/null +++ b/Grinder/ui/graph/GraphConnectionBase.cpp @@ -0,0 +1,84 @@ +/****************************************************************************** + * File: GraphConnectionBase.cpp + * Date: 27.1.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "GraphConnectionBase.h" + +GraphConnectionBase::GraphConnectionBase(GraphScene* scene, QGraphicsItem* parent) : GraphNode(scene, parent), + _connectionStyle{_style.getConnectionNodeStyle()} +{ + +} + +QPainterPath GraphConnectionBase::shape() const +{ + // Create a shape that is slightly thicker than the shown connection line + QPen pen; + pen.setJoinStyle(Qt::RoundJoin); + pen.setWidthF(_connectionStyle.lineWidth + _connectionStyle.selectionMargin); + + QPainterPathStroker ps{pen}; + auto path = ps.createStroke(_geometry.connectionPath); + path.addPath(_geometry.connectionPath); + + return path; +} + +QRectF GraphConnectionBase::boundingRect() const +{ + return _geometry.connectionPath.controlPointRect(); +} + +void GraphConnectionBase::drawConnection(QPainter* painter, QColor sourceColor, QColor destColor) +{ + // Draw path using a gradient (source color -> dest color) + QLinearGradient gradient(_geometry.sourcePosition, _geometry.destPosition); + gradient.setColorAt(0.0, sourceColor); + gradient.setColorAt(1.0, destColor); + + QPen connectionPen{QBrush{gradient}, _connectionStyle.lineWidth}; + connectionPen.setCapStyle(Qt::SquareCap); + connectionPen.setJoinStyle(Qt::RoundJoin); + + painter->setPen(connectionPen); + painter->drawPath(_geometry.connectionPath); +} + +void GraphConnectionBase::updateGeometry() +{ + // Calculate the scene-relative positions of the in & out ports + _geometry.sourcePosition = calcSourcePosition(); + _geometry.destPosition = calcDestPosition(); + + // Update the connection path + QPainterPath path{_geometry.sourcePosition}; + + // Source lies right of dest, so draw two curves + if (_geometry.sourcePosition.x() > _geometry.destPosition.x()) + { + auto yDiff = std::abs(_geometry.destPosition.y() - _geometry.sourcePosition.y()); + auto middleY = (_geometry.sourcePosition.y() + _geometry.destPosition.y()) / 2.0; + + auto curve1End = QPointF{_geometry.sourcePosition.x(), middleY}; + auto curve1CP1 = QPointF{_geometry.sourcePosition.x() + yDiff / 2.0, _geometry.sourcePosition.y()}; + auto curve1CP2 = QPointF{_geometry.sourcePosition.x() + yDiff / 2.0, middleY}; + + auto curve2Start = QPointF{_geometry.destPosition.x(), middleY}; + auto curve2CP1 = QPointF{_geometry.destPosition.x() - yDiff / 2.0, middleY}; + auto curve2CP2 = QPointF{_geometry.destPosition.x() - yDiff / 2.0, _geometry.destPosition.y()}; + + path.cubicTo(curve1CP1, curve1CP2, curve1End); + path.lineTo(curve2Start); + path.cubicTo(curve2CP1, curve2CP2, _geometry.destPosition); + } + else // Otherwise, draw a cubic line from source to dest + path.cubicTo((_geometry.sourcePosition.x() + _geometry.destPosition.x()) / 2.0, _geometry.sourcePosition.y(), + (_geometry.sourcePosition.x() + _geometry.destPosition.x()) / 2.0, _geometry.destPosition.y(), + _geometry.destPosition.x(), _geometry.destPosition.y()); + + _geometry.connectionPath = path; + + GraphNode::updateGeometry(); +} diff --git a/Grinder/ui/graph/GraphConnectionBase.h b/Grinder/ui/graph/GraphConnectionBase.h new file mode 100644 index 0000000000000000000000000000000000000000..549ae22c5519dddc3a56e43f52f801486cb5be78 --- /dev/null +++ b/Grinder/ui/graph/GraphConnectionBase.h @@ -0,0 +1,47 @@ +/****************************************************************************** + * File: GraphConnectionBase.h + * Date: 27.1.2018 + *****************************************************************************/ + +#ifndef GRAPHCONNECTIONBASE_H +#define GRAPHCONNECTIONBASE_H + +#include "GraphNode.h" +#include "GraphStyle.h" + +namespace grndr +{ + class GraphConnectionBase : public GraphNode + { + Q_OBJECT + + public: + GraphConnectionBase(GraphScene* scene, QGraphicsItem* parent = nullptr); + + public: + virtual QPainterPath shape() const override; + virtual QRectF boundingRect() const override; + + protected: + void drawConnection(QPainter* painter, QColor sourceColor, QColor destColor); + + protected: + virtual void updateGeometry() override; + + virtual QPointF calcSourcePosition() const = 0; + virtual QPointF calcDestPosition() const = 0; + + struct + { + QPointF sourcePosition; + QPointF destPosition; + + QPainterPath connectionPath; + } _geometry; + + protected: + const GraphStyle::ConnectionNodeStyle& _connectionStyle; + }; +} + +#endif diff --git a/Grinder/ui/graph/GraphConnectionBlueprint.cpp b/Grinder/ui/graph/GraphConnectionBlueprint.cpp new file mode 100644 index 0000000000000000000000000000000000000000..c68508e691556ac5109934e8c1e89e8ba07a6684 --- /dev/null +++ b/Grinder/ui/graph/GraphConnectionBlueprint.cpp @@ -0,0 +1,150 @@ +/****************************************************************************** + * File: GraphConnectionBlueprint.cpp + * Date: 27.1.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "GraphConnectionBlueprint.h" +#include "GraphBlockNode.h" +#include "GraphPortNode.h" +#include "GraphScene.h" +#include "core/GrinderApplication.h" +#include "controller/PipelineController.h" + +GraphConnectionBlueprint::GraphConnectionBlueprint(GraphScene* scene, grndr::GraphPortNode* startPortNode, QGraphicsItem* parent) : GraphConnectionBase(scene, parent), + _startPortNode{startPortNode} +{ + if (!startPortNode) + throw std::invalid_argument{_EXCPT("startPortNode may not be null")}; + + setFlag(ItemIsFocusable, false); + setFlag(ItemIsSelectable, false); + setFlag(ItemSendsGeometryChanges, false); + + // Make sure that the blueprint is always drawn on top + setZValue(5.0f); + + // Create a hidden text node for showing the rejection reason + _statusMessageNode = new GraphConnectionMessage{_scene, this}; + _statusMessageNode->setZValue(zValue() + 1.0); + _statusMessageNode->setVisible(false); +} + +void GraphConnectionBlueprint::setTargetPortNode(GraphPortNode* node) +{ + _targetPortNode = node; + + if (_targetPortNode && _targetPortNode != _startPortNode) // A (potential) connection has been established + { + validateConnection(); + } + else + { + _status = Status::Pending; + _statusMessageNode->setVisible(false); + } + + prepareGeometryChange(); + updateGeometry(); +} + +void GraphConnectionBlueprint::setTargetPos(QPointF pos) +{ + _targetPos = pos; + + prepareGeometryChange(); + updateGeometry(); +} + +Port* GraphConnectionBlueprint::_connectionPort(Port::Direction dir) const +{ + // Find the connection port that matches the given direction + auto startPort = _startPortNode ? _startPortNode->port().lock() : nullptr; + auto targetPort = _targetPortNode ? _targetPortNode->port().lock() : nullptr; + + if (startPort && startPort->getDirection() == dir) + return startPort.get(); + else if (targetPort && targetPort->getDirection() == dir) + return targetPort.get(); + else + return nullptr; +} + +void GraphConnectionBlueprint::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) +{ + Q_UNUSED(option); + Q_UNUSED(widget); + + painter->setRenderHints(QPainter::Antialiasing|QPainter::TextAntialiasing|QPainter::HighQualityAntialiasing|QPainter::SmoothPixmapTransform); + painter->setOpacity(_connectionStyle.blueprintOpacity); + + QColor color; + + switch (_status) + { + case Status::Valid: + color = _connectionStyle.blueprintValidColor; + break; + + case Status::Invalid: + color = _connectionStyle.blueprintInvalidColor; + break; + + default: + color = _connectionStyle.blueprintPendingColor; + } + + drawConnection(painter, color, color); +} + +QPointF GraphConnectionBlueprint::calcSourcePosition() const +{ + auto portNode = _targetPortNode; + auto startPort = _startPortNode->port().lock(); + + if (startPort && startPort->isOut()) + portNode = _startPortNode; + + if (portNode) + return portNode->blockNode()->mapToScene(portNode->getCenterPos(false)); + else + return _targetPos; +} + +QPointF GraphConnectionBlueprint::calcDestPosition() const +{ + auto portNode = _targetPortNode; + auto startPort = _startPortNode->port().lock(); + + if (startPort && startPort->isIn()) + portNode = _startPortNode; + + if (portNode) + return portNode->blockNode()->mapToScene(portNode->getCenterPos(false)); + else + return _targetPos; +} + +void GraphConnectionBlueprint::validateConnection() +{ + _status = Status::Invalid; + _statusMessage = "Unknown rejection reason"; + + auto sourcePort = connectionPort(Port::Direction::Out); + auto destPort = connectionPort(Port::Direction::In); + + try { + grinder()->pipelineController().validateConnection(sourcePort, destPort); + + // We're fine, the connection is valid + _status = Status::Valid; + _statusMessage = ""; + } catch (std::exception& e) { + _statusMessage = GetExceptionMessage(e.what()); + } + + // Update the status message node + _statusMessageNode->setVisible(_status == Status::Invalid); + _statusMessageNode->setText(_statusMessage); + _statusMessageNode->setPos(boundingRect().center() - _statusMessageNode->boundingRect().center()); +} diff --git a/Grinder/ui/graph/GraphConnectionBlueprint.h b/Grinder/ui/graph/GraphConnectionBlueprint.h new file mode 100644 index 0000000000000000000000000000000000000000..ef48f61c2faffad9da1660beb6d2fc3c5ba30aa1 --- /dev/null +++ b/Grinder/ui/graph/GraphConnectionBlueprint.h @@ -0,0 +1,67 @@ +/****************************************************************************** + * File: GraphConnectionBlueprint.h + * Date: 27.1.2018 + *****************************************************************************/ + +#ifndef GRAPHCONNECTIONBLUEPRINT_H +#define GRAPHCONNECTIONBLUEPRINT_H + +#include "GraphConnectionBase.h" +#include "GraphConnectionMessage.h" +#include "pipeline/Port.h" + +namespace grndr +{ + class GraphPortNode; + + class GraphConnectionBlueprint : public GraphConnectionBase + { + public: + enum class Status + { + Pending, + Valid, + Invalid, + }; + + public: + GraphConnectionBlueprint(GraphScene* scene, GraphPortNode* startPortNode, QGraphicsItem* parent = nullptr); + + public: + void setTargetPortNode(GraphPortNode* node); + void setTargetPos(QPointF pos); + + GraphPortNode* startPortNode() { return _startPortNode; } + const GraphPortNode* startPortNode() const { return _startPortNode; } + GraphPortNode* targetPortNode() { return _targetPortNode; } + const GraphPortNode* targetPortNode() const { return _targetPortNode; } + + Port* connectionPort(Port::Direction dir) { return _connectionPort(dir); } + const Port* connectionPort(Port::Direction dir) const { return _connectionPort(dir); } + + public: + virtual void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) override; + + protected: + virtual QPointF calcSourcePosition() const override; + virtual QPointF calcDestPosition() const override; + + private: + void validateConnection(); + + private: + Port* _connectionPort(Port::Direction dir) const; + + private: + GraphPortNode* _startPortNode{nullptr}; + GraphPortNode* _targetPortNode{nullptr}; + + QPointF _targetPos; + + Status _status{Status::Pending}; + QString _statusMessage{""}; + GraphConnectionMessage* _statusMessageNode{nullptr}; + }; +} + +#endif diff --git a/Grinder/ui/graph/GraphConnectionMessage.cpp b/Grinder/ui/graph/GraphConnectionMessage.cpp new file mode 100644 index 0000000000000000000000000000000000000000..5623beb48b721cb0d0dab93ac6e661e9c3e4e4a3 --- /dev/null +++ b/Grinder/ui/graph/GraphConnectionMessage.cpp @@ -0,0 +1,49 @@ +/****************************************************************************** + * File: GraphConnectionMessage.cpp + * Date: 29.1.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "GraphConnectionMessage.h" + +GraphConnectionMessage::GraphConnectionMessage(grndr::GraphScene* scene, QGraphicsItem* parent) : GraphNode(scene, parent), + _connectionStyle{_style.getConnectionMessageStyle()} +{ + setFlag(ItemIsFocusable, false); + setFlag(ItemIsSelectable, false); + setFlag(ItemSendsGeometryChanges, false); +} + + +void GraphConnectionMessage::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) +{ + Q_UNUSED(option); + Q_UNUSED(widget); + + painter->setRenderHints(QPainter::Antialiasing|QPainter::TextAntialiasing|QPainter::HighQualityAntialiasing|QPainter::SmoothPixmapTransform); + + // Draw the background + QColor color = _connectionStyle.blueprintInvalidColor; + + painter->setOpacity(_connectionStyle.messageOpacity); + + painter->save(); + painter->setPen(QPen{color, 0.0}); + painter->setBrush(color); + painter->drawRoundedRect(_nodeRect, _connectionStyle.borderRadius, _connectionStyle.borderRadius); + painter->restore(); + + // Draw the text + painter->setPen(_connectionStyle.messageColor); + painter->setFont(_connectionStyle.messageFont); + painter->drawText(_nodeRect, Qt::AlignHCenter|Qt::AlignVCenter, _text); +} + +void GraphConnectionMessage::updateGeometry() +{ + QFontMetricsF fontMetrics{_connectionStyle.messageFont}; + _geometry._textRect = QRectF{0.0, 0.0, fontMetrics.width(_text), fontMetrics.height()}; + + _nodeRect = _geometry._textRect + _connectionStyle.messageMargins; + _nodeRectSelected = _nodeRect; +} diff --git a/Grinder/ui/graph/GraphConnectionMessage.h b/Grinder/ui/graph/GraphConnectionMessage.h new file mode 100644 index 0000000000000000000000000000000000000000..cddff0aec41f78ad1f54f1eaaefa28c3a3bca541 --- /dev/null +++ b/Grinder/ui/graph/GraphConnectionMessage.h @@ -0,0 +1,41 @@ +/****************************************************************************** + * File: GraphConnectionMessage.h + * Date: 29.1.2018 + *****************************************************************************/ + +#ifndef GRAPHCONNECTIONMESSAGE_H +#define GRAPHCONNECTIONMESSAGE_H + +#include "GraphNode.h" +#include "GraphStyle.h" + +namespace grndr +{ + class GraphConnectionMessage : public GraphNode + { + public: + GraphConnectionMessage(GraphScene* scene, QGraphicsItem *parent = nullptr); + + public: + QString text() const { return _text; } + void setText(QString text) { _text = text; prepareGeometryChange(); updateGeometry(); } + + public: + virtual void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) override; + + protected: + virtual void updateGeometry() override; + + struct + { + QRectF _textRect; + } _geometry; + + private: + QString _text; + + const GraphStyle::ConnectionMessageStyle& _connectionStyle; + }; +} + +#endif diff --git a/Grinder/ui/graph/GraphConnectionNode.cpp b/Grinder/ui/graph/GraphConnectionNode.cpp new file mode 100644 index 0000000000000000000000000000000000000000..859e4ed1f1653a2b8b0db7c913bda2013c882313 --- /dev/null +++ b/Grinder/ui/graph/GraphConnectionNode.cpp @@ -0,0 +1,143 @@ +/****************************************************************************** + * File: GraphConnectionNode.cpp + * Date: 25.1.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "GraphConnectionNode.h" +#include "GraphBlockNode.h" +#include "GraphPortNode.h" +#include "GraphScene.h" +#include "pipeline/Block.h" +#include "pipeline/Port.h" +#include "pipeline/Connection.h" + +GraphConnectionNode::GraphConnectionNode(GraphScene* scene, const std::shared_ptr<Connection>& con, QGraphicsItem* parent) : GraphConnectionBase(scene, parent), + _connection{con} +{ + if (!con) + throw std::invalid_argument{_EXCPT("con may not be null")}; + + connectPortSignals(); + + updateGeometry(); + + // Create node actions + _deleteAction->setText("&Remove connection"); + + // Set the tooltip showing information about this connection + updateToolTip(); +} + +GraphPortNode* GraphConnectionNode::_sourcePortNode() const +{ + if (auto con = _connection.lock()) // Make sure that the underlying connection still exists + return _scene->findPortNode(con->sourcePort()); + else + return nullptr; +} + +GraphPortNode* GraphConnectionNode::_destPortNode() const +{ + if (auto con = _connection.lock()) // Make sure that the underlying connection still exists + return _scene->findPortNode(con->destPort()); + else + return nullptr; +} + +void GraphConnectionNode::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) +{ + Q_UNUSED(option); + Q_UNUSED(widget); + + painter->setRenderHints(QPainter::Antialiasing|QPainter::TextAntialiasing); + + if (auto con = _connection.lock()) // Make sure that the underlying connection still exists + { + if (isSelected()) // Draw outer glow if selected + { + bool inactive = !_scene->hasFocus(); + QPen selectionPen{QBrush{inactive ? _connectionStyle.selectionColorInactive : _connectionStyle.selectionColor}, _connectionStyle.lineWidth + _connectionStyle.selectionMargin}; + selectionPen.setJoinStyle(Qt::RoundJoin); + + painter->save(); + painter->setPen(selectionPen); + painter->setOpacity(_connectionStyle.selectionOpacity); + painter->drawPath(_geometry.connectionPath); + painter->restore(); + } + + drawConnection(painter, getPortColor(sourcePortNode()), getPortColor(destPortNode())); + } +} + +QPointF GraphConnectionNode::calcSourcePosition() const +{ + if (auto portNode = sourcePortNode()) + return portNode->blockNode()->mapToScene(portNode->getCenterPos()); + else + return QPointF{}; +} + +QPointF GraphConnectionNode::calcDestPosition() const +{ + if (auto portNode = destPortNode()) + return portNode->blockNode()->mapToScene(portNode->getCenterPos()); + else + return QPointF{}; +} + +void GraphConnectionNode::updateToolTip() +{ + if (auto con = _connection.lock()) // Make sure that the underlying connection still exists + { + QString toolTip = QString{"<b>From: </b>%1<br><b>To: </b>%2"} + .arg(con->sourcePort()->getFormattedName()) + .arg(con->destPort()->getFormattedName()); + + setToolTip(toolTip); + } +} + +void GraphConnectionNode::connectPortSignals() +{ + // Remove any previous signals + disconnect(nullptr, this, SLOT(connectedBlockMoved(VisualNode*))); + + // Listen for movement events of both involved blocks in order to update our own shape + auto sourceNode = sourcePortNode(); + auto destNode = destPortNode(); + + if (sourceNode) + { + connect(sourceNode->blockNode(), &GraphBlockNode::nodeMoved, this, &GraphConnectionNode::connectedBlockMoved); + connect(sourceNode->blockNode(), &GraphBlockNode::nodeGeometryUpdated, this, &GraphConnectionNode::connectedBlockMoved); + } + + if (destNode) + { + connect(destNode->blockNode(), &GraphBlockNode::nodeMoved, this, &GraphConnectionNode::connectedBlockMoved); + connect(destNode->blockNode(), &GraphBlockNode::nodeGeometryUpdated, this, &GraphConnectionNode::connectedBlockMoved); + } +} + +QColor GraphConnectionNode::getPortColor(const GraphPortNode* portNode) const +{ + // Get the color of the block associated with the given port + if (portNode) + { + if (auto port = portNode->port().lock()) + return _style.getBlockNodeStyle().getBlockCategoryColor(port->block()->getCategory()); + } + + return QColor{}; +} + +void GraphConnectionNode::connectedBlockMoved(VisualNode* node) +{ + Q_UNUSED(node); + + // One of the connected blocks has been moved, so we need to update our geometry + prepareGeometryChange(); + updateGeometry(); +} diff --git a/Grinder/ui/graph/GraphConnectionNode.h b/Grinder/ui/graph/GraphConnectionNode.h new file mode 100644 index 0000000000000000000000000000000000000000..9bc1538401079ba02b21e11f517da61d915e6144 --- /dev/null +++ b/Grinder/ui/graph/GraphConnectionNode.h @@ -0,0 +1,59 @@ +/****************************************************************************** + * File: GraphConnectionNode.h + * Date: 25.1.2018 + *****************************************************************************/ + +#ifndef GRAPHCONNECTIONNODE_H +#define GRAPHCONNECTIONNODE_H + +#include <memory> + +#include "GraphConnectionBase.h" + +namespace grndr +{ + class GraphPortNode; + class Connection; + + class GraphConnectionNode : public GraphConnectionBase + { + Q_OBJECT + + public: + GraphConnectionNode(GraphScene* scene, const std::shared_ptr<Connection>& con, QGraphicsItem* parent = nullptr); + + public: + GraphPortNode* sourcePortNode() { return _sourcePortNode(); } + const GraphPortNode* sourcePortNode() const { return _sourcePortNode(); } + GraphPortNode* destPortNode() { return _destPortNode(); } + const GraphPortNode* destPortNode() const { return _destPortNode(); } + + std::weak_ptr<Connection>& connection() { return _connection; } + const std::weak_ptr<Connection>& connection() const { return _connection; } + + public: + virtual void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) override; + + protected: + virtual QPointF calcSourcePosition() const override; + virtual QPointF calcDestPosition() const override; + + private: + void updateToolTip(); + void connectPortSignals(); + + QColor getPortColor(const GraphPortNode* node) const; + + private: + GraphPortNode* _sourcePortNode() const; + GraphPortNode* _destPortNode() const; + + private slots: + void connectedBlockMoved(VisualNode* node); + + private: + std::weak_ptr<Connection> _connection; + }; +} + +#endif diff --git a/Grinder/ui/graph/GraphLayout.cpp b/Grinder/ui/graph/GraphLayout.cpp new file mode 100644 index 0000000000000000000000000000000000000000..1c19898a95276cf033cea58f7f43afaf80a7dfad --- /dev/null +++ b/Grinder/ui/graph/GraphLayout.cpp @@ -0,0 +1,146 @@ +/****************************************************************************** + * File: GraphLayout.cpp + * Date: 23.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "GraphLayout.h" +#include "GraphBlockNode.h" +#include "GraphView.h" +#include "GraphScene.h" +#include "pipeline/Pipeline.h" +#include "util/StringConv.h" +#include "util/SerializationUtils.h" + +const char* GraphLayout::Serialization_Group_Item = "Item"; + +const char* GraphLayout::Serialization_Value_Block = "Block"; +const char* GraphLayout::Serialization_Value_Position = "Position"; + +void GraphLayout::applyLayout(GraphScene* scene, bool centerLayout) const +{ + qreal maxX = 0.0; + qreal maxY = 0.0; + + if (centerLayout) + { + // Adjust layout to be centered + for (const auto& layoutItem : _layout) + { + if (auto block = layoutItem->_block.lock()) // Make sure that the block still exists + { + if (auto blockNode = scene->findBlockNode(block.get())) + { + auto rect = blockNode->boundingRect(false); + + maxX = std::max(maxX, layoutItem->_position.x() + rect.width()) * 0.5; + maxY = std::max(maxY, layoutItem->_position.y() + rect.height()) * 0.5; + } + } + } + } + + for (const auto& layoutItem : _layout) + { + if (auto block = layoutItem->_block.lock()) // Make sure that the block still exists + { + if (auto blockNode = scene->findBlockNode(block.get())) + blockNode->setPos(QPointF{layoutItem->_position.x() - maxX, layoutItem->_position.y() - maxY}); + } + } +} + +void GraphLayout::fromScene(GraphScene* scene) +{ + if (!scene) + throw std::invalid_argument{_EXCPT("scene may not be null")}; + + _layout.clear(); + + for (auto blockNode : scene->getNodes<GraphBlockNode>()) + _layout.emplace_back(std::make_shared<LayoutItem>(blockNode->_block, blockNode->pos())); +} + +void GraphLayout::fromBlockHierarchy(GraphScene* scene, const BlockHierarchy& blockHierarchy) +{ + if (!scene) + throw std::invalid_argument{_EXCPT("scene may not be null")}; + + _layout.clear(); + + QPointF currentPos{0.0, 0.0}; + auto layoutStyle = scene->view()->sceneStyle().getLayoutStyle(); + + // Each level is layed out on a column + for (const auto& level : blockHierarchy) + { + auto columnSize = addHierarchyLevel(scene, currentPos, level); + + currentPos.setX(currentPos.x() + columnSize.width() + layoutStyle.layoutMargins.width()); + currentPos.setY(0.0); + } +} + +void GraphLayout::serialize(SerializationContext& ctx) const +{ + SerializationUtils::serializeContainer(_layout, Serialization_Group_Item, ctx); +} + +void GraphLayout::deserialize(const Pipeline* pipeline, DeserializationContext& ctx) +{ + SerializationUtils::deserializeContainer<Layout>(Serialization_Group_Item, ctx, [&ctx, pipeline, this](const SettingsContainer& settings) -> std::shared_ptr<LayoutItem> { + int blockIndex = settings[Serialization_Value_Block].toInt(); + QPointF position = StringConv::convertString<QPointF>(settings[Serialization_Value_Position].toString()); + auto block = ctx.getBlock(blockIndex); + + if (block) + { + // Get the shared_ptr belonging to the block + auto it = pipeline->blocks().find(block); + + if (it != pipeline->blocks().cend()) + _layout.emplace_back(std::make_shared<LayoutItem>(*it, position)); + } + + // We don't need to explicitly deserialize the new layout item + return nullptr; + }); +} + +QSizeF GraphLayout::addHierarchyLevel(GraphScene* scene, QPointF startPos, const BlockHierarchy::HierarchyLevel& blocks) +{ + auto layoutStyle = scene->view()->sceneStyle().getLayoutStyle(); + auto currentPos = startPos; + qreal width = 0.0; + + for (const auto& block : blocks) + { + auto blockNode = scene->findBlockNode(block); + + if (blockNode) + { + _layout.emplace_back(std::make_shared<LayoutItem>(blockNode->_block, currentPos)); + + auto rect = blockNode->boundingRect(false); + width = std::max(width, rect.width()); + currentPos.setY(currentPos.y() + rect.height() + layoutStyle.layoutMargins.height()); + } + } + + return QSizeF{width, currentPos.y()}; +} + +void GraphLayout::LayoutItem::serialize(SerializationContext& ctx) const +{ + if (auto block = _block.lock()) // Make sure that the block still exists + { + ctx.settings()[Serialization_Value_Block] = ctx.getBlockIndex(block.get()); + ctx.settings()[Serialization_Value_Position] = StringConv::convertValue(_position); + } +} + +void GraphLayout::LayoutItem::deserialize(DeserializationContext& ctx) +{ + // Nothing to deserialize (all done by the superordinate layout) + Q_UNUSED(ctx); +} diff --git a/Grinder/ui/graph/GraphLayout.h b/Grinder/ui/graph/GraphLayout.h new file mode 100644 index 0000000000000000000000000000000000000000..8a612ca174a7aefea5bf767566767d044686beba --- /dev/null +++ b/Grinder/ui/graph/GraphLayout.h @@ -0,0 +1,80 @@ +/****************************************************************************** + * File: GraphLayout.h + * Date: 23.2.2018 + *****************************************************************************/ + +#ifndef GRAPHLAYOUT_H +#define GRAPHLAYOUT_H + +#include <QPointF> +#include <QSizeF> +#include <memory> + +#include "pipeline/BlockHierarchy.h" +#include "project/serialization/SerializationContext.h" +#include "project/serialization/DeserializationContext.h" + +namespace grndr +{ + class BlockHierarchy; + class GraphScene; + + class GraphLayout + { + public: + static const char* Serialization_Group_Item; + + static const char* Serialization_Value_Block; + static const char* Serialization_Value_Position; + + public: + struct LayoutItem + { + LayoutItem() { } + LayoutItem(std::weak_ptr<Block> block, QPointF pos) : _block{block}, _position{pos} { } + + std::weak_ptr<Block> _block; + QPointF _position; + + void serialize(SerializationContext& ctx) const; + void deserialize(DeserializationContext& ctx); + }; + + using Layout = std::vector<std::shared_ptr<LayoutItem>>; + + public: + GraphLayout() { } + GraphLayout(const GraphLayout& graphLayout) = default; + GraphLayout(GraphLayout&& graphLayout) = default; + GraphLayout(const Layout& layout) { _layout = layout; } + GraphLayout(Layout&& layout) { _layout = std::move(layout); } + + GraphLayout& operator =(const GraphLayout& graphLayout) = default; + GraphLayout& operator =(GraphLayout&& graphLayout) = default; + GraphLayout& operator =(const Layout& layout) { _layout = layout; return *this; } + GraphLayout& operator =(Layout&& layout) { _layout = std::move(layout); return *this; } + + public: + void applyLayout(GraphScene* scene, bool centerLayout = true) const; + + public: + void fromScene(GraphScene* scene); + void fromBlockHierarchy(GraphScene* scene, const BlockHierarchy& blockHierarchy); + + public: + Layout& layout() { return _layout; } + const Layout& layout() const { return _layout; } + + public: + void serialize(SerializationContext& ctx) const; + void deserialize(const Pipeline* pipeline, DeserializationContext& ctx); + + private: + QSizeF addHierarchyLevel(GraphScene* scene, QPointF startPos, const BlockHierarchy::HierarchyLevel& blocks); + + private: + Layout _layout; + }; +} + +#endif diff --git a/Grinder/ui/graph/GraphNode.cpp b/Grinder/ui/graph/GraphNode.cpp new file mode 100644 index 0000000000000000000000000000000000000000..6ea31edac0cfd7bc6cc656c6c1da6488e2788bac --- /dev/null +++ b/Grinder/ui/graph/GraphNode.cpp @@ -0,0 +1,36 @@ +/****************************************************************************** + * File: GraphNode.cpp + * Date: 22.1.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "GraphNode.h" +#include "GraphScene.h" +#include "GraphView.h" +#include "core/GrinderApplication.h" +#include "controller/PipelineController.h" +#include "util/UIUtils.h" +#include "res/Resources.h" + +#include <QMenu> + +GraphNode::GraphNode(GraphScene* scene, QGraphicsItem* parent) : VisualNode(scene, parent), + _scene{scene}, _style{scene->view()->sceneStyle()} +{ + // Create node actions + _deleteAction = createNodeAction("&Delete", FILE_ICON_DELETE, SLOT(deleteNode()), "Remove the selected item", "Del"); +} + +std::vector<QAction*> GraphNode::getNodesActions(QMenu& menu) const +{ + // If multiple items are selected, only show a "delete all" action + auto action = UIUtils::createAction(&menu, "&Delete selected items", FILE_ICON_DELETE_SELECTED, nullptr, "Delete the selected items", "Del"); + action->connect(action, &QAction::triggered, _scene->view(), &GraphView::removeSelectedItems); + + return {action}; +} + +void GraphNode::deleteNode() +{ + grinder()->pipelineController().removeSelectedNodes(); +} diff --git a/Grinder/ui/graph/GraphNode.h b/Grinder/ui/graph/GraphNode.h new file mode 100644 index 0000000000000000000000000000000000000000..fa1646fcdce9c0e19d2cd63a4ba8363b0ce00008 --- /dev/null +++ b/Grinder/ui/graph/GraphNode.h @@ -0,0 +1,38 @@ +/****************************************************************************** + * File: GraphNode.h + * Date: 22.1.2018 + *****************************************************************************/ + +#ifndef GRAPHNODE_H +#define GRAPHNODE_H + +#include "ui/visscene/VisualNode.h" + +namespace grndr +{ + class GraphScene; + class GraphStyle; + + class GraphNode : public VisualNode + { + Q_OBJECT + + public: + GraphNode(GraphScene* scene, QGraphicsItem* parent = nullptr); + + protected: + virtual std::vector<QAction*> getNodeActions(QMenu& menu) const override { Q_UNUSED(menu); return {nullptr, _deleteAction}; } + virtual std::vector<QAction*> getNodesActions(QMenu& menu) const override; + + protected slots: + void deleteNode(); + + protected: + GraphScene* _scene{nullptr}; + const GraphStyle& _style; + + QAction* _deleteAction; + }; +} + +#endif diff --git a/Grinder/ui/graph/GraphNodeFactory.cpp b/Grinder/ui/graph/GraphNodeFactory.cpp new file mode 100644 index 0000000000000000000000000000000000000000..3cc15fc691bc57612304f21adaeb10ea890847bc --- /dev/null +++ b/Grinder/ui/graph/GraphNodeFactory.cpp @@ -0,0 +1,24 @@ +/****************************************************************************** + * File: GraphNodeFactory.cpp + * Date: 22.1.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "GraphNodeFactory.h" +#include "GraphBlockNode.h" +#include "pipeline/Block.h" + +GraphNodeFactory::GraphNodeFactory(GraphScene* scene) : VisualNodeFactory(scene) +{ + registerStandardBlocks(); +} + +GraphBlockNode* GraphNodeFactory::createDefaultNode(const std::shared_ptr<Block>& block) const +{ + return new GraphBlockNode{_scene, block}; +} + +void GraphNodeFactory::registerStandardBlocks() +{ + +} diff --git a/Grinder/ui/graph/GraphNodeFactory.h b/Grinder/ui/graph/GraphNodeFactory.h new file mode 100644 index 0000000000000000000000000000000000000000..b9442f3fd47738f3a2f61a23b0d0857a16577fc0 --- /dev/null +++ b/Grinder/ui/graph/GraphNodeFactory.h @@ -0,0 +1,31 @@ +/****************************************************************************** + * File: GraphNodeFactory.h + * Date: 22.1.2018 + *****************************************************************************/ + +#ifndef GRAPHNODEFACTORY_H +#define GRAPHNODEFACTORY_H + +#include "ui/visscene/VisualNodeFactory.h" +#include "pipeline/BlockType.h" + +namespace grndr +{ + class GraphScene; + class GraphBlockNode; + class Block; + + class GraphNodeFactory : public VisualNodeFactory<GraphBlockNode, Block, BlockType, GraphScene> + { + public: + GraphNodeFactory(GraphScene* scene); + + protected: + virtual GraphBlockNode* createDefaultNode(const std::shared_ptr<Block>& block) const override; + + private: + void registerStandardBlocks(); + }; +} + +#endif diff --git a/Grinder/ui/graph/GraphPortNode.cpp b/Grinder/ui/graph/GraphPortNode.cpp new file mode 100644 index 0000000000000000000000000000000000000000..10f7dacc437b63616df7d0b7a428afb8b864835c --- /dev/null +++ b/Grinder/ui/graph/GraphPortNode.cpp @@ -0,0 +1,217 @@ +/****************************************************************************** + * File: GraphPortNode.cpp + * Date: 24.1.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "GraphPortNode.h" +#include "GraphBlockNode.h" +#include "GraphConnectionBlueprint.h" +#include "GraphScene.h" +#include "core/GrinderApplication.h" +#include "pipeline/Port.h" +#include "controller/PipelineController.h" +#include "controller/EngineController.h" +#include "util/DataUtils.h" +#include "res/Resources.h" + +#include <opencv2/highgui.hpp> + +GraphPortNode::GraphPortNode(GraphBlockNode* blockNode, grndr::GraphScene* scene, const std::shared_ptr<grndr::Port>& port, QGraphicsItem* parent) : GraphNode(scene, parent), + _blockNode{blockNode}, _port{port}, _portStyle{_style.getPortNodeStyle()} +{ + if (!blockNode) + throw std::invalid_argument{_EXCPT("blockNode may not be null")}; + + if (!port) + throw std::invalid_argument{_EXCPT("port may not be null")}; + + setFlag(ItemIsSelectable, false); + setFlag(ItemIsFocusable, false); + + updateGeometry(); + + // Create node actions + _viewImageAction = createNodeAction("&View image", FILE_ICON_VIEWIMAGE, SLOT(viewImage()), "View the image at this port"); + _disconnectAction = createNodeAction("&Disconnect from all", FILE_ICON_DELETE, SLOT(disconnectFromAll()), "Remove all connections to/from this port"); + + // Set the tooltip showing what this port is accepting/offering + updateToolTip(); +} + +QPointF GraphPortNode::getCenterPos(bool shiftCenter) const +{ + auto centerPos = pos() + _geometry.portRect.center(); + + if (shiftCenter) + { + if (auto port = _port.lock()) // Make sure that the underlying port still exists + { + // Slightly shift the center point right/left (makes creating connections easier) + auto shift = _portStyle.innerSize + _style.getConnectionNodeStyle().lineWidth / 2.0; + + if (port->isOut()) + centerPos.setX(centerPos.x() + shift); + else if (port->isIn()) + centerPos.setX(centerPos.x() - shift); + } + } + + return centerPos; +} + +void GraphPortNode::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) +{ + Q_UNUSED(option); + Q_UNUSED(widget); + + painter->setRenderHints(QPainter::Antialiasing|QPainter::TextAntialiasing|QPainter::HighQualityAntialiasing|QPainter::SmoothPixmapTransform); + + // Draw the port connector + QPen pen{_portStyle.borderColor, _portStyle.borderWidth}; + pen.setCapStyle(Qt::FlatCap); + pen.setJoinStyle(Qt::MiterJoin); + + painter->setPen(pen); + painter->fillRect(_geometry.portRect, _portStyle.darkBackgroundColor); + painter->drawRect(_geometry.portRect); + + auto sizeDiff = (_portStyle.outerSize - _portStyle.innerSize) / 2.0; + auto innerRect = _geometry.portRect - QMarginsF{sizeDiff, sizeDiff, sizeDiff, sizeDiff}; + + if (auto port = _port.lock()) // Make sure that the underlying port still exists + { + if (!port->isConnected()) + painter->fillRect(innerRect, _portStyle.lightBackgroundColor); + + painter->drawRect(innerRect); + + // Draw the port name + painter->setPen(QPen{_portStyle.textColor}); + painter->setFont(_portStyle.nameFont); + painter->drawText(_geometry.portNameRect, (port->isIn() ? Qt::AlignLeft : Qt::AlignRight)|Qt::AlignVCenter, port->getName()); + } +} + +void GraphPortNode::contextMenuEvent(QGraphicsSceneContextMenuEvent* event) +{ + // Select the block node exclusively if it hasn't been selected + if (!_blockNode->isSelected()) + { + _scene->clearSelection(); + _blockNode->setSelected(true); + } + + showContextMenu(event->screenPos()); + + event->accept(); +} + +void GraphPortNode::updateGeometry() +{ + if (auto port = _port.lock()) // Make sure that the underlying port still exists + { + _geometry.portRect = QRectF{0.0, 0.0, _portStyle.outerSize, _portStyle.outerSize}; + _geometry.portNameSize = calcPortNameSize(port.get()); + + _nodeRect = QRectF{0.0, 0.0, _geometry.portRect.width() + _geometry.portNameSize.width(), std::max(_geometry.portRect.height(), _geometry.portNameSize.height())}; + _nodeRectSelected = _nodeRect; + + _geometry.portNameRect = _nodeRect; + + if (port->isOut()) + { + // Update geometry for a right-aligned port + _geometry.portRect.moveLeft(_nodeRect.right() - _geometry.portRect.width()); + _geometry.portNameRect.setRight(_geometry.portNameRect.right() - (_geometry.portRect.width() + _portStyle.textMargins.right())); + } + else + _geometry.portNameRect.setLeft(_geometry.portNameRect.left() + _geometry.portRect.width() + _portStyle.textMargins.left()); + } + + GraphNode::updateGeometry(); +} + +QSizeF GraphPortNode::calcPortNameSize(const Port* port) const +{ + auto marginsX = _portStyle.textMargins.left() + _portStyle.textMargins.right(); + auto marginsY = _portStyle.textMargins.top() + _portStyle.textMargins.bottom(); + + QFontMetricsF fontNameMetrics{_portStyle.nameFont}; + auto nameWidth = fontNameMetrics.width(port->getName()) + marginsX; + auto nameHeight = fontNameMetrics.height() + marginsY; + + return QSizeF{nameWidth, nameHeight}; +} + +void GraphPortNode::viewImage() +{ + if (auto port = _port.lock()) // Make sure that the underlying port still exists + { + if (auto activeLabel = grinder()->projectController().activeLabel()) + { + auto portImage = grinder()->engineController().executeLabelEx(activeLabel, port.get(), Engine::ExecutionMode::Execute); + + if (!portImage.empty()) + cv::imshow(port->getFormattedName().toStdString(), portImage); + } + } +} + +void GraphPortNode::disconnectFromAll() +{ + if (auto port = _port.lock()) // Make sure that the underlying port still exists + grinder()->pipelineController().disconnectPortFromAll(port.get()); +} + +void GraphPortNode::updateToolTip() +{ + if (auto port = _port.lock()) // Make sure that the underlying port still exists + { + QString toolTip = QString{"<b>%1</b>"}.arg(port->getFormattedName()); + QStringList dataDescInfos; + + for (const auto& dataDesc : port->dataDescriptors()) + { + dataDescInfos << QString{"<br><u>%1</u><br><em> Structure type: </em>%2<br><em> Field type: </em>%3<br><em> Value type: </em>%4"} + .arg(dataDesc.getName()) + .arg(DataUtils::getDataDescriptorTypeName(dataDesc.getStructureType())) + .arg(DataUtils::getDataDescriptorTypeName(dataDesc.getFieldType())) + .arg(DataUtils::getDataDescriptorTypeName(dataDesc.getValueType())); + } + + setToolTip(toolTip + dataDescInfos.join("<br>")); + } +} + +std::vector<QAction*> GraphPortNode::getNodeActions(QMenu& menu) const +{ + Q_UNUSED(menu); + + _viewImageAction->setEnabled(false); + _disconnectAction->setEnabled(false); + + if (auto port = _port.lock()) // Make sure that the underlying port still exists + { + // Check if this port has at least one matrix type + bool hasMatrixType = false; + + for (const auto& dataDesc : port->dataDescriptors()) + { + if (dataDesc.getStructureType() == DataDescriptor::StructureType::Matrix) + { + hasMatrixType = true; + break; + } + } + + if (hasMatrixType && (port->isOut() || (port->isIn() && port->isConnected()))) // In-ports must be connected to a source + _viewImageAction->setEnabled(true); + + // Check if this port is connected to something + if (!port->connections().empty() || !port->getConnections(Port::Direction::In).empty()) + _disconnectAction->setEnabled(true); + } + + return {_viewImageAction, nullptr, _disconnectAction}; +} diff --git a/Grinder/ui/graph/GraphPortNode.h b/Grinder/ui/graph/GraphPortNode.h new file mode 100644 index 0000000000000000000000000000000000000000..1aaddf6b4491ae3f4c84a06c86f86ee010d948c3 --- /dev/null +++ b/Grinder/ui/graph/GraphPortNode.h @@ -0,0 +1,75 @@ +/****************************************************************************** + * File: GraphPortNode.h + * Date: 24.1.2018 + *****************************************************************************/ + +#ifndef GRAPHPORTNODE_H +#define GRAPHPORTNODE_H + +#include <memory> + +#include "GraphNode.h" +#include "GraphStyle.h" + +namespace grndr +{ + class GraphBlockNode; + class GraphConnectionBlueprint; + class Port; + + class GraphPortNode : public GraphNode + { + Q_OBJECT + + public: + GraphPortNode(GraphBlockNode* blockNode, GraphScene* scene, const std::shared_ptr<Port>& port, QGraphicsItem* parent = nullptr); + + public: + GraphBlockNode* blockNode() { return _blockNode; } + const GraphBlockNode* blockNode() const { return _blockNode; } + std::weak_ptr<Port>& port() { return _port; } + const std::weak_ptr<Port>& port() const { return _port; } + + QRectF getPortRect() const { return _geometry.portRect; } + QPointF getCenterPos(bool shiftCenter = true) const; + + public: + virtual void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) override; + + protected: + virtual void contextMenuEvent(QGraphicsSceneContextMenuEvent* event) override; + + protected: + virtual void updateGeometry() override; + + virtual std::vector<QAction*> getNodeActions(QMenu& menu) const override; + + private: + QSizeF calcPortNameSize(const Port* port) const; + + struct + { + QRectF portRect; + QSizeF portNameSize; + QRectF portNameRect; + } _geometry; + + private slots: + void viewImage(); + void disconnectFromAll(); + + private: + void updateToolTip(); + + private: + GraphBlockNode* _blockNode{nullptr}; + std::weak_ptr<Port> _port; + + GraphStyle::PortNodeStyle _portStyle; + + QAction* _viewImageAction; + QAction* _disconnectAction; + }; +} + +#endif diff --git a/Grinder/ui/graph/GraphScene.cpp b/Grinder/ui/graph/GraphScene.cpp new file mode 100644 index 0000000000000000000000000000000000000000..a9bb12d112f6e59540d7304b8a96226874e9930a --- /dev/null +++ b/Grinder/ui/graph/GraphScene.cpp @@ -0,0 +1,124 @@ +/****************************************************************************** + * File: GraphScene.cpp + * Date: 18.1.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "GraphScene.h" +#include "GraphView.h" +#include "GraphStyle.h" +#include "GraphBlockNode.h" +#include "GraphPortNode.h" +#include "GraphConnectionNode.h" +#include "pipeline/Pipeline.h" + +GraphScene::GraphScene(Pipeline* pipeline, GraphView* view, QSizeF initialSize) : VisualScene{view, QRectF{-(initialSize.width() / 2), -(initialSize.height() / 2), initialSize.width(), initialSize.height()}}, + _pipeline{pipeline}, _nodeFactory{this} +{ + if (!pipeline) + throw std::invalid_argument{_EXCPT("pipeline may not be null")}; +} + +void GraphScene::buildScene() +{ + clear(); + + // Add all elements of the pipeline of the label to the scene + if (_pipeline) + { + // Add all blocks to the scene + for (const auto& block : _pipeline->blocks()) + createBlockNode(block); + + // Add all connections to the scene + for (const auto& block : _pipeline->blocks()) + { + for (const auto& port : block->ports().selectByDirection(Port::Direction::Out)) + { + for (const auto& con : port->connections()) + createConnectionNode(con); + } + } + } +} + +void GraphScene::createBlockNode(const std::shared_ptr<Block>& block) +{ + if (!block) + throw std::invalid_argument{_EXCPT("block may not be null")}; + + if (!findBlockNode(block.get())) + addItem(_nodeFactory.createNode(block)); +} + +void GraphScene::removeBlockNode(GraphBlockNode* blockNode) +{ + if (blockNode) + delete blockNode; +} + +void GraphScene::createConnectionNode(const std::shared_ptr<Connection>& connection) +{ + if (!connection) + throw std::invalid_argument{_EXCPT("connection may not be null")}; + + if (!findConnectionNode(connection.get())) + addItem(new GraphConnectionNode{this, connection}); +} + +void GraphScene::removeConnectionNode(GraphConnectionNode* connectionNode) +{ + if (connectionNode) + delete connectionNode; +} + +GraphBlockNode* GraphScene::_findBlockNode(const Block* block) const +{ + for (const auto& node : items()) + { + auto blockNode = dynamic_cast<GraphBlockNode*>(node); + + if (blockNode && blockNode->block().lock().get() == block) + return blockNode; + } + + return nullptr; +} + +GraphPortNode* GraphScene::_findPortNode(const Port* port) const +{ + auto blockNode = findBlockNode(port->block()); + + if (blockNode) + { + auto searchPorts = [port](const std::vector<GraphPortNode*>& portNodes) -> GraphPortNode* { + auto it = std::find_if(portNodes.cbegin(), portNodes.cend(), [port](auto portNode) { return portNode->port().lock().get() == port; }); + + if (it != portNodes.cend()) + return *it; + + return nullptr; + }; + + if (auto inPort = searchPorts(blockNode->inPortNodes())) + return inPort; + + if (auto outPort = searchPorts(blockNode->outPortNodes())) + return outPort; + } + + return nullptr; +} + +GraphConnectionNode* GraphScene::_findConnectionNode(const Connection* con) const +{ + for (const auto& node : items()) + { + auto conNode = dynamic_cast<GraphConnectionNode*>(node); + + if (conNode && conNode->connection().lock().get() == con) + return conNode; + } + + return nullptr; +} diff --git a/Grinder/ui/graph/GraphScene.h b/Grinder/ui/graph/GraphScene.h new file mode 100644 index 0000000000000000000000000000000000000000..69a120e149aaa4d398790a784d2d9787bb525877 --- /dev/null +++ b/Grinder/ui/graph/GraphScene.h @@ -0,0 +1,65 @@ +/****************************************************************************** + * File: GraphScene.h + * Date: 18.1.2018 + *****************************************************************************/ + +#ifndef GRAPHSCENE_H +#define GRAPHSCENE_H + +#include "ui/visscene/VisualScene.h" +#include "GraphView.h" +#include "GraphNodeFactory.h" + +namespace grndr +{ + class Pipeline; + class Block; + class Port; + class Connection; + class GraphView; + class GraphBlockNode; + class GraphPortNode; + class GraphConnectionNode; + + class GraphScene : public VisualScene<GraphView> + { + Q_OBJECT + + public: + GraphScene(Pipeline* pipeline, GraphView* view, QSizeF initialSize); + + public: + void buildScene(); + + void createBlockNode(const std::shared_ptr<Block>& block); + void removeBlockNode(const std::shared_ptr<Block>& block) { removeBlockNode(findBlockNode(block.get())); } + void removeBlockNode(GraphBlockNode* blockNode); + + void createConnectionNode(const std::shared_ptr<Connection>& connection); + void removeConnectionNode(const std::shared_ptr<Connection>& connection) { removeConnectionNode(findConnectionNode(connection.get())); } + void removeConnectionNode(GraphConnectionNode* connectionNode); + + public: + GraphBlockNode* findBlockNode(const Block* block) { return _findBlockNode(block); } + const GraphBlockNode* findBlockNode(const Block* block) const { return _findBlockNode(block); } + GraphPortNode* findPortNode(const Port* port) { return _findPortNode(port); } + const GraphPortNode* findPortNode(const Port* port) const { return _findPortNode(port); } + GraphConnectionNode* findConnectionNode(const Connection* con) { return _findConnectionNode(con); } + const GraphConnectionNode* findConnectionNode(const Connection* con) const { return _findConnectionNode(con); } + + public: + const GraphNodeFactory& nodeFactory() const { return _nodeFactory; } + + private: + GraphBlockNode* _findBlockNode(const Block* block) const; + GraphPortNode* _findPortNode(const Port* port) const; + GraphConnectionNode* _findConnectionNode(const Connection* con) const; + + private: + Pipeline* _pipeline{nullptr}; + + GraphNodeFactory _nodeFactory; + }; +} + +#endif diff --git a/Grinder/ui/graph/GraphStyle.cpp b/Grinder/ui/graph/GraphStyle.cpp new file mode 100644 index 0000000000000000000000000000000000000000..9677cceccdd4f3380c9653ca8503fbf244e2bc7c --- /dev/null +++ b/Grinder/ui/graph/GraphStyle.cpp @@ -0,0 +1,41 @@ +/****************************************************************************** + * File: GraphStyle.cpp + * Date: 20.1.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "GraphStyle.h" + +QColor GraphStyle::BlockNodeStyle::getBlockCategoryColor(BlockCategory category) const +{ + if (category == BlockCategory::Input) + return QColor{220, 0, 20}; + else if (category == BlockCategory::Output) + return QColor{100, 0, 140}; + else if (category == BlockCategory::Conversion) + return QColor{0, 80, 140}; + else if (category == BlockCategory::Thresholding) + return QColor{100, 220, 0}; + else + return QColor{255, 255, 255}; +} + +GraphStyle::NodeStyle::NodeStyle() +{ + font.setStyleStrategy(QFont::StyleStrategy(QFont::PreferAntialias|QFont::PreferQuality)); +} + +GraphStyle::BlockNodeStyle::BlockNodeStyle() +{ + nameFont.setPointSize(10); + nameFont.setBold(true); + nameFont.setStyleStrategy(QFont::StyleStrategy(QFont::PreferAntialias|QFont::PreferQuality)); + + typeFont.setPointSize(8); + typeFont.setStyleStrategy(QFont::StyleStrategy(QFont::PreferAntialias|QFont::PreferQuality)); +} + +GraphStyle::GraphStyle() +{ + _viewStyle.defaultSceneSize = {4000.0f, 2000.0f}; +} diff --git a/Grinder/ui/graph/GraphStyle.h b/Grinder/ui/graph/GraphStyle.h new file mode 100644 index 0000000000000000000000000000000000000000..9028715a5b2217e565708f259502f3e4c5d6ba12 --- /dev/null +++ b/Grinder/ui/graph/GraphStyle.h @@ -0,0 +1,116 @@ +/****************************************************************************** + * File: GraphStyle.h + * Date: 20.1.2018 + *****************************************************************************/ + +#ifndef GRAPHSTYLE_H +#define GRAPHSTYLE_H + +#include <QColor> +#include <QFont> +#include <QSizeF> +#include <QMarginsF> +#include <QPalette> + +#include "ui/visscene/VisualSceneStyle.h" +#include "pipeline/BlockCategory.h" + +namespace grndr +{ + class GraphScene; + + class GraphStyle : public VisualSceneStyle + { + public: + struct LayoutStyle + { + QSizeF layoutMargins{100.0, 50.0}; + }; + + struct NodeStyle + { + QFont font{}; + + QColor darkBackgroundColor{100, 100, 100}; + QColor lightBackgroundColor{200, 200, 200}; + QColor borderColor{0, 0, 0}; + float borderRadius{3.0f}; + + QColor selectionColor{QPalette{}.highlight().color().lighter()}; + QColor selectionColorInactive{QPalette{}.color(QPalette::Inactive, QPalette::Highlight).darker()}; + float selectionMargin{6.0f}; + float selectionOpacity{0.6f}; + + NodeStyle(); + }; + + struct BlockNodeStyle : public NodeStyle + { + QSizeF minimumSize{150.0, 60.0}; + + QFont nameFont{font}; + QFont typeFont{font}; + QColor textColor{0, 0, 0}; + QMarginsF textMargins{20.0, 3.0, 20.0, 3.0}; + + float borderWidth{3.0}; + + QMarginsF portMargins{6.0, 6.0, 6.0, 6.0}; + + QColor getBlockCategoryColor(BlockCategory category) const; + + BlockNodeStyle(); + }; + + struct PortNodeStyle : public NodeStyle + { + float outerSize{15.0}; + float innerSize {7.5}; + + QFont nameFont{font}; + QColor textColor{0, 0, 0}; + QMarginsF textMargins{5.0, 0.0, 5.0, 0.0}; + + float borderWidth{1.0}; + }; + + struct ConnectionNodeStyle : public NodeStyle + { + float lineWidth{5.0f}; + + float blueprintOpacity{0.7f}; + QColor blueprintPendingColor{50, 150, 255}; + QColor blueprintValidColor{50, 255, 150}; + QColor blueprintInvalidColor{255, 50, 50}; + }; + + struct ConnectionMessageStyle : public ConnectionNodeStyle + { + float messageOpacity{0.8f}; + QFont messageFont{font}; + QColor messageColor{255, 255, 255}; + QMarginsF messageMargins{7.5, 7.5, 7.5, 7.5}; + }; + + public: + GraphStyle(); + + public: + const LayoutStyle& getLayoutStyle() const { return _layoutStyle; } + + const BlockNodeStyle& getBlockNodeStyle() const { return _blockNodeStyle; } + const PortNodeStyle& getPortNodeStyle() const { return _portNodeStyle; } + const ConnectionNodeStyle& getConnectionNodeStyle() const { return _connectionNodeStyle; } + const ConnectionMessageStyle& getConnectionMessageStyle() const { return _connectionMessageStyle; } + + private: + LayoutStyle _layoutStyle; + + BlockNodeStyle _blockNodeStyle; + PortNodeStyle _portNodeStyle; + ConnectionNodeStyle _connectionNodeStyle; + ConnectionMessageStyle _connectionMessageStyle; + }; +} + +#endif diff --git a/Grinder/ui/graph/GraphView.cpp b/Grinder/ui/graph/GraphView.cpp new file mode 100644 index 0000000000000000000000000000000000000000..958426573228efca71d2fff7587de3c345819834 --- /dev/null +++ b/Grinder/ui/graph/GraphView.cpp @@ -0,0 +1,188 @@ +/****************************************************************************** + * File: GraphView.cpp + * Date: 18.1.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "GraphView.h" +#include "GraphScene.h" +#include "GraphWidget.h" +#include "GraphBlockList.h" +#include "GraphBlockNode.h" +#include "GraphLayout.h" +#include "core/GrinderApplication.h" +#include "util/UIUtils.h" +#include "res/Resources.h" + +GraphView::GraphView(QWidget* parent) : VisualSceneView(parent) +{ + setAcceptDrops(true); + + // Remove widget border + setStyleSheet("QGraphicsView { border: none; }"); + + // Create view actions + _layoutGraphAction = UIUtils::createAction(this, "&Layout graph", FILE_ICON_LAYOUTGRAPH, SLOT(layoutGraph()), "Automatically arrange the graph", "Ctrl+R", Qt::WindowShortcut); + + // We need to save the current graph layout before saving the project + connect(&grinder()->projectController(), &ProjectController::projectSaving, this, &GraphView::projectSaving); + + // Listen for label switches in order to store/restore graph layouts + connect(&grinder()->projectController(), &ProjectController::labelSwitching, this, &GraphView::labelSwitching); + connect(&grinder()->projectController(), &ProjectController::labelSwitched, this, &GraphView::labelSwitched); + + updateActions(); +} + +void GraphView::setGraphScene(GraphScene* scene) +{ + _graphScene = scene; + VisualSceneView::setScene(scene); +} + +void GraphView::layoutGraph() +{ + auto hierarchy = grinder()->pipelineController().createBlockHierarchy(); + + if (!hierarchy.empty()) + { + GraphLayout layout; + + layout.fromBlockHierarchy(_graphScene, hierarchy); + layout.applyLayout(_graphScene); + centerOn(0.0, 0.0); + } +} + +void GraphView::removeSelectedItems() const +{ + if (_graphScene) + grinder()->pipelineController().removeSelectedNodes(); +} + +void GraphView::updateCurrentGraphLayout() const +{ + if (auto currentLabel = grinder()->projectController().activeLabel()) + currentLabel->graphLayout().fromScene(_graphScene); +} + +void GraphView::moveSelectedItems(QPoint delta) +{ + if (_graphScene) + { + // Only move block nodes + for (auto block : _graphScene->getNodes<GraphBlockNode>(true)) + block->setPos(block->pos() + delta); + } +} + +void GraphView::dragEnterEvent(QDragEnterEvent* event) +{ + if (event->mimeData()->hasFormat(GRAPHBLOCKLIST_DRAG_MIMETYPE)) + event->acceptProposedAction(); + else + event->ignore(); +} + +void GraphView::dragMoveEvent(QDragMoveEvent* event) +{ + if (event->mimeData()->hasFormat(GRAPHBLOCKLIST_DRAG_MIMETYPE)) + event->acceptProposedAction(); + else + event->ignore(); +} + +void GraphView::dropEvent(QDropEvent* event) +{ + if (event->mimeData()->hasFormat(GRAPHBLOCKLIST_DRAG_MIMETYPE)) + { + BlockType type = event->mimeData()->data(GRAPHBLOCKLIST_DRAG_MIMETYPE).toStdString().data(); + + // Create a new block of the dropped type + if (auto pipeline = grinder()->pipelineController().activePipeline()) + { + QString newBlockName = StringUtils::generateUniqueItemName(pipeline->blocks(), type, &Block::getName); + auto block = grinder()->pipelineController().createBlock(type, newBlockName); + + if (block && _graphScene) + { + // Find the just-created block node and move it to the drop-position + auto blockNode = _graphScene->findBlockNode(block.get()); + + if (blockNode) + { + auto center = mapToScene(viewport()->rect().center()); + + blockNode->setPos(mapToScene(event->pos())); + + _graphScene->clearSelection(); + blockNode->setSelected(true); + blockNode->setFocus(); + + _graphScene->update(); + + centerOn(center); + } + } + + event->acceptProposedAction(); + } + } + else + event->ignore(); +} + +std::vector<QAction*> GraphView::getActions(AddActionsMode mode) const +{ + std::vector<QAction*> actions; + + actions.push_back(_layoutGraphAction); + actions.push_back(nullptr); + + if (mode != AddActionsMode::Toolbar) + { + actions.push_back(_selectAllAction); + actions.push_back(_deleteSelectedAction); + actions.push_back(nullptr); + } + + actions.push_back(_zoomInAction); + actions.push_back(_zoomOutAction); + actions.push_back(_zoomFullAction); + + return actions; +} + +void GraphView::updateActions() +{ + VisualSceneView::updateActions(); + + _layoutGraphAction->setEnabled(_graphScene && !scene()->items().isEmpty()); +} + +void GraphView::projectSaving(QString fileName) +{ + Q_UNUSED(fileName); + + // Before saving the project, save the current graph layout + updateCurrentGraphLayout(); +} + +void GraphView::labelSwitching(Label* label) +{ + if (label) + { + // Save the current layout + label->graphLayout().fromScene(_graphScene); + } +} + +void GraphView::labelSwitched(Label* label) +{ + if (label) + { + // Apply the saved layout + label->graphLayout().applyLayout(_graphScene, false); + centerOn(0.0, 0.0); + } +} diff --git a/Grinder/ui/graph/GraphView.h b/Grinder/ui/graph/GraphView.h new file mode 100644 index 0000000000000000000000000000000000000000..c46b8d62243ebb34bf4cb391be6fec36af39852c --- /dev/null +++ b/Grinder/ui/graph/GraphView.h @@ -0,0 +1,68 @@ +/****************************************************************************** + * File: GraphView.h + * Date: 18.1.2018 + *****************************************************************************/ + +#ifndef GRAPHVIEW_H +#define GRAPHVIEW_H + +#include <QToolBar> + +#include "ui/visscene/VisualSceneView.h" +#include "GraphStyle.h" + +namespace grndr +{ + class Label; + + class GraphView : public VisualSceneView + { + Q_OBJECT + + public: + GraphView(QWidget* parent = nullptr); + + public: + GraphScene* graphScene() { return _graphScene; } + const GraphScene* graphScene() const { return _graphScene; } + void setGraphScene(GraphScene* scene); + + public slots: + void layoutGraph(); + void updateCurrentGraphLayout() const; + + public slots: + virtual void moveSelectedItems(QPoint delta) override; + virtual void removeSelectedItems() const override; + + public: + virtual const GraphStyle& sceneStyle() const override { return _graphStyle; } + + protected: + virtual void dragEnterEvent(QDragEnterEvent*event) override; + virtual void dragMoveEvent(QDragMoveEvent* event) override; + virtual void dropEvent(QDropEvent* event) override; + + protected: + virtual std::vector<QAction*> getActions(AddActionsMode mode) const override; + + protected slots: + virtual void updateActions() override; + + private slots: + void projectSaving(QString fileName); + + void labelSwitching(Label* label); + void labelSwitched(Label* label); + + private: + GraphScene* _graphScene{nullptr}; + + GraphStyle _graphStyle; + + private: + QAction* _layoutGraphAction{nullptr}; + }; +} + +#endif diff --git a/Grinder/ui/graph/GraphWidget.cpp b/Grinder/ui/graph/GraphWidget.cpp new file mode 100644 index 0000000000000000000000000000000000000000..3828479c01ccc2f5bcdbc61bc50efe7c2b5d79ae --- /dev/null +++ b/Grinder/ui/graph/GraphWidget.cpp @@ -0,0 +1,25 @@ +/****************************************************************************** + * File: GraphWidget.cpp + * Date: 02.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "GraphWidget.h" +#include "GraphView.h" +#include "GraphBlockList.h" +#include "ui/StyleSheet.h" + +GraphWidget::GraphWidget(QWidget* parent) : QFrame(parent), + _layout{new QGridLayout{this}} +{ + _layout->setContentsMargins(0, 0, 0, 0); + _layout->setHorizontalSpacing(0); + _layout->setVerticalSpacing(0); + + // Create the graph controls + _graphView = new GraphView{}; + _layout->addWidget(_graphView, 0, 1); + + _graphBlockList = new GraphBlockList{}; + _layout->addWidget(_graphBlockList, 0, 0); +} diff --git a/Grinder/ui/graph/GraphWidget.h b/Grinder/ui/graph/GraphWidget.h new file mode 100644 index 0000000000000000000000000000000000000000..a1a9c71617bf60c5fbadb11f9cfd2340c22f0860 --- /dev/null +++ b/Grinder/ui/graph/GraphWidget.h @@ -0,0 +1,38 @@ +/****************************************************************************** + * File: GraphWidget.h + * Date: 02.2.2018 + *****************************************************************************/ + +#ifndef GRAPHCONTAINERWIDGET_H +#define GRAPHCONTAINERWIDGET_H + +#include <QFrame> +#include <QGridLayout> + +namespace grndr +{ + class GraphView; + class GraphBlockList; + + class GraphWidget : public QFrame + { + Q_OBJECT + + public: + GraphWidget(QWidget* parent = nullptr); + + public: + GraphView* graphView() { return _graphView; } + const GraphView* graphView() const { return _graphView; } + GraphBlockList* graphBlockList() { return _graphBlockList; } + const GraphBlockList* graphBlockList() const { return _graphBlockList; } + + private: + QGridLayout* _layout{nullptr}; + + GraphView* _graphView{nullptr}; + GraphBlockList* _graphBlockList{nullptr}; + }; +} + +#endif diff --git a/Grinder/ui/image/ColorPresetsWidget.cpp b/Grinder/ui/image/ColorPresetsWidget.cpp new file mode 100644 index 0000000000000000000000000000000000000000..495bbcb4e77d7fea678becd650a14a347b88314e --- /dev/null +++ b/Grinder/ui/image/ColorPresetsWidget.cpp @@ -0,0 +1,76 @@ +/****************************************************************************** + * File: ColorPresetsWidget.cpp + * Date: 03.4.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ColorPresetsWidget.h" +#include "util/UIUtils.h" + +#define PRESET_COUNT 10 + +ColorPresetsWidget::ColorPresetsWidget(QWidget* parent) : QWidget(parent), + _layout{new QGridLayout{this}} +{ + _layout->setContentsMargins(0, 0, 0, 0); + _layout->setHorizontalSpacing(2); + _layout->setVerticalSpacing(2); + + // Create all color widgets + createPresetWidgets(); +} + +void ColorPresetsWidget::assignUiComponents(QWidget* actionOwner) +{ + // Create all actions + for (unsigned int i = 0; i < _colorPresets.size(); ++i) + { + auto& presetEntry = _colorPresets[i]; + presetEntry.presetAction = createAction(i, actionOwner); + } +} + +void ColorPresetsWidget::createPresetWidgets() +{ + int row = 0; + int col = 0; + + for (int i = 0; i < PRESET_COUNT; ++i) + { + auto colorWidget = new ColorWidget{ColorWidget::Flag::SmallBox|ColorWidget::Flag::SelectColorOnDoubleClick}; + colorWidget->setExtraToolTip(QString{"<b>Preset %1</b> (%2)"}.arg(i + 1).arg((i + 1) % 10)); + _colorPresets.push_back({colorWidget, nullptr}); + + // React to color selections + connect(colorWidget, &ColorWidget::colorClicked, this, &ColorPresetsWidget::presetSelected); + + _layout->addWidget(colorWidget, row, col++); + + if (col >= (PRESET_COUNT / 2)) + { + row++; + col = 0; + } + } +} + +QAction* ColorPresetsWidget::createAction(unsigned int index, QWidget* owner) +{ + auto action = UIUtils::createAction(owner, QString{"Preset %1"}.arg(index + 1), "", SLOT(presetActionTriggered()), "", QString{"%1"}.arg((index + 1) % 10), Qt::WidgetWithChildrenShortcut, this); + action->setData(index); + return action; +} + +void ColorPresetsWidget::presetSelected(QColor color) +{ + emit colorSelected(color); +} + +void ColorPresetsWidget::presetActionTriggered() +{ + if (auto action = dynamic_cast<QAction*>(sender())) + { + unsigned int index = action->data().toUInt(); + emit colorSelected(getColor(index)); + } +} diff --git a/Grinder/ui/image/ColorPresetsWidget.h b/Grinder/ui/image/ColorPresetsWidget.h new file mode 100644 index 0000000000000000000000000000000000000000..f58ca2b0c613e64285b70a29780e71c713a0760e --- /dev/null +++ b/Grinder/ui/image/ColorPresetsWidget.h @@ -0,0 +1,55 @@ +/****************************************************************************** + * File: ColorPresetsWidget.h + * Date: 03.4.2018 + *****************************************************************************/ + +#ifndef COLORPRESETSWIDGET_H +#define COLORPRESETSWIDGET_H + +#include <QGridLayout> + +#include "ui/widget/ColorWidget.h" + +namespace grndr +{ + class ColorPresetsWidget : public QWidget + { + Q_OBJECT + + public: + ColorPresetsWidget(QWidget* parent = nullptr); + + public: + void assignUiComponents(QWidget* actionOwner); + + public: + QColor getColor(unsigned int index) const { return _colorPresets[index].presetWidget->getColor(); } + void setColor(unsigned int index, QColor color) { _colorPresets[index].presetWidget->setColor(color); } + + auto getPresetsCount() const { return _colorPresets.size(); } + + signals: + void colorSelected(QColor color); + + private: + void createPresetWidgets(); + QAction* createAction(unsigned int index, QWidget* owner); + + private slots: + void presetSelected(QColor color); + void presetActionTriggered(); + + private: + QGridLayout* _layout{nullptr}; + + struct PresetEntry + { + ColorWidget* presetWidget; + QAction* presetAction{nullptr}; + }; + + std::vector<PresetEntry> _colorPresets; + }; +} + +#endif diff --git a/Grinder/ui/image/DraftItemNode.cpp b/Grinder/ui/image/DraftItemNode.cpp new file mode 100644 index 0000000000000000000000000000000000000000..9ef754fb7b4693b49b2bbf21ff1bb274cca8e403 --- /dev/null +++ b/Grinder/ui/image/DraftItemNode.cpp @@ -0,0 +1,166 @@ +/****************************************************************************** + * File: DraftItemNode.cpp + * Date: 21.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "DraftItemNode.h" +#include "ImageEditor.h" +#include "image/DraftItem.h" +#include "util/UIUtils.h" +#include "util/MathUtils.h" +#include "res/Resources.h" + +DraftItemNode::DraftItemNode(ImageEditorScene* scene, const std::shared_ptr<DraftItem>& item, QGraphicsItem* parent) : ImageEditorNode(scene, parent), + _draftItem{item}, _draftItemStyle{_style.getDraftItemNodeStyle()} +{ + if (!item) + throw std::invalid_argument{_EXCPT("item may not be null")}; + + setFlag(ItemIsMovable); + + if (auto draftItem = _draftItem.lock()) // Make sure that the underlying draft item still exists + { + // Get a renderer for the draft item + _renderer = draftItem->createRenderer(_draftItemStyle); + + // Whenever a property of the draft item changes, update the node to reflect the new values + for (auto& property : draftItem->properties()) + connect(property.get(), &PropertyBase::valueChanged, this, &DraftItemNode::updateNode); + } + + updateZOrder(); + updateVisibility(); + + // Create node actions + _deleteAction = createNodeAction("&Delete", FILE_ICON_DELETE, SLOT(deleteNode()), "Remove the selected item", "Del"); +} + +void DraftItemNode::initDraftItemNode() +{ + _inPlaceEditor = createInPlaceEditor(); + + if (_inPlaceEditor) + { + _inPlaceEditor->setParentItem(this); + _inPlaceEditor->setVisible(false); + } +} + +void DraftItemNode::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) +{ + Q_UNUSED(option); + Q_UNUSED(widget); + + painter->setRenderHints(QPainter::TextAntialiasing|QPainter::HighQualityAntialiasing|QPainter::SmoothPixmapTransform); + painter->setRenderHints(QPainter::Antialiasing, false); + + // Use the renderer to draw the item + if (_renderer) + { + DraftItemRendererBase::RenderFlags flags = DraftItemRendererBase::RenderFlag::NoFlag; + + if (isSelected()) + flags |= DraftItemRendererBase::RenderFlag::Selected; + + if (!_scene->hasFocus()) + flags |= DraftItemRendererBase::RenderFlag::Inactive; + + if (_imageEditor->environment().showDirectionArrows()) + flags |= DraftItemRendererBase::RenderFlag::ShowDirections; + + if (_imageEditor->environment().showTags()) + flags |= DraftItemRendererBase::RenderFlag::ShowTags; + + _renderer->render(painter, DraftItemRendererBase::RenderModeFlag::RenderToScene, flags); + } +} + +QPainterPath DraftItemNode::shape() const +{ + // Use the renderer to get the item's shape + if (_renderer) + { + QPainterPath path = _renderer->shape(); + + if (!path.isEmpty()) + return path; + } + + return ImageEditorNode::shape(); +} + +void DraftItemNode::updateNode() +{ + if (auto draftItem = _draftItem.lock()) // Make sure that the underlying draft item still exists + { + // Reflect the draft item's position + QPoint pos = *draftItem->position(); + setPos(QPointF{static_cast<qreal>(pos.x()), static_cast<qreal>(pos.y())}); + } + + prepareGeometryChange(); + updateGeometry(); + + // Also update the in-place editor + if (_inPlaceEditor) + _inPlaceEditor->updateEditor(); + + update(); +} + +void DraftItemNode::updateZOrder() +{ + if (auto draftItem = _draftItem.lock()) // Make sure that the underlying draft item still exists + setZValue(draftItem->getZOrder()); // Use the z-order of the draft item +} + +void DraftItemNode::updateVisibility() +{ + if (auto draftItem = _draftItem.lock()) // Make sure that the underlying draft item still exists + setVisible(draftItem->layer()->isVisible()); // Use the visibility flag of the layer +} + +QVariant DraftItemNode::itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant& value) +{ + if (change == QGraphicsItem::ItemPositionHasChanged) + { + if (auto draftItem = _draftItem.lock()) // Make sure that the underlying draft item still exists + { + // Update the draft item's position accordingly + auto newPos = value.toPointF(); + QPoint position = MathUtils::round(newPos); + draftItem->position()->setValue(position); + } + } + else if (change == QGraphicsItem::ItemSelectedHasChanged) + { + // Show the in-place editor if the item is currently selected + if (_inPlaceEditor) + _inPlaceEditor->setVisible(value.toBool()); + } + + return ImageEditorNode::itemChange(change, value); +} + +void DraftItemNode::updateGeometry() +{ + _nodeRect = getBoundingRect(); + _nodeRectSelected = _nodeRect + QMarginsF{_draftItemStyle.selectionMargin, _draftItemStyle.selectionMargin, _draftItemStyle.selectionMargin, _draftItemStyle.selectionMargin}; + + ImageEditorNode::updateGeometry(); +} + +std::vector<QAction*> DraftItemNode::getNodesActions(QMenu& menu) const +{ + // If multiple items are selected, only show a "delete all" action + auto action = UIUtils::createAction(&menu, "&Delete selected items", FILE_ICON_DELETE_SELECTED, nullptr, "Delete the selected items", "Del"); + action->connect(action, &QAction::triggered, _scene->view(), &ImageEditorView::removeSelectedItems); + + return {action}; +} + +void DraftItemNode::deleteNode() +{ + _imageEditor->controller().removeSelectedNodes(); +} diff --git a/Grinder/ui/image/DraftItemNode.h b/Grinder/ui/image/DraftItemNode.h new file mode 100644 index 0000000000000000000000000000000000000000..e331806a0f81adf197faf397bb64cdc335d72ee4 --- /dev/null +++ b/Grinder/ui/image/DraftItemNode.h @@ -0,0 +1,71 @@ +/****************************************************************************** + * File: DraftItemNode.h + * Date: 21.3.2018 + *****************************************************************************/ + +#ifndef DRAFTITEMNODE_H +#define DRAFTITEMNODE_H + +#include "ImageEditorNode.h" +#include "ImageEditorStyle.h" +#include "InPlaceEditor.h" +#include "image/DraftItemRendererBase.h" + +namespace grndr +{ + class DraftItemRendererBase; + + class DraftItemNode : public ImageEditorNode + { + Q_OBJECT + + public: + DraftItemNode(ImageEditorScene* scene, const std::shared_ptr<DraftItem>& item, QGraphicsItem* parent = nullptr); + + public: + void initDraftItemNode(); + + public: + std::weak_ptr<DraftItem>& draftItem() { return _draftItem; } + const std::weak_ptr<DraftItem>& draftItem() const { return _draftItem; } + + public: + virtual void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) override; + virtual QPainterPath shape() const override; + + public slots: + void updateNode(); + + void updateZOrder(); + void updateVisibility(); + + protected: + virtual std::unique_ptr<InPlaceEditor> createInPlaceEditor() { return std::unique_ptr<InPlaceEditor>{}; } + + protected: + virtual QVariant itemChange(GraphicsItemChange change, const QVariant& value) override; + + protected: + virtual QRect getBoundingRect() const = 0; + virtual void updateGeometry() override; + + virtual std::vector<QAction*> getNodeActions(QMenu& menu) const override { Q_UNUSED(menu); return {nullptr, _deleteAction}; } + virtual std::vector<QAction*> getNodesActions(QMenu& menu) const override; + + protected slots: + void deleteNode(); + + protected: + std::weak_ptr<DraftItem> _draftItem; + std::unique_ptr<DraftItemRendererBase> _renderer; + + const ImageEditorStyle::DraftItemNodeStyle& _draftItemStyle; + + std::unique_ptr<InPlaceEditor> _inPlaceEditor; + + private: + QAction* _deleteAction; + }; +} + +#endif diff --git a/Grinder/ui/image/DraftItemNodeFactory.cpp b/Grinder/ui/image/DraftItemNodeFactory.cpp new file mode 100644 index 0000000000000000000000000000000000000000..5f923191017a94aff9313304de17b27d1a08e967 --- /dev/null +++ b/Grinder/ui/image/DraftItemNodeFactory.cpp @@ -0,0 +1,31 @@ +/****************************************************************************** + * File: ImageEditorNodeFactory.cpp + * Date: 21.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "DraftItemNodeFactory.h" +#include "ImageEditorScene.h" +#include "image/ImageExceptions.h" +#include "DraftItemNode.h" + +#include "draftitems/LineDraftItemNode.h" +#include "draftitems/BoxDraftItemNode.h" + +DraftItemNodeFactory::DraftItemNodeFactory(ImageEditorScene* scene) : VisualNodeFactory(scene) +{ + registerStandardDraftItems(); +} + +DraftItemNode* DraftItemNodeFactory::createDefaultNode(const std::shared_ptr<DraftItem>& item) const +{ + // Each draft item must be explicitly registered, so there is no "default" node type + Q_UNUSED(item); + throw ImageEditorException{nullptr, _EXCPT("Tried to create a draft item node of an unknown type")}; +} + +void DraftItemNodeFactory::registerStandardDraftItems() +{ + REGISTER_NODEFACTORY_TYPE(LineDraftItemNode); + REGISTER_NODEFACTORY_TYPE(BoxDraftItemNode); +} diff --git a/Grinder/ui/image/DraftItemNodeFactory.h b/Grinder/ui/image/DraftItemNodeFactory.h new file mode 100644 index 0000000000000000000000000000000000000000..da08b082693f190a5eb2068059f8d5ec2ad56704 --- /dev/null +++ b/Grinder/ui/image/DraftItemNodeFactory.h @@ -0,0 +1,31 @@ +/****************************************************************************** + * File: DraftItemNodeFactory.h + * Date: 21.3.2018 + *****************************************************************************/ + +#ifndef DRAFTITEMNODEFACTORY_H +#define DRAFTITEMNODEFACTORY_H + +#include "ui/visscene/VisualNodeFactory.h" +#include "image/DraftItemType.h" + +namespace grndr +{ + class ImageEditorScene; + class DraftItem; + class DraftItemNode; + + class DraftItemNodeFactory : public VisualNodeFactory<DraftItemNode, DraftItem, DraftItemType, ImageEditorScene> + { + public: + DraftItemNodeFactory(ImageEditorScene* scene); + + protected: + virtual DraftItemNode* createDefaultNode(const std::shared_ptr<DraftItem>& item) const override; + + private: + void registerStandardDraftItems(); + }; +} + +#endif diff --git a/Grinder/ui/image/ImageEditor.cpp b/Grinder/ui/image/ImageEditor.cpp new file mode 100644 index 0000000000000000000000000000000000000000..e05bb36c17c3ab42493dee2eb23ba7cbed27b703 --- /dev/null +++ b/Grinder/ui/image/ImageEditor.cpp @@ -0,0 +1,15 @@ +/****************************************************************************** + * File: ImageEditor.cpp + * Date: 27.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ImageEditor.h" + +ImageEditor::ImageEditor(const Block* block) : + _editorController{this}, _editorEnvironment{&_editorController}, _editorTools{this}, _editorDockWidget{new ImageEditorDockWidget{}}, _editorWidget{new ImageEditorWidget{this, _editorDockWidget}} +{ + // Assign the editor widget to the dock widget + _editorDockWidget->setWidget(_editorWidget); + _editorDockWidget->updateDockName(block); +} diff --git a/Grinder/ui/image/ImageEditor.h b/Grinder/ui/image/ImageEditor.h new file mode 100644 index 0000000000000000000000000000000000000000..eb51378e7d67c6b29109d3e0e3f98d490937c36c --- /dev/null +++ b/Grinder/ui/image/ImageEditor.h @@ -0,0 +1,49 @@ +/****************************************************************************** + * File: ImageEditor.h + * Date: 27.3.2018 + *****************************************************************************/ + +#ifndef IMAGEEDITOR_H +#define IMAGEEDITOR_H + +#include "controller/ImageEditorController.h" +#include "ImageEditorEnvironment.h" +#include "ImageEditorDockWidget.h" +#include "ImageEditorWidget.h" +#include "ImageEditorToolList.h" + +namespace grndr +{ + class Block; + + class ImageEditor : public QObject + { + Q_OBJECT + + public: + ImageEditor(const Block* block); + + public: + ImageEditorController& controller() { return _editorController; } + const ImageEditorController& controller() const { return _editorController; } + ImageEditorEnvironment& environment() { return _editorEnvironment; } + const ImageEditorEnvironment& environment() const { return _editorEnvironment; } + ImageEditorToolList& editorTools() { return _editorTools; } + const ImageEditorToolList& editorTools() const { return _editorTools; } + + ImageEditorDockWidget* dockWidget() { return _editorDockWidget; } + const ImageEditorDockWidget* dockWidget() const { return _editorDockWidget; } + ImageEditorWidget* editorWidget() { return _editorWidget; } + const ImageEditorWidget* editorWidget() const { return _editorWidget; } + + private: + ImageEditorController _editorController; + ImageEditorEnvironment _editorEnvironment; + ImageEditorToolList _editorTools; + + ImageEditorDockWidget* _editorDockWidget{nullptr}; + ImageEditorWidget* _editorWidget{nullptr}; + }; +} + +#endif diff --git a/Grinder/ui/image/ImageEditorComponent.cpp b/Grinder/ui/image/ImageEditorComponent.cpp new file mode 100644 index 0000000000000000000000000000000000000000..caaa8a7a10715867044ccf04049d397b2f378367 --- /dev/null +++ b/Grinder/ui/image/ImageEditorComponent.cpp @@ -0,0 +1,13 @@ +/****************************************************************************** + * File: ImageEditorComponent.cpp + * Date: 28.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ImageEditorComponent.h" + +ImageEditorComponent::ImageEditorComponent(ImageEditor* imageEditor) : + _imageEditor{imageEditor} +{ + +} diff --git a/Grinder/ui/image/ImageEditorComponent.h b/Grinder/ui/image/ImageEditorComponent.h new file mode 100644 index 0000000000000000000000000000000000000000..95936b61e10b87fb45ce0f9b0f452f676ab247ab --- /dev/null +++ b/Grinder/ui/image/ImageEditorComponent.h @@ -0,0 +1,27 @@ +/****************************************************************************** + * File: ImageEditorComponent.h + * Date: 27.3.2018 + *****************************************************************************/ + +#ifndef IMAGEEDITORCOMPONENT_H +#define IMAGEEDITORCOMPONENT_H + +namespace grndr +{ + class ImageEditor; + + class ImageEditorComponent + { + public: + ImageEditorComponent(ImageEditor* imageEditor = nullptr); + + public: + ImageEditor* imageEditor() { return _imageEditor; } + const ImageEditor* imageEditor() const { return _imageEditor; } + + protected: + ImageEditor* _imageEditor{nullptr}; + }; +} + +#endif diff --git a/Grinder/ui/image/ImageEditorDockWidget.cpp b/Grinder/ui/image/ImageEditorDockWidget.cpp new file mode 100644 index 0000000000000000000000000000000000000000..456fe4110e77e5e0a9fafc5425ab6e6ed3298967 --- /dev/null +++ b/Grinder/ui/image/ImageEditorDockWidget.cpp @@ -0,0 +1,44 @@ +/****************************************************************************** + * File: ImageEditorDockWidget.cpp + * Date: 13.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ImageEditorDockWidget.h" +#include "ImageEditorWidget.h" +#include "core/GrinderApplication.h" +#include "ui/StyleSheet.h" +#include "res/Resources.h" + +ImageEditorDockWidget::ImageEditorDockWidget(QWidget *parent) : QDockWidget(parent) +{ + // Some basic setup + setAttribute(Qt::WA_DeleteOnClose); + setFont(GrinderApplication::boldFont(this)); + setFeatures(QDockWidget::AllDockWidgetFeatures); + + setStyleSheet(StyleSheet::loadStyleSheet(FILE_STYLESHEET_IMAGEEDITORDOCKWIDGET)); + + connect(this, &QDockWidget::visibilityChanged, this, &ImageEditorDockWidget::dockShown); +} + +void ImageEditorDockWidget::closeEvent(QCloseEvent* event) +{ + emit dockClosed(this); + QDockWidget::closeEvent(event); +} + +void ImageEditorDockWidget::dockShown(bool shown) +{ + if (shown) + { + if (auto editorWidget = findChild<ImageEditorWidget*>()) + editorWidget->setFocus(); + } +} + +void ImageEditorDockWidget::updateDockName(const Block* block) +{ + setObjectName("ImageEditor@" + block->getFormattedName()); + setWindowTitle(block->getFormattedName()); +} diff --git a/Grinder/ui/image/ImageEditorDockWidget.h b/Grinder/ui/image/ImageEditorDockWidget.h new file mode 100644 index 0000000000000000000000000000000000000000..d99e1b49a8eab0634749bd97d5d970888a9a647b --- /dev/null +++ b/Grinder/ui/image/ImageEditorDockWidget.h @@ -0,0 +1,37 @@ +/****************************************************************************** + * File: ImageEditorDockWidget.h + * Date: 13.3.2018 + *****************************************************************************/ + +#ifndef IMAGEEDITORDOCKWIDGET_H +#define IMAGEEDITORDOCKWIDGET_H + +#include <QDockWidget> +#include <QDebug> + +namespace grndr +{ + class Block; + + class ImageEditorDockWidget : public QDockWidget + { + Q_OBJECT + + public: + ImageEditorDockWidget(QWidget *parent = nullptr); + + public: + void updateDockName(const Block* block); + + signals: + void dockClosed(ImageEditorDockWidget*); + + protected: + virtual void closeEvent(QCloseEvent* event) override; + + private slots: + void dockShown(bool shown); + }; +} + +#endif diff --git a/Grinder/ui/image/ImageEditorEnvironment.cpp b/Grinder/ui/image/ImageEditorEnvironment.cpp new file mode 100644 index 0000000000000000000000000000000000000000..c07991f03b813d621cfee75c6a59a7e22084835c --- /dev/null +++ b/Grinder/ui/image/ImageEditorEnvironment.cpp @@ -0,0 +1,41 @@ +/****************************************************************************** + * File: ImageEditorEnvironment.cpp + * Date: 26.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ImageEditorEnvironment.h" + +ImageEditorEnvironment::ImageEditorEnvironment(ImageEditorController* controller) : + _editorController{controller} +{ + if (!controller) + throw std::invalid_argument{_EXCPT("controller may not be null")}; +} + +void ImageEditorEnvironment::setPrimaryColor(QColor color) +{ + if (color != _primaryColor) + { + _primaryColor = color; + emit primaryColorChanged(color); + } +} + +void ImageEditorEnvironment::setShowDirectionArrows(bool show) +{ + if (show != _showDirectionArrows) + { + _showDirectionArrows = show; + emit showDirectionArrowsChanged(show); + } +} + +void ImageEditorEnvironment::setShowTags(bool show) +{ + if (show != _showTags) + { + _showTags = show; + emit showTagsChanged(show); + } +} diff --git a/Grinder/ui/image/ImageEditorEnvironment.h b/Grinder/ui/image/ImageEditorEnvironment.h new file mode 100644 index 0000000000000000000000000000000000000000..b45a5fd55840bfdbeda99360aaf638dbd408cf21 --- /dev/null +++ b/Grinder/ui/image/ImageEditorEnvironment.h @@ -0,0 +1,48 @@ +/****************************************************************************** + * File: ImageEditorEnvironment.h + * Date: 26.3.2018 + *****************************************************************************/ + +#ifndef IMAGEEDITORENVIRONMENT_H +#define IMAGEEDITORENVIRONMENT_H + +#include <QObject> +#include <QColor> + +namespace grndr +{ + class ImageEditorController; + + class ImageEditorEnvironment : public QObject + { + Q_OBJECT + + public: + ImageEditorEnvironment(ImageEditorController* controller); + + public: + QColor getPrimaryColor() const { return _primaryColor; } + void setPrimaryColor(QColor color); + + bool showDirectionArrows() const { return _showDirectionArrows; } + void setShowDirectionArrows(bool show); + bool showTags() const { return _showTags; } + void setShowTags(bool show); + + signals: + void primaryColorChanged(QColor); + void showDirectionArrowsChanged(bool); + void showTagsChanged(bool); + + private: + ImageEditorController* _editorController{nullptr}; + + private: + QColor _primaryColor{255, 255, 255}; + + bool _showDirectionArrows{true}; + bool _showTags{true}; + }; +} + +#endif diff --git a/Grinder/ui/image/ImageEditorManager.cpp b/Grinder/ui/image/ImageEditorManager.cpp new file mode 100644 index 0000000000000000000000000000000000000000..0a8646c5e4ed20ae4f75b53ab9c0ddc7a7b4d9be --- /dev/null +++ b/Grinder/ui/image/ImageEditorManager.cpp @@ -0,0 +1,125 @@ +/****************************************************************************** + * File: ImageEditorManager.cpp + * Date: 13.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ImageEditorManager.h" +#include "ImageEditorWidget.h" +#include "core/GrinderApplication.h" +#include "image/ImageExceptions.h" +#include "pipeline/Pipeline.h" + +ImageEditorManager::ImageEditorManager(PipelineManager* pipelineManager) : QObject(pipelineManager) +{ + connect(pipelineManager, &PipelineManager::pipelineRemoved, this, &ImageEditorManager::pipelineRemoved); +} + +void ImageEditorManager::showEditor(const Block* block, const std::shared_ptr<grndr::ImageBuild>& imageBuild) +{ + if (!block) + throw std::invalid_argument{_EXCPT("block may not be null")}; + + if (!imageBuild) + throw std::invalid_argument{_EXCPT("imageBuild may not be null")}; + + auto editor = findEditor(block); + + // Create an image editor if necessary + if (!editor) + { + if (!(editor = createEditor(block))) + throw ImageEditorException{nullptr, _EXCPT(QString{"Unable to create an image editor for block '%1'"}.arg(block->getName()))}; + } + + editor->editorWidget()->setImageBuild(imageBuild); + + // Let whoever is interested in the newly created editor handle showing it + emit editorShown(editor); +} + +void ImageEditorManager::closeEditor(const Block* block) +{ + // If an image editor exists for the block, close and remove it + if (auto editor = findEditor(block)) + { + editor->dockWidget()->close(); + removeEditor(block); + } +} + +ImageEditor* ImageEditorManager::createEditor(const Block* block) +{ + auto editor = std::make_unique<ImageEditor>(block); + + // When a dock is closed, remove its associated editor + connect(editor->dockWidget(), &ImageEditorDockWidget::dockClosed, this, &ImageEditorManager::dockClosed); + + _editors.emplace(block, std::move(editor)); + + // If a block is removed, close any open associated editors + connect(block->pipeline(), &Pipeline::blockRemoved, this, &ImageEditorManager::blockRemoved); + + // Reflect pipeline and block renamings + connect(block, &PipelineItem::itemRenamed, this, &ImageEditorManager::updateDockNames); + connect(block->pipeline(), &Pipeline::pipelineRenamed, this, &ImageEditorManager::updateDockNames); + + return _editors[block].get(); +} + +void ImageEditorManager::removeEditor(const Block* block) +{ + if (auto editor = findEditor(block)) + emit editorClosed(editor); + + // Disconnect any signals from the block + disconnect(block, nullptr, this, nullptr); + + _editors.erase(block); + + // Disconnect any signals from the block's pipeline if no further blocks of the same pipeline are associated with an editor + if (std::none_of(_editors.cbegin(), _editors.cend(), [block](const auto& editor) { return editor.first->pipeline() == block->pipeline(); })) + disconnect(block->pipeline(), nullptr, this, nullptr); +} + +ImageEditor* ImageEditorManager::findEditor(const Block* block) const +{ + auto it = _editors.find(block); + + if (it != _editors.end()) + return it->second.get(); + else + return nullptr; +} + +void ImageEditorManager::dockClosed(ImageEditorDockWidget* dock) +{ + // Find the editor associated with the dock and remove it + for (const auto& editor : _editors) + { + if (editor.second->dockWidget() == dock) + { + removeEditor(editor.first); + break; + } + } +} + +void ImageEditorManager::pipelineRemoved(const std::shared_ptr<Pipeline>& pipeline) +{ + // Close any editors associated with one of the pipeline's blocks + for (const auto& block : pipeline->blocks()) + closeEditor(block.get()); +} + +void ImageEditorManager::blockRemoved(const std::shared_ptr<Block>& block) +{ + closeEditor(block.get()); +} + +void ImageEditorManager::updateDockNames() +{ + // Update all dock names + for (const auto& editor : _editors) + editor.second->dockWidget()->updateDockName(editor.first); +} diff --git a/Grinder/ui/image/ImageEditorManager.h b/Grinder/ui/image/ImageEditorManager.h new file mode 100644 index 0000000000000000000000000000000000000000..52a645fadfcac774b817dcdc1a3a6ab126e81b9e --- /dev/null +++ b/Grinder/ui/image/ImageEditorManager.h @@ -0,0 +1,53 @@ +/****************************************************************************** + * File: ImageEditorManager.h + * Date: 13.3.2018 + *****************************************************************************/ + +#ifndef IMAGEEDITORMANAGER_H +#define IMAGEEDITORMANAGER_H + +#include <memory> + +#include "ImageEditor.h" + +namespace grndr +{ + class PipelineManager; + class Pipeline; + class ImageBuild; + + class ImageEditorManager : public QObject + { + Q_OBJECT + + public: + ImageEditorManager(PipelineManager* pipelineManager); + + public: + void showEditor(const Block* block, const std::shared_ptr<ImageBuild>& imageBuild); + void closeEditor(const Block* block); + + signals: + void editorShown(ImageEditor*); + void editorClosed(ImageEditor*); + + private: + ImageEditor* createEditor(const Block* block); + void removeEditor(const Block* block); + + ImageEditor* findEditor(const Block* block) const; + + private slots: + void dockClosed(ImageEditorDockWidget* dock); + + void pipelineRemoved(const std::shared_ptr<Pipeline>& pipeline); + void blockRemoved(const std::shared_ptr<Block>& block); + + void updateDockNames(); + + private: + std::map<const Block*, std::unique_ptr<ImageEditor>> _editors; + }; +} + +#endif diff --git a/Grinder/ui/image/ImageEditorNode.cpp b/Grinder/ui/image/ImageEditorNode.cpp new file mode 100644 index 0000000000000000000000000000000000000000..fa8d8cd53600f15d6f99c83babfa8e085fed25a2 --- /dev/null +++ b/Grinder/ui/image/ImageEditorNode.cpp @@ -0,0 +1,15 @@ +/****************************************************************************** + * File: ImageEditorNode.cpp + * Date: 21.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ImageEditorNode.h" +#include "ImageEditorScene.h" +#include "ImageEditorView.h" + +ImageEditorNode::ImageEditorNode(ImageEditorScene* scene, QGraphicsItem* parent) : VisualNode(scene, parent), ImageEditorComponent(scene->imageEditor()), + _scene{scene}, _style{scene->view()->sceneStyle()} +{ + +} diff --git a/Grinder/ui/image/ImageEditorNode.h b/Grinder/ui/image/ImageEditorNode.h new file mode 100644 index 0000000000000000000000000000000000000000..1bc2d1bbd359436b9c96fbd537781e288970410d --- /dev/null +++ b/Grinder/ui/image/ImageEditorNode.h @@ -0,0 +1,34 @@ +/****************************************************************************** + * File: ImageEditorNode.h + * Date: 21.3.2018 + *****************************************************************************/ + +#ifndef IMAGEEDITORNODE_H +#define IMAGEEDITORNODE_H + +#include <memory> + +#include "ui/visscene/VisualNode.h" +#include "ImageEditorComponent.h" + +namespace grndr +{ + class ImageEditorScene; + class ImageEditor; + class ImageEditorStyle; + class DraftItem; + + class ImageEditorNode : public VisualNode, public ImageEditorComponent + { + Q_OBJECT + + public: + ImageEditorNode(ImageEditorScene* scene, QGraphicsItem* parent = nullptr); + + protected: + ImageEditorScene* _scene{nullptr}; + const ImageEditorStyle& _style; + }; +} + +#endif diff --git a/Grinder/ui/image/ImageEditorPropertyWidget.cpp b/Grinder/ui/image/ImageEditorPropertyWidget.cpp new file mode 100644 index 0000000000000000000000000000000000000000..7f312b3a0752f0ea9d873bee3ad9e0950c906c60 --- /dev/null +++ b/Grinder/ui/image/ImageEditorPropertyWidget.cpp @@ -0,0 +1,135 @@ +/****************************************************************************** + * File: ImageEditorPropertyWidget.cpp + * Date: 23.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ImageEditorPropertyWidget.h" +#include "ImageEditorWidget.h" +#include "ImageEditorScene.h" +#include "core/GrinderApplication.h" +#include "controller/ImageEditorController.h" +#include "util/UIUtils.h" + +ImageEditorPropertyWidget::ImageEditorPropertyWidget(QWidget *parent) : QWidget(parent), + _layout{new QGridLayout{this}} +{ + _layout->setContentsMargins(6, 6, 6, 6); + _layout->setHorizontalSpacing(6); + _layout->setVerticalSpacing(4); + setLayout(_layout); +} + +void ImageEditorPropertyWidget::assignUiComponents(ImageEditor* imageEditor) +{ + if (!imageEditor) + throw std::invalid_argument{_EXCPT("imageEditor may not be null")}; + + _imageEditor = imageEditor; + + // Listen for various events in order to update the property list accordingly + connect(&_imageEditor->editorTools(), &ImageEditorToolList::activeToolChanged, this, &ImageEditorPropertyWidget::imageEditorToolChanged); + connect(&_imageEditor->controller(), &ImageEditorController::imageBuildSwitching, this, &ImageEditorPropertyWidget::imageBuildSwitching); + connect(&_imageEditor->controller(), &ImageEditorController::imageBuildSwitched, this, &ImageEditorPropertyWidget::imageBuildSwitched); +} + +void ImageEditorPropertyWidget::clear() +{ + // Remove all properties from the layout + UIUtils::removeChildrenFromLayout(_layout); +} + +void ImageEditorPropertyWidget::addProperties(const PropertyVector* properties) +{ + clear(); + + bool propertiesEmpty = true; + + if (properties) + { + for (auto& property : *properties) + { + if (!property->hasFlag(PropertyBase::Flag::ReadOnly) && !property->hasFlag(PropertyBase::Flag::Hidden)) + { + addProperty(property); + propertiesEmpty = false; + } + } + } + + if (propertiesEmpty) + { + auto label = new QLabel{"No properties to display"}; + label->setEnabled(false); + _layout->addWidget(label, 0, 0, 1, 2, Qt::AlignCenter); + } + + // Add a spacer item to align all properties + _layout->addItem(new QSpacerItem{0, 0, QSizePolicy::Expanding, QSizePolicy::Expanding}, _layout->rowCount(), 1); +} + +void ImageEditorPropertyWidget::addProperty(const std::shared_ptr<PropertyBase>& property) +{ + auto editor = property->createEditor(nullptr); + auto label = new QLabel{property->getName() + ":"}; + label->setFont(GrinderApplication::boldFont(label)); + + if (!editor) + editor = new QLabel{"No editor available"}; + + editor->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred); + + int row = _layout->rowCount(); + _layout->addWidget(label, row, 0); + _layout->addWidget(editor, row, 1); +} + +void ImageEditorPropertyWidget::populateProperties() +{ + const PropertyVector* properties = &_imageEditor->editorTools().activeTool()->properties(); // Use the active tool's properties by default + + // See if there are any items selected in the editor + if (auto scene = _imageEditor->controller().activeScene()) + { + auto selectedItems = scene->getNodes<DraftItemNode>(true); + + if (!selectedItems.empty()) + { + // TODO: Multi-selection + if (selectedItems.size() == 1) + { + if (auto draftItem = selectedItems.front()->draftItem().lock()) // Make sure that the underlying draft item still exists + properties = &draftItem->properties(); + } + } + } + + addProperties(properties); +} + +void ImageEditorPropertyWidget::imageEditorToolChanged(const ImageEditorTool* tool) +{ + Q_UNUSED(tool); + populateProperties(); +} + +void ImageEditorPropertyWidget::imageBuildSwitching(ImageBuild* imageBuild) +{ + Q_UNUSED(imageBuild); + + clear(); + + if (auto scene = _imageEditor->controller().activeScene()) + disconnect(scene, &ImageEditorScene::selectionChanged, this, &ImageEditorPropertyWidget::populateProperties); +} + +void ImageEditorPropertyWidget::imageBuildSwitched(ImageBuild* imageBuild) +{ + Q_UNUSED(imageBuild); + + // Listen for selection changes in the current editor scene + if (auto scene = _imageEditor->controller().activeScene()) + connect(scene, &ImageEditorScene::selectionChanged, this, &ImageEditorPropertyWidget::populateProperties); + + populateProperties(); +} diff --git a/Grinder/ui/image/ImageEditorPropertyWidget.h b/Grinder/ui/image/ImageEditorPropertyWidget.h new file mode 100644 index 0000000000000000000000000000000000000000..f2e772455234f2b4ba37376cf2b2e0afce76abf3 --- /dev/null +++ b/Grinder/ui/image/ImageEditorPropertyWidget.h @@ -0,0 +1,51 @@ +/****************************************************************************** + * File: ImageEditorPropertyWidget.h + * Date: 23.3.2018 + *****************************************************************************/ + +#ifndef IMAGEEDITORPROPERTYWIDGET_H +#define IMAGEEDITORPROPERTYWIDGET_H + +#include <QWidget> +#include <QGridLayout> + +#include "ImageEditorComponent.h" +#include "common/PropertyVector.h" + +namespace grndr +{ + class ImageEditor; + class ImageEditorTool; + class ImageBuild; + + class ImageEditorPropertyWidget : public QWidget, public ImageEditorComponent + { + Q_OBJECT + + public: + ImageEditorPropertyWidget(QWidget *parent = nullptr); + + public: + void assignUiComponents(ImageEditor* imageEditor); + + public: + void clear(); + + private: + void addProperties(const PropertyVector* properties); + void addProperty(const std::shared_ptr<PropertyBase>& property); + + private slots: + void populateProperties(); + + void imageEditorToolChanged(const ImageEditorTool* tool); + + void imageBuildSwitching(ImageBuild* imageBuild); + void imageBuildSwitched(ImageBuild* imageBuild); + + private: + QGridLayout* _layout{nullptr}; + }; +} + +#endif diff --git a/Grinder/ui/image/ImageEditorScene.cpp b/Grinder/ui/image/ImageEditorScene.cpp new file mode 100644 index 0000000000000000000000000000000000000000..55ba57c5af2b705c5ddfa1dd9a59cd96e82e14a8 --- /dev/null +++ b/Grinder/ui/image/ImageEditorScene.cpp @@ -0,0 +1,192 @@ +/****************************************************************************** + * File: ImageEditorScene.cpp + * Date: 15.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ImageEditorScene.h" +#include "ImageEditor.h" +#include "DraftItemNode.h" +#include "image/ImageExceptions.h" +#include "controller/ImageEditorController.h" +#include "util/CVUtils.h" +#include "res/Resources.h" + +ImageEditorScene::ImageEditorScene(ImageEditor* imageEditor, ImageBuild* imageBuild, ImageEditorView* view) : VisualScene(view), ImageEditorComponent(imageEditor), + _imageBuild{imageBuild}, _nodeFactory{this} +{ + if (!imageEditor) + throw std::invalid_argument{_EXCPT("imageEditor may not be null")}; + + if (!imageBuild) + throw std::invalid_argument{_EXCPT("imageBuild may not be null")}; + + _drawGrid = false; +} + +void ImageEditorScene::buildScene() +{ + clear(); + + createBackgroundItem(); + + // Add all draft items of each layer to the scene + if (_imageBuild) + { + for (const auto& layer : _imageBuild->layers()) + { + for (const auto& item : layer->draftItems()) + createDraftItemNode(item); + } + } +} + +void ImageEditorScene::refreshImage(bool centerOnImage) +{ + // Try to get an image from the image build's data + auto image = CVUtils::matrixToPixmap(_imageBuild->imageData()); + + if (!image.isNull()) + { + _imageItem->setPixmap(image); + + // Calculate the new scene rectangle -> add a certain border around the image which is always positioned at (0, 0) + QRectF sceneRect{QPointF{0.0, 0.0}, image.size()}; + sceneRect += _view->sceneStyle().getEditorStyle().editorMargins; + setSceneRect(sceneRect); + + if (centerOnImage) + _view->centerOn(_imageItem); + } + else + { + _imageItem->setPixmap(QPixmap{FILE_ICON_EDITOR_NOIMAGE}); // If the image is invalid, show a question mark indicating an error + throw ImageBuildException{_imageBuild, _EXCPT("Invalid or incompatible image data")}; + } + + update(); +} + +void ImageEditorScene::fitViewToImage() +{ + _view->fitToWindow(_imageItem); +} + +void ImageEditorScene::createDraftItemNode(const std::shared_ptr<DraftItem>& item) +{ + if (!item) + throw std::invalid_argument{_EXCPT("item may not be null")}; + + if (!findDraftItemNode(item.get())) + { + auto node = _nodeFactory.createNode(item); + + try { // Propagate initialization errors to the caller + node->initDraftItemNode(); + } + catch (...) { + throw; + } + + addItem(node); + } +} + +void ImageEditorScene::removeDraftItemNode(DraftItemNode* node) +{ + if (node) + delete node; +} + +void ImageEditorScene::updateDraftItemsOrder() +{ + for (auto& draftItem : findDraftItemNodes()) + draftItem->updateZOrder(); +} + +void ImageEditorScene::updateDraftItemsVisibility(const Layer* layer) +{ + for (auto& draftItem : findDraftItemNodes(layer)) + draftItem->updateVisibility(); +} + +void ImageEditorScene::mousePressEvent(QGraphicsSceneMouseEvent* mouseEvent) +{ + if (handleMouseEvent(mouseEvent, &ImageEditorTool::handleMousePressEvent) != ImageEditorTool::InputEventResult::Process) + VisualScene::mousePressEvent(mouseEvent); // The active tool didn't process the event, so call the default handler +} + +void ImageEditorScene::mouseMoveEvent(QGraphicsSceneMouseEvent* mouseEvent) +{ + if (handleMouseEvent(mouseEvent, &ImageEditorTool::handleMouseMoveEvent) != ImageEditorTool::InputEventResult::Process) + VisualScene::mouseMoveEvent(mouseEvent); // The active tool didn't process the event, so call the default handler +} + +void ImageEditorScene::mouseReleaseEvent(QGraphicsSceneMouseEvent* mouseEvent) +{ + if (handleMouseEvent(mouseEvent, &ImageEditorTool::handleMouseReleaseEvent) != ImageEditorTool::InputEventResult::Process) + VisualScene::mouseReleaseEvent(mouseEvent); // The active tool didn't process the event, so call the default handler +} + +void ImageEditorScene::keyPressEvent(QKeyEvent* keyEvent) +{ + if (handleKeyEvent(keyEvent, &ImageEditorTool::handleKeyPressEvent) != ImageEditorTool::InputEventResult::Process) + VisualScene::keyPressEvent(keyEvent); // The active tool didn't process the event, so call the default handler +} + +void ImageEditorScene::keyReleaseEvent(QKeyEvent* keyEvent) +{ + if (handleKeyEvent(keyEvent, &ImageEditorTool::handleKeyReleaseEvent) != ImageEditorTool::InputEventResult::Process) + VisualScene::keyReleaseEvent(keyEvent); // The active tool didn't process the event, so call the default handler +} + +void ImageEditorScene::createBackgroundItem() +{ + // Set up the background item + _imageItem = new QGraphicsPixmapItem{}; + _imageItem->setPos(QPointF{0.0, 0.0}); + _imageItem->setZValue(-1.0); // Always the lowest item + _imageItem->setShapeMode(QGraphicsPixmapItem::BoundingRectShape); + _imageItem->setTransformationMode(Qt::SmoothTransformation); + + addItem(_imageItem); +} + +ImageEditorTool::InputEventResult ImageEditorScene::handleMouseEvent(QGraphicsSceneMouseEvent* mouseEvent, std::function<ImageEditorTool::InputEventResult(ImageEditorTool*, QGraphicsSceneMouseEvent*)> handler) +{ + mouseEvent->ignore(); + + // Let the active tool handle the mouse event + if (auto activeTool = _imageEditor->editorTools().activeTool()) + return handler(activeTool, mouseEvent); + + return ImageEditorTool::InputEventResult::Ignore; +} + +VisualSceneInputHandler::InputEventResult ImageEditorScene::handleKeyEvent(QKeyEvent* keyEvent, std::function<VisualSceneInputHandler::InputEventResult (ImageEditorTool*, QKeyEvent*)> handler) +{ + keyEvent->ignore(); + + // Let the active tool handle the key event + if (auto activeTool = _imageEditor->editorTools().activeTool()) + return handler(activeTool, keyEvent); + + return ImageEditorTool::InputEventResult::Ignore; +} + +DraftItemNode* ImageEditorScene::_findDraftItemNode(const DraftItem* item) const +{ + for (const auto& node : items()) + { + if (auto itemNode = dynamic_cast<DraftItemNode*>(node)) + { + if (auto draftItem = itemNode->draftItem().lock()) + { + if (draftItem.get() == item) + return itemNode; + } + } + } + + return nullptr; +} diff --git a/Grinder/ui/image/ImageEditorScene.h b/Grinder/ui/image/ImageEditorScene.h new file mode 100644 index 0000000000000000000000000000000000000000..b4dc7d4049edadb995ce9e2ccc24ba7bbd50827b --- /dev/null +++ b/Grinder/ui/image/ImageEditorScene.h @@ -0,0 +1,84 @@ +/****************************************************************************** + * File: ImageEditorScene.h + * Date: 15.3.2018 + *****************************************************************************/ + +#ifndef IMAGEEDITORSCENE_H +#define IMAGEEDITORSCENE_H + +#include "ui/visscene/VisualScene.h" +#include "ImageEditorView.h" +#include "ImageEditorComponent.h" +#include "DraftItemNodeFactory.h" +#include "ImageEditorTool.h" +#include "image/ImageBuild.h" + +namespace grndr +{ + class ImageEditor; + class ImageEditorNode; + + class ImageEditorScene : public VisualScene<ImageEditorView>, public ImageEditorComponent + { + Q_OBJECT + + public: + ImageEditorScene(ImageEditor* imageEditor, ImageBuild* imageBuild, ImageEditorView* view); + + public: + void buildScene(); + + void refreshImage(bool centerOnImage = false); + void fitViewToImage(); + + void createDraftItemNode(const std::shared_ptr<DraftItem>& item); + void removeDraftItemNode(const std::shared_ptr<DraftItem>& item) { removeDraftItemNode(findDraftItemNode(item.get())); } + void removeDraftItemNode(DraftItemNode* node); + + void updateDraftItemsOrder(); + void updateDraftItemsVisibility(const Layer* layer = nullptr); + + public: + DraftItemNode* findDraftItemNode(const DraftItem* item) { return _findDraftItemNode(item); } + const DraftItemNode* findDraftItemNode(const DraftItem* item) const { return _findDraftItemNode(item); } + std::vector<DraftItemNode*> findDraftItemNodes(const Layer* layer = nullptr) { return _findDraftItemNodes<DraftItemNode*>(layer); } + std::vector<const DraftItemNode*> findDraftItemNodes(const Layer* layer = nullptr) const { return _findDraftItemNodes<const DraftItemNode*>(layer); } + + public: + ImageBuild* imageBuild() { return _imageBuild; } + const ImageBuild* imageBuild() const { return _imageBuild; } + + const DraftItemNodeFactory& nodeFactory() const { return _nodeFactory; } + + protected: + virtual void mousePressEvent(QGraphicsSceneMouseEvent* mouseEvent) override; + virtual void mouseMoveEvent(QGraphicsSceneMouseEvent* mouseEvent) override; + virtual void mouseReleaseEvent(QGraphicsSceneMouseEvent* mouseEvent) override; + + virtual void keyPressEvent(QKeyEvent* keyEvent) override; + virtual void keyReleaseEvent(QKeyEvent* keyEvent) override; + + private: + void createBackgroundItem(); + + ImageEditorTool::InputEventResult handleMouseEvent(QGraphicsSceneMouseEvent* mouseEvent, std::function<ImageEditorTool::InputEventResult(ImageEditorTool*, QGraphicsSceneMouseEvent*)> handler); + ImageEditorTool::InputEventResult handleKeyEvent(QKeyEvent* keyEvent, std::function<ImageEditorTool::InputEventResult(ImageEditorTool*, QKeyEvent*)> handler); + + private: + DraftItemNode* _findDraftItemNode(const DraftItem* item) const; + template<typename ValType> + std::vector<ValType> _findDraftItemNodes(const Layer* layer = nullptr) const; + + private: + ImageBuild* _imageBuild{nullptr}; + + DraftItemNodeFactory _nodeFactory; + + private: + QGraphicsPixmapItem* _imageItem{nullptr}; + }; +} + +#include "ImageEditorScene.impl.h" + +#endif diff --git a/Grinder/ui/image/ImageEditorScene.impl.h b/Grinder/ui/image/ImageEditorScene.impl.h new file mode 100644 index 0000000000000000000000000000000000000000..6473129417c5dd3d379c18a870cb99d154969542 --- /dev/null +++ b/Grinder/ui/image/ImageEditorScene.impl.h @@ -0,0 +1,28 @@ +/****************************************************************************** + * File: ImageEditorScene.impl.h + * Date: 21.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ImageEditorScene.h" +#include "DraftItemNode.h" + +template<typename ValType> +std::vector<ValType> ImageEditorScene::_findDraftItemNodes(const Layer* layer) const +{ + std::vector<ValType> nodes; + + for (const auto& node : items()) + { + if (auto itemNode = dynamic_cast<DraftItemNode*>(node)) + { + if (auto item = itemNode->draftItem().lock()) + { + if (!layer || item->layer() == layer) + nodes.push_back(itemNode); + } + } + } + + return nodes; +} diff --git a/Grinder/ui/image/ImageEditorStyle.cpp b/Grinder/ui/image/ImageEditorStyle.cpp new file mode 100644 index 0000000000000000000000000000000000000000..392d7292a94914421ff5a533afff7a30d0bf814c --- /dev/null +++ b/Grinder/ui/image/ImageEditorStyle.cpp @@ -0,0 +1,12 @@ +/****************************************************************************** + * File: ImageEditorStyle.cpp + * Date: 16.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ImageEditorStyle.h" + +ImageEditorStyle::ImageEditorStyle() +{ + _viewStyle.maxZoomLevel = 10.0; +} diff --git a/Grinder/ui/image/ImageEditorStyle.h b/Grinder/ui/image/ImageEditorStyle.h new file mode 100644 index 0000000000000000000000000000000000000000..1ea70e1e0899311ab61c90a422fc00e6cbb5aca6 --- /dev/null +++ b/Grinder/ui/image/ImageEditorStyle.h @@ -0,0 +1,57 @@ +/****************************************************************************** + * File: ImageEditorStyle.h + * Date: 16.3.2018 + *****************************************************************************/ + +#ifndef IMAGEEDITORSTYLE_H +#define IMAGEEDITORSTYLE_H + +#include <QMarginsF> +#include <QPalette> + +#include "ui/visscene/VisualSceneStyle.h" +#include "image/DraftItemRendererBase.h" + +namespace grndr +{ + class ImageEditorStyle : public VisualSceneStyle + { + public: + struct EditorStyle + { + QMarginsF editorMargins{100.0, 100.0, 100.0, 100.0}; + }; + + struct DraftItemNodeStyle : public DraftItemRendererBase::RendererStyle + { + + }; + + struct DragHandleStyle + { + QSizeF handleSize{15.0, 15.0}; + float radius{5.0}; + QColor outerBorderColor{255, 255, 255}; + float outerBorderWidth{3.0}; + QColor innerBorderColor{0, 0, 0}; + float innerBorderWidth{1.0}; + }; + + public: + ImageEditorStyle(); + + public: + const EditorStyle& getEditorStyle() const { return _editorStyle; } + + const DraftItemNodeStyle& getDraftItemNodeStyle() const { return _draftItemNodeStyle; } + const DragHandleStyle& getDragHandleStyle() const { return _dragHandleStyle; } + + private: + EditorStyle _editorStyle; + + DraftItemNodeStyle _draftItemNodeStyle; + DragHandleStyle _dragHandleStyle; + }; +} + +#endif diff --git a/Grinder/ui/image/ImageEditorTool.cpp b/Grinder/ui/image/ImageEditorTool.cpp new file mode 100644 index 0000000000000000000000000000000000000000..afa19447a8ec7ba9ca6fbd7598f38bf3ca9d7d49 --- /dev/null +++ b/Grinder/ui/image/ImageEditorTool.cpp @@ -0,0 +1,19 @@ +/****************************************************************************** + * File: ImageEditorTool.cpp + * Date: 22.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ImageEditorTool.h" + +ImageEditorTool::ImageEditorTool(ImageEditor* imageEditor, QString name, QString icon, QString shortcut, QCursor cursor) : ImageEditorComponent(imageEditor), + _name{name}, _icon{icon}, _shortcut{shortcut}, _cursor{cursor} +{ + if (!imageEditor) + throw std::invalid_argument{_EXCPT("imageEditor may not be null")}; +} + +void ImageEditorTool::initImageEditorTool() +{ + createProperties(); +} diff --git a/Grinder/ui/image/ImageEditorTool.h b/Grinder/ui/image/ImageEditorTool.h new file mode 100644 index 0000000000000000000000000000000000000000..845ee1087b5eced3c44241494a08c0e80ba8cc3e --- /dev/null +++ b/Grinder/ui/image/ImageEditorTool.h @@ -0,0 +1,53 @@ +/****************************************************************************** + * File: ImageEditorTool.h + * Date: 22.3.2018 + *****************************************************************************/ + +#ifndef IMAGEEDITORTOOL_H +#define IMAGEEDITORTOOL_H + +#include <QIcon> +#include <QCursor> +#include <QKeySequence> + +#include "common/PropertyObject.h" +#include "ui/visscene/VisualSceneInputHandler.h" +#include "ImageEditorComponent.h" + +namespace grndr +{ + class ImageEditor; + + class ImageEditorTool : public PropertyObject, public ImageEditorComponent, public VisualSceneInputHandler + { + Q_OBJECT + + public: + ImageEditorTool(ImageEditor* imageEditor, QString name, QString icon, QString shortcut, QCursor cursor = QCursor{Qt::ArrowCursor}); + + public: + void initImageEditorTool(); + + public: + virtual void toolActivated(ImageEditorTool* prevTool) { Q_UNUSED(prevTool); } + virtual void toolDeactivated(ImageEditorTool* nextTool) { Q_UNUSED(nextTool); } + + public: + virtual QString getToolType() const = 0; + + QString getName() const { return _name; } + QIcon getIcon() const { return _icon; } + QString getShortcut() const { return _shortcut; } + bool isActionShortcut() const { return _isActionShortcut; } + QCursor getCursor() const { return _cursor; } + + protected: + QString _name{""}; + QIcon _icon; + QString _shortcut; + bool _isActionShortcut{true}; + QCursor _cursor; + }; +} + +#endif diff --git a/Grinder/ui/image/ImageEditorToolList.cpp b/Grinder/ui/image/ImageEditorToolList.cpp new file mode 100644 index 0000000000000000000000000000000000000000..fce04df66499b93612c065c14ce955f6e49ede9e --- /dev/null +++ b/Grinder/ui/image/ImageEditorToolList.cpp @@ -0,0 +1,109 @@ +/****************************************************************************** + * File: ImageEditorToolList.cpp + * Date: 22.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ImageEditorToolList.h" +#include "ImageEditor.h" +#include "util/UIUtils.h" + +#include "tools/DefaultImageEditorTool.h" +#include "tools/BoxDraftItemTool.h" +#include "tools/LineDraftItemTool.h" +#include "tools/ColorPickerTool.h" + +ImageEditorToolList::ImageEditorToolList(ImageEditor* imageEditor) : ImageEditorComponent(imageEditor) +{ + _tools.reserve(3); + + // Create all tools but do not create actions yet + _tools.push_back(ToolEntry{createTool<DefaultImageEditorTool>(imageEditor), nullptr}); + _tools.push_back(ToolEntry{createTool<ColorPickerTool>(imageEditor), nullptr}); + _tools.push_back(ToolEntry{createTool<BoxDraftItemTool>(imageEditor), nullptr}); + _tools.push_back(ToolEntry{createTool<LineDraftItemTool>(imageEditor), nullptr}); +} + +void ImageEditorToolList::assignUiComponents(QWidget* actionOwner) +{ + // Create all actions + for (auto& toolEntry : _tools) + toolEntry.toolAction = createAction(toolEntry.tool.get(), actionOwner); + + activateDefaultTool(); +} + +void ImageEditorToolList::activateTool(QString toolType) +{ + for (auto& toolEntry : _tools) + { + if (toolEntry.tool->getToolType() == toolType) + { + setActiveTool(toolEntry.toolAction); + break; + } + } +} + +void ImageEditorToolList::activateDefaultTool() +{ + activateTool(DefaultImageEditorTool::tool_type); +} + +std::vector<QAction*> ImageEditorToolList::getActions() const +{ + std::vector<QAction*> actions; + + for (auto& toolEntry : _tools) + actions.push_back(toolEntry.toolAction); + + return actions; +} + +void ImageEditorToolList::toolSelected() +{ + if (auto toolAction = dynamic_cast<QAction*>(sender())) + setActiveTool(toolAction); +} + +QAction* ImageEditorToolList::createAction(const ImageEditorTool* tool, QWidget* owner) +{ + QString toolTip = QString{"%1 (%2)"}.arg(tool->getName()).arg(tool->getShortcut()); + auto action = UIUtils::createAction(owner, tool->getName(), "", SLOT(toolSelected()), toolTip, tool->isActionShortcut() ? tool->getShortcut() : "", Qt::WidgetWithChildrenShortcut, this); + + action->setCheckable(true); + action->setIcon(tool->getIcon()); + + return action; +} + +void ImageEditorToolList::setActiveTool(QAction* toolAction) +{ + for (auto& toolEntry : _tools) + { + // Restore previously unset shortcuts + if (toolEntry.tool->isActionShortcut()) + toolEntry.toolAction->setShortcut(toolEntry.tool->getShortcut()); + + if (toolEntry.toolAction == toolAction && toolEntry.tool.get() != _activeTool) + { + auto prevTool = _activeTool; + + emit activeToolChanging(toolEntry.tool.get()); + + if (prevTool) + prevTool->toolDeactivated(toolEntry.tool.get()); + + _activeTool = toolEntry.tool.get(); + + emit activeToolChanged(_activeTool); + _activeTool->toolActivated(prevTool); + + // Remove the action shortcut from the currently active tool so that its key sequence can be handled by the image editor (necessary for deselecting all items by pressing Esc) + if (toolEntry.tool->isActionShortcut()) + toolEntry.toolAction->setShortcut(QKeySequence{}); + } + + toolEntry.toolAction->setChecked(toolEntry.toolAction == toolAction); + } +} diff --git a/Grinder/ui/image/ImageEditorToolList.h b/Grinder/ui/image/ImageEditorToolList.h new file mode 100644 index 0000000000000000000000000000000000000000..67589df613518e88ff6f8f07a54adfd03eb413c6 --- /dev/null +++ b/Grinder/ui/image/ImageEditorToolList.h @@ -0,0 +1,60 @@ +/****************************************************************************** + * File: ImageEditorToolList.h + * Date: 22.3.2018 + *****************************************************************************/ + +#ifndef IMAGEEDITORTOOLLIST_H +#define IMAGEEDITORTOOLLIST_H + +#include "ImageEditorTool.h" +#include "ImageEditorComponent.h" + +namespace grndr +{ + class ImageEditorToolList : public QObject, public ImageEditorComponent + { + Q_OBJECT + + public: + ImageEditorToolList(ImageEditor* imageEditor); + + public: + void assignUiComponents(QWidget* actionOwner); + + public: + ImageEditorTool* activeTool() { return _activeTool; } + const ImageEditorTool* activeTool() const { return _activeTool; } + void activateTool(QString toolType); + void activateDefaultTool(); + + std::vector<QAction*> getActions() const; + + signals: + void activeToolChanging(const ImageEditorTool*); + void activeToolChanged(const ImageEditorTool*); + + private slots: + void toolSelected(); + + private: + template<typename ToolType> + std::unique_ptr<ToolType> createTool(ImageEditor* imageEditor); + QAction* createAction(const ImageEditorTool* tool, QWidget* owner); + + void setActiveTool(QAction* toolAction); + + private: + struct ToolEntry + { + std::unique_ptr<ImageEditorTool> tool; + QAction* toolAction{nullptr}; + }; + + std::vector<ToolEntry> _tools; + ImageEditorTool* _activeTool{nullptr}; + }; +} + +#include "ImageEditorToolList.impl.h" + +#endif diff --git a/Grinder/ui/image/ImageEditorToolList.impl.h b/Grinder/ui/image/ImageEditorToolList.impl.h new file mode 100644 index 0000000000000000000000000000000000000000..59e513d6b19523ca033c150d5f31e5fa0fdc9c8b --- /dev/null +++ b/Grinder/ui/image/ImageEditorToolList.impl.h @@ -0,0 +1,15 @@ +/****************************************************************************** + * File: ImageEditorToolList.impl.h + * Date: 22.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ImageEditorToolList.h" + +template<typename ToolType> +std::unique_ptr<ToolType> ImageEditorToolList::createTool(ImageEditor* imageEditor) +{ + auto tool = std::make_unique<ToolType>(imageEditor); + tool->initImageEditorTool(); + return tool; +} diff --git a/Grinder/ui/image/ImageEditorView.cpp b/Grinder/ui/image/ImageEditorView.cpp new file mode 100644 index 0000000000000000000000000000000000000000..1b13ef3fae4a08e5453b3c08f519db394a26b017 --- /dev/null +++ b/Grinder/ui/image/ImageEditorView.cpp @@ -0,0 +1,151 @@ +/****************************************************************************** + * File: ImageEditorView.cpp + * Date: 15.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ImageEditorView.h" +#include "ImageEditorScene.h" +#include "ImageEditor.h" +#include "controller/ImageEditorController.h" +#include "util/UIUtils.h" +#include "res/Resources.h" + +ImageEditorView::ImageEditorView(QWidget* parent) : VisualSceneView(parent) +{ + // Set the background of the view to a checkerboard pattern + setBackgroundBrush(QPixmap{FILE_ICON_EDITOR_BACKGROUND}); + + // Set widget border + setStyleSheet(QString{"QGraphicsView { border: 1px solid %1; }"}.arg(QPalette{}.color(QPalette::Dark).name())); + + // Create view actions + _showDirectionsAction = UIUtils::createAction(this, "&Show direction arrows", FILE_ICON_EDITOR_SHOWDIRECTIONS, SLOT(showDirectionArrows()), "Show direction arrows on items"); + _showDirectionsAction->setCheckable(true); + _showDirectionsAction->setChecked(true); + + _showTagsAction = UIUtils::createAction(this, "&Show tags", FILE_ICON_EDITOR_SHOWTAGS, SLOT(showTags()), "Show tags on items"); + _showTagsAction->setCheckable(true); + _showTagsAction->setChecked(true); + + _zoomFitAction = UIUtils::createAction(this, "Zoom to &window", FILE_ICON_ZOOMFIT, SLOT(fitImageToWindow()), "Zoom the view to fit the image to its window", "Ctrl+Alt+A"); + + updateActions(); +} + +void ImageEditorView::setEditorScene(ImageEditorScene* scene) +{ + if (_editorScene) + { + disconnect(&_editorScene->imageEditor()->editorTools(), nullptr, this, nullptr); + disconnect(&_editorScene->imageEditor()->environment(), nullptr, this, nullptr); + } + + _editorScene = scene; + VisualSceneView::setScene(scene); + + if (_editorScene) + { + // Update the view's focus and cursor when the active image editor tool has changed + connect(&_editorScene->imageEditor()->editorTools(), &ImageEditorToolList::activeToolChanging, this, &ImageEditorView::imageEditorToolChanging); + connect(&_editorScene->imageEditor()->editorTools(), &ImageEditorToolList::activeToolChanged, this, &ImageEditorView::imageEditorToolChanged); + + // Update the view's actions when the image editor environment has changed + connect(&_editorScene->imageEditor()->environment(), &ImageEditorEnvironment::showDirectionArrowsChanged, this, &ImageEditorView::showDirectionArrowsChanged); + connect(&_editorScene->imageEditor()->environment(), &ImageEditorEnvironment::showTagsChanged, this, &ImageEditorView::showTagsChanged); + } +} + +void ImageEditorView::removeSelectedItems() const +{ + if (_editorScene) + _editorScene->imageEditor()->controller().removeSelectedNodes(); +} + +std::vector<QAction*> ImageEditorView::getActions(AddActionsMode mode) const +{ + std::vector<QAction*> actions; + + if (mode != AddActionsMode::Toolbar) + { + actions.push_back(_selectAllAction); + actions.push_back(_deleteSelectedAction); + actions.push_back(nullptr); + } + + actions.push_back(_showDirectionsAction); + actions.push_back(_showTagsAction); + actions.push_back(nullptr); + actions.push_back(_zoomInAction); + actions.push_back(_zoomOutAction); + actions.push_back(_zoomFullAction); + actions.push_back(_zoomFitAction); + + return actions; +} +void ImageEditorView::drawBackground(QPainter* painter, const QRectF& rect) +{ + // Draw scale-invariant background + auto scaleFactor = painter->transform().m11(); + auto bgRect = rect; + + bgRect.setTopLeft(bgRect.topLeft() * scaleFactor); + bgRect.setBottomRight(bgRect.bottomRight() * scaleFactor); + + painter->scale(1.0 / scaleFactor, 1.0 / scaleFactor); + painter->fillRect(bgRect, backgroundBrush()); + painter->scale(scaleFactor, scaleFactor); +} + +void ImageEditorView::updateActions() +{ + VisualSceneView::updateActions(); + + _zoomFitAction->setEnabled(_scene); +} + +void ImageEditorView::showDirectionArrows() +{ + if (_editorScene) + _editorScene->imageEditor()->environment().setShowDirectionArrows(_showDirectionsAction->isChecked()); +} + +void ImageEditorView::showTags() +{ + if (_editorScene) + _editorScene->imageEditor()->environment().setShowTags(_showTagsAction->isChecked()); +} + +void ImageEditorView::fitImageToWindow() +{ + if (_editorScene) + _editorScene->fitViewToImage(); +} + +void ImageEditorView::imageEditorToolChanging(const ImageEditorTool* tool) +{ + Q_UNUSED(tool); + + // Set the focus to the editor view when changing the current tool + setFocus(); +} + +void ImageEditorView::imageEditorToolChanged(const ImageEditorTool* tool) +{ + setCursor(tool->getCursor()); + + if (_editorScene) + _editorScene->clearSelection(); +} + +void ImageEditorView::showDirectionArrowsChanged(bool show) +{ + _showDirectionsAction->setChecked(show); + update(); +} + +void ImageEditorView::showTagsChanged(bool show) +{ + _showTagsAction->setChecked(show); + update(); +} diff --git a/Grinder/ui/image/ImageEditorView.h b/Grinder/ui/image/ImageEditorView.h new file mode 100644 index 0000000000000000000000000000000000000000..57f312e09276b0b087106e085e3df1c61528979a --- /dev/null +++ b/Grinder/ui/image/ImageEditorView.h @@ -0,0 +1,66 @@ +/****************************************************************************** + * File: ImageEditorView.h + * Date: 15.3.2018 + *****************************************************************************/ + +#ifndef IMAGEEDITORVIEW_H +#define IMAGEEDITORVIEW_H + +#include "ui/visscene/VisualSceneView.h" +#include "ImageEditorStyle.h" + +namespace grndr +{ + class ImageEditorScene; + class ImageEditorTool; + + class ImageEditorView : public VisualSceneView + { + Q_OBJECT + + public: + ImageEditorView(QWidget *parent = nullptr); + + public: + ImageEditorScene* editorScene() { return _editorScene; } + const ImageEditorScene* editorScene() const { return _editorScene; } + void setEditorScene(ImageEditorScene* scene); + + public slots: + virtual void removeSelectedItems() const override; + + public: + virtual const ImageEditorStyle& sceneStyle() const override { return _editorStyle; } + + protected: + virtual std::vector<QAction*> getActions(AddActionsMode mode) const override; + + protected: + virtual void drawBackground(QPainter* painter, const QRectF& rect) override; + + protected slots: + virtual void updateActions() override; + + private slots: + void showDirectionArrows(); + void showTags(); + void fitImageToWindow(); + + void imageEditorToolChanging(const ImageEditorTool* tool); + void imageEditorToolChanged(const ImageEditorTool* tool); + void showDirectionArrowsChanged(bool show); + void showTagsChanged(bool show); + + private: + ImageEditorScene* _editorScene{nullptr}; + + ImageEditorStyle _editorStyle; + + private: + QAction* _showDirectionsAction{nullptr}; + QAction* _showTagsAction{nullptr}; + QAction* _zoomFitAction{nullptr}; + }; +} + +#endif diff --git a/Grinder/ui/image/ImageEditorWidget.cpp b/Grinder/ui/image/ImageEditorWidget.cpp new file mode 100644 index 0000000000000000000000000000000000000000..39b3697e292caa3ffcbd80a35845d394584d8b81 --- /dev/null +++ b/Grinder/ui/image/ImageEditorWidget.cpp @@ -0,0 +1,210 @@ +/****************************************************************************** + * File: ImageEditorWidget.cpp + * Date: 13.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ImageEditorWidget.h" +#include "ui_ImageEditorWidget.h" +#include "tools/DefaultImageEditorTool.h" +#include "core/GrinderApplication.h" +#include "image/ImageBuild.h" +#include "image/ImageExceptions.h" +#include "ui/StyleSheet.h" +#include "util/UIUtils.h" +#include "res/Resources.h" + +ImageEditorWidget::ImageEditorWidget(ImageEditor* imageEditor, QWidget* parent) : QWidget(parent), ImageEditorComponent(imageEditor), + ui{new Ui::ImageEditorWidget} +{ + if (!imageEditor) + throw std::runtime_error{_EXCPT("imageEditor may not be null")}; + + setupUi(); + + // Reflect primary color changes + connect(&_imageEditor->environment(), &ImageEditorEnvironment::primaryColorChanged, this, &ImageEditorWidget::primaryColorChanged); +} + +ImageEditorWidget::~ImageEditorWidget() +{ + delete ui; +} + +void ImageEditorWidget::setImageBuild(const std::shared_ptr<ImageBuild>& imageBuild) +{ + if (auto scene = _imageEditor->controller().activeScene()) + disconnect(scene, nullptr, this, nullptr); + + // Let the controller handle showing the new image build + _imageEditor->controller().showImageBuild(imageBuild.get()); + + // Listen for node selection changes in the new scene to update the primary color + if (auto scene = _imageEditor->controller().activeScene()) + connect(scene, &ImageEditorScene::selectionChanged, this, &ImageEditorWidget::updatePrimaryColor); +} + +void ImageEditorWidget::keyPressEvent(QKeyEvent* event) +{ + QWidget::keyPressEvent(event); + + // Handle the Esc key if it hasn't been consumed + if (!event->isAccepted()) + { + if (event->key() == Qt::Key_Escape) + { + if (_imageEditor->editorTools().activeTool()->getToolType() != DefaultImageEditorTool::tool_type) + { + _imageEditor->editorTools().activateDefaultTool(); + return; + } + } + } +} + +void ImageEditorWidget::setupUi() +{ + ui->setupUi(this); + + connect(ui->primaryColorWidget, &ColorWidget::colorChanged, this, &ImageEditorWidget::primaryColorSelected); + connect(ui->colorPresetsWidget, &ColorPresetsWidget::colorSelected, this, &ImageEditorWidget::colorPresetSelected); + + // Create editor-global actions + _newLayerAction = UIUtils::createAction(this, "&New layer", FILE_ICON_ADD, SLOT(newLayer()), "Add a new layer", "Ctrl+Shift+L", Qt::WidgetWithChildrenShortcut, ui->layersList); + + // Assign the UI components to the controller + _imageEditor->controller().assignUiComponents(ui->imageScene, ui->layersList); + + // Assign the UI components to other UI components + ui->colorPresetsWidget->assignUiComponents(this); + ui->imageEditorProperties->assignUiComponents(_imageEditor); + ui->layersList->assignUiComponents(_imageEditor); + _imageEditor->editorTools().assignUiComponents(this); + + setupImageControlBar(); + + // Set up the various UI components + ui->layersList->setupUi(_newLayerAction, nullptr, ui->layersControlBar); + + ui->propertiesArea->setStyleSheet(StyleSheet::loadStyleSheet(FILE_STYLESHEET_IMAGEEDITORPROPERTIES)); + + updateSceneZoomLevel(ui->imageScene->getZoom()); + updatePrimaryColorLabel(); + + grinder()->settings().getSplitterState("ImageEditorLeft", ui->splitterLeft); + grinder()->settings().getSplitterState("ImageEditorRight", ui->splitterRight); +} + +void ImageEditorWidget::setupImageControlBar() +{ + ui->imageSceneControlBar->setDefaultAlignment(Qt::AlignRight); + ui->imageSceneControlBar->setDefaultToolButtonStyle(Qt::ToolButtonIconOnly); + + // Add all image editor tool actions + for (auto toolAction : _imageEditor->editorTools().getActions()) + ui->imageSceneControlBar->addAction(toolAction, Qt::ToolButtonFollowStyle, Qt::AlignLeft); + + ui->imageScene->setupUi(static_cast<QMenu*>(nullptr), ui->imageSceneControlBar); + + // Add the zoom level label + _imageSceneZoomLabel = new QLabel{""}; + _imageSceneZoomLabel->setAlignment(Qt::AlignCenter); + ui->imageSceneControlBar->addSeparator(); + ui->imageSceneControlBar->addWidget(_imageSceneZoomLabel); + + QFontMetrics fontMetrics{_imageSceneZoomLabel->font()}; + _imageSceneZoomLabel->setMinimumWidth(fontMetrics.width("9999%")); + + // Show the current zoom level in the image scene control bar + connect(ui->imageScene, &VisualSceneView::zoomChanged, this, &ImageEditorWidget::updateSceneZoomLevel); +} + +void ImageEditorWidget::updatePrimaryColorLabel() +{ + auto color = ui->primaryColorWidget->getColor(); + ui->lblPrimaryColor->setText(QString{"R: %1\nG: %2\nB: %3"}.arg(color.red()).arg(color.green()).arg(color.blue())); +} + +void ImageEditorWidget::on_splitterLeft_splitterMoved(int pos, int index) +{ + Q_UNUSED(pos); + Q_UNUSED(index); + + grinder()->settings().setSplitterState("ImageEditorLeft", ui->splitterLeft); +} + +void ImageEditorWidget::on_splitterRight_splitterMoved(int pos, int index) +{ + Q_UNUSED(pos); + Q_UNUSED(index); + + grinder()->settings().setSplitterState("ImageEditorRight", ui->splitterRight); +} + +void ImageEditorWidget::on_primaryColorWidget_customContextMenuRequested(const QPoint& pos) +{ + QMenu menu; + auto presetsMenu = menu.addMenu("&Assign to preset"); + + for (unsigned int i = 0; i < ui->colorPresetsWidget->getPresetsCount(); ++i) + { + auto action = presetsMenu->addAction(QString{"Preset &%1"}.arg(i + 1), this, SLOT(assignPrimaryColorToPreset())); + action->setData(i); + } + + menu.exec(ui->primaryColorWidget->mapToGlobal(pos)); +} + +void ImageEditorWidget::updateSceneZoomLevel(qreal zoomLevel) +{ + _imageSceneZoomLabel->setText(QString{"%1%"}.arg(static_cast<int>(zoomLevel * 100.0))); +} + +void ImageEditorWidget::updatePrimaryColor() +{ + if (auto scene = _imageEditor->controller().activeScene()) + { + auto selNodes = scene->getNodes<DraftItemNode>(true); + + if (!selNodes.empty()) + { + if (auto item = selNodes.front()->draftItem().lock()) // Make sure that the underlying draft item still exists + _imageEditor->environment().setPrimaryColor(item->primaryColor()->getValue()); + } + } +} + +void ImageEditorWidget::primaryColorSelected(QColor color, bool byUser) +{ + if (byUser) + { + _imageEditor->environment().setPrimaryColor(color); + + if (_imageEditor->controller().activeScene()) + { + auto nodes = _imageEditor->controller().activeScene()->getNodes<DraftItemNode>(true); + _imageEditor->controller().setNodesPrimaryColor(color, nodes); + } + } + + updatePrimaryColorLabel(); +} + +void ImageEditorWidget::colorPresetSelected(QColor color) +{ + primaryColorSelected(color, true); +} + +void ImageEditorWidget::assignPrimaryColorToPreset() +{ + if (auto action = dynamic_cast<QAction*>(sender())) + { + unsigned int index = action->data().toUInt(); + ui->colorPresetsWidget->setColor(index, ui->primaryColorWidget->getColor()); + } +} + +void ImageEditorWidget::primaryColorChanged(QColor color) +{ + ui->primaryColorWidget->setColor(color); +} diff --git a/Grinder/ui/image/ImageEditorWidget.h b/Grinder/ui/image/ImageEditorWidget.h new file mode 100644 index 0000000000000000000000000000000000000000..3a50dc88cb1ce52ecc9ebc4a59efe800ea2b22ee --- /dev/null +++ b/Grinder/ui/image/ImageEditorWidget.h @@ -0,0 +1,67 @@ +/****************************************************************************** + * File: ImageEditorWidget.h + * Date: 13.3.2018 + *****************************************************************************/ + +#ifndef IMAGEEDITORWIDGET_H +#define IMAGEEDITORWIDGET_H + +#include <QLabel> +#include <memory> + +#include "ImageEditorComponent.h" + +namespace Ui +{ + class ImageEditorWidget; +} + +namespace grndr +{ + class ImageEditor; + class ImageBuild; + + class ImageEditorWidget : public QWidget, public ImageEditorComponent + { + Q_OBJECT + + public: + ImageEditorWidget(ImageEditor* imageEditor, QWidget* parent = nullptr); + virtual ~ImageEditorWidget(); + + public: + void setImageBuild(const std::shared_ptr<ImageBuild>& imageBuild); + + protected: + virtual void keyPressEvent(QKeyEvent* event) override; + + private: + void setupUi(); + void setupImageControlBar(); + Ui::ImageEditorWidget *ui; + + void updatePrimaryColorLabel(); + + QLabel* _imageSceneZoomLabel{nullptr}; + + private slots: + void on_splitterLeft_splitterMoved(int pos, int index); + void on_splitterRight_splitterMoved(int pos, int index); + + void on_primaryColorWidget_customContextMenuRequested(const QPoint &pos); + + private slots: + void updateSceneZoomLevel(qreal zoomLevel); + + void updatePrimaryColor(); + void primaryColorChanged(QColor color); + void primaryColorSelected(QColor color, bool byUser); + void colorPresetSelected(QColor color); + void assignPrimaryColorToPreset(); + + private: + QAction* _newLayerAction{nullptr}; + }; +} + +#endif diff --git a/Grinder/ui/image/ImageEditorWidget.ui b/Grinder/ui/image/ImageEditorWidget.ui new file mode 100644 index 0000000000000000000000000000000000000000..2dba5dc9bde6c80cc470e7f771fec6dcc1bb376b --- /dev/null +++ b/Grinder/ui/image/ImageEditorWidget.ui @@ -0,0 +1,314 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ImageEditorWidget</class> + <widget class="QWidget" name="ImageEditorWidget"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>783</width> + <height>515</height> + </rect> + </property> + <property name="windowTitle"> + <string>Form</string> + </property> + <layout class="QGridLayout" name="gridLayout_3"> + <property name="leftMargin"> + <number>6</number> + </property> + <property name="topMargin"> + <number>6</number> + </property> + <property name="rightMargin"> + <number>6</number> + </property> + <property name="bottomMargin"> + <number>6</number> + </property> + <property name="spacing"> + <number>2</number> + </property> + <item row="0" column="0"> + <widget class="QSplitter" name="splitterRight"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="childrenCollapsible"> + <bool>false</bool> + </property> + <widget class="QWidget" name="widget_2" native="true"> + <layout class="QGridLayout" name="gridLayout_2"> + <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> + <property name="spacing"> + <number>6</number> + </property> + <item row="0" column="0"> + <widget class="ControlBar" name="imageSceneControlBar"> + <property name="minimumSize"> + <size> + <width>0</width> + <height>20</height> + </size> + </property> + <property name="frameShape"> + <enum>QFrame::StyledPanel</enum> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QSplitter" name="splitterLeft"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="childrenCollapsible"> + <bool>false</bool> + </property> + <widget class="QDockWidget" name="dockWidget_2"> + <property name="font"> + <font> + <weight>75</weight> + <bold>true</bold> + </font> + </property> + <property name="features"> + <set>QDockWidget::NoDockWidgetFeatures</set> + </property> + <property name="windowTitle"> + <string>Properties</string> + </property> + <widget class="QWidget" name="dockWidgetContents_2"> + <layout class="QVBoxLayout" name="verticalLayout"> + <property name="leftMargin"> + <number>1</number> + </property> + <property name="topMargin"> + <number>6</number> + </property> + <property name="rightMargin"> + <number>1</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QWidget" name="widget_3" native="true"> + <property name="minimumSize"> + <size> + <width>0</width> + <height>30</height> + </size> + </property> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="spacing"> + <number>6</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="QLabel" name="label"> + <property name="text"> + <string>Color:</string> + </property> + </widget> + </item> + <item> + <widget class="ColorWidget" name="primaryColorWidget" native="true"> + <property name="minimumSize"> + <size> + <width>30</width> + <height>30</height> + </size> + </property> + <property name="contextMenuPolicy"> + <enum>Qt::CustomContextMenu</enum> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="lblPrimaryColor"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="minimumSize"> + <size> + <width>45</width> + <height>0</height> + </size> + </property> + <property name="styleSheet"> + <string notr="true">margin-right: 5px;</string> + </property> + <property name="text"> + <string><html><head/><body><p>R: 255<br/>G: 255<br/>B: 255</p></body></html></string> + </property> + </widget> + </item> + <item> + <widget class="ColorPresetsWidget" name="colorPresetsWidget" native="true"/> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QScrollArea" name="propertiesArea"> + <property name="widgetResizable"> + <bool>true</bool> + </property> + <widget class="ImageEditorPropertyWidget" name="imageEditorProperties"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>163</width> + <height>173</height> + </rect> + </property> + </widget> + </widget> + </item> + </layout> + </widget> + </widget> + <widget class="ImageEditorView" name="imageScene"> + <property name="renderHints"> + <set>QPainter::Antialiasing|QPainter::HighQualityAntialiasing|QPainter::SmoothPixmapTransform|QPainter::TextAntialiasing</set> + </property> + <property name="viewportUpdateMode"> + <enum>QGraphicsView::FullViewportUpdate</enum> + </property> + </widget> + </widget> + </item> + </layout> + </widget> + <widget class="QDockWidget" name="dockWidget"> + <property name="font"> + <font> + <weight>75</weight> + <bold>true</bold> + </font> + </property> + <property name="features"> + <set>QDockWidget::NoDockWidgetFeatures</set> + </property> + <property name="windowTitle"> + <string>Layers</string> + </property> + <widget class="QWidget" name="dockWidgetContents"> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <property name="spacing"> + <number>2</number> + </property> + <property name="leftMargin"> + <number>1</number> + </property> + <property name="topMargin"> + <number>6</number> + </property> + <property name="rightMargin"> + <number>1</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="LayersListWidget" name="layersList"> + <property name="editTriggers"> + <set>QAbstractItemView::EditKeyPressed</set> + </property> + </widget> + </item> + <item> + <widget class="ControlBar" name="layersControlBar"> + <property name="minimumSize"> + <size> + <width>0</width> + <height>20</height> + </size> + </property> + <property name="frameShape"> + <enum>QFrame::StyledPanel</enum> + </property> + </widget> + </item> + </layout> + </widget> + </widget> + </widget> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>ControlBar</class> + <extends>QFrame</extends> + <header>ui/widget/ControlBar.h</header> + <container>1</container> + </customwidget> + <customwidget> + <class>ImageEditorView</class> + <extends>QGraphicsView</extends> + <header>ui/image/ImageEditorView.h</header> + </customwidget> + <customwidget> + <class>LayersListWidget</class> + <extends>QListWidget</extends> + <header>ui/image/LayersListWidget.h</header> + </customwidget> + <customwidget> + <class>ImageEditorPropertyWidget</class> + <extends>QWidget</extends> + <header>ui/image/ImageEditorPropertyWidget.h</header> + <container>1</container> + </customwidget> + <customwidget> + <class>ColorWidget</class> + <extends>QWidget</extends> + <header>ui/widget/ColorWidget.h</header> + <container>1</container> + </customwidget> + <customwidget> + <class>ColorPresetsWidget</class> + <extends>QWidget</extends> + <header>ui/image/ColorPresetsWidget.h</header> + <container>1</container> + </customwidget> + </customwidgets> + <resources/> + <connections/> +</ui> diff --git a/Grinder/ui/image/InPlaceEditor.cpp b/Grinder/ui/image/InPlaceEditor.cpp new file mode 100644 index 0000000000000000000000000000000000000000..33e56299d1429b62297e2a57291eda041fd21d6e --- /dev/null +++ b/Grinder/ui/image/InPlaceEditor.cpp @@ -0,0 +1,77 @@ +/****************************************************************************** + * File: InPlaceEditor.cpp + * Date: 28.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "InPlaceEditor.h" +#include "DraftItemNode.h" +#include "ImageEditorScene.h" + +InPlaceEditor::InPlaceEditor(ImageEditorScene* scene, DraftItemNode* draftItemNode) : ImageEditorNode(scene, draftItemNode), + _draftItemNode{draftItemNode} +{ + if (!draftItemNode) + throw std::invalid_argument{_EXCPT("draftItemNode may not be null")}; + + setFlag(ItemHasNoContents); + setFlag(ItemIsFocusable, false); + setFlag(ItemIsSelectable, false); +} + +void InPlaceEditor::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) +{ + Q_UNUSED(painter); + Q_UNUSED(option); + Q_UNUSED(widget); + + // Don't paint anything by default +} + +QVariant InPlaceEditor::itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant& value) +{ + if (change == QGraphicsItem::ItemVisibleHasChanged) + updateEditor(); + + return ImageEditorNode::itemChange(change, value); +} + +InPlaceEditorDragHandle* InPlaceEditor::createDragHandle(InPlaceEditorDragHandle::AllowedDirections allowedDirections, QPointF position) +{ + auto dragHandle = new InPlaceEditorDragHandle{_scene, allowedDirections, this}; + dragHandle->setPos(position); + + // Listen for drag handle events + connect(dragHandle, &InPlaceEditorDragHandle::handlePressed, this, &InPlaceEditor::handlePressed); + connect(dragHandle, &InPlaceEditorDragHandle::handleMoved, this, &InPlaceEditor::handleMoved); + connect(dragHandle, &InPlaceEditorDragHandle::handleReleased, this, &InPlaceEditor::handleReleased); + + return dragHandle; +} + +void InPlaceEditor::dragHandleReleased(InPlaceEditorDragHandle* dragHandle) +{ + Q_UNUSED(dragHandle); + + // When a handle has been dragged, normalize the property values (e.g., to adjust for negative sizes) + if (auto draftItem = _draftItemNode->draftItem().lock()) // Make sure that the underlying draft item still exists + draftItem->normalizePropertyValues(); +} + +void InPlaceEditor::handlePressed() +{ + if (auto dragHandle = dynamic_cast<InPlaceEditorDragHandle*>(sender())) + dragHandlePressed(dragHandle); +} + +void InPlaceEditor::handleMoved(QPoint offset) +{ + if (auto dragHandle = dynamic_cast<InPlaceEditorDragHandle*>(sender())) + dragHandleMoved(dragHandle, offset); +} + +void InPlaceEditor::handleReleased() +{ + if (auto dragHandle = dynamic_cast<InPlaceEditorDragHandle*>(sender())) + dragHandleReleased(dragHandle); +} diff --git a/Grinder/ui/image/InPlaceEditor.h b/Grinder/ui/image/InPlaceEditor.h new file mode 100644 index 0000000000000000000000000000000000000000..63b8f5aaef73021defbe71ced1674220bd21850d --- /dev/null +++ b/Grinder/ui/image/InPlaceEditor.h @@ -0,0 +1,49 @@ +/****************************************************************************** + * File: InPlaceEditor.h + * Date: 27.3.2018 + *****************************************************************************/ + +#ifndef INPLACEEDITOR_H +#define INPLACEEDITOR_H + +#include "ImageEditorNode.h" +#include "InPlaceEditorDragHandle.h" + +namespace grndr +{ + class DraftItemNode; + + class InPlaceEditor : public ImageEditorNode + { + Q_OBJECT + + public: + InPlaceEditor(ImageEditorScene* scene, DraftItemNode* draftItemNode); + + public: + virtual void updateEditor() = 0; + + public: + virtual void paint(QPainter* painter, const QStyleOptionGraphicsItem *option, QWidget* widget) override; + + protected: + virtual QVariant itemChange(GraphicsItemChange change, const QVariant& value) override; + + protected: + InPlaceEditorDragHandle* createDragHandle(InPlaceEditorDragHandle::AllowedDirections allowedDirections, QPointF position = QPointF{0.0, 0.0}); + + virtual void dragHandlePressed(InPlaceEditorDragHandle* dragHandle) { Q_UNUSED(dragHandle); } + virtual void dragHandleMoved(InPlaceEditorDragHandle* dragHandle, QPoint offset) { Q_UNUSED(dragHandle); Q_UNUSED(offset); } + virtual void dragHandleReleased(InPlaceEditorDragHandle* dragHandle); + + private slots: + void handlePressed(); + void handleMoved(QPoint offset); + void handleReleased(); + + protected: + DraftItemNode* _draftItemNode{nullptr}; + }; +} + +#endif diff --git a/Grinder/ui/image/InPlaceEditorDragHandle.cpp b/Grinder/ui/image/InPlaceEditorDragHandle.cpp new file mode 100644 index 0000000000000000000000000000000000000000..b47f95d78ae1236f5ac7bb9e24b9a6904a42493e --- /dev/null +++ b/Grinder/ui/image/InPlaceEditorDragHandle.cpp @@ -0,0 +1,104 @@ +/****************************************************************************** + * File: InPlaceEditorDragHandle.cpp + * Date: 28.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "InPlaceEditorDragHandle.h" + +InPlaceEditorDragHandle::InPlaceEditorDragHandle(ImageEditorScene* scene, AllowedDirections allowedDirections, QGraphicsItem* parent) : ImageEditorNode(scene, parent), + _dragHandleStyle{_style.getDragHandleStyle()}, _allowedDirections{allowedDirections} +{ + setFlag(ItemIgnoresTransformations); + setFlag(ItemIsFocusable, false); + setFlag(ItemIsSelectable, false); + + updateGeometry(); +} + +void InPlaceEditorDragHandle::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) +{ + Q_UNUSED(option); + Q_UNUSED(widget); + + painter->setRenderHints(QPainter::Antialiasing|QPainter::TextAntialiasing|QPainter::HighQualityAntialiasing|QPainter::SmoothPixmapTransform); + painter->setBrush(Qt::NoBrush); + + QPen outerPen{_dragHandleStyle.outerBorderColor, _dragHandleStyle.outerBorderWidth}; + outerPen.setCapStyle(Qt::RoundCap); + outerPen.setJoinStyle(Qt::RoundJoin); + + QPen innerPen{_dragHandleStyle.innerBorderColor, _dragHandleStyle.innerBorderWidth}; + outerPen.setCapStyle(Qt::RoundCap); + outerPen.setJoinStyle(Qt::RoundJoin); + + // Draw a small two-colored circle as the drag handle + auto circleRect = QRectF{-_dragHandleStyle.radius, -_dragHandleStyle.radius, _dragHandleStyle.radius * 2.0, _dragHandleStyle.radius * 2.0}; + + painter->setPen(outerPen); + painter->drawEllipse(circleRect); + + painter->setPen(innerPen); + painter->drawEllipse(circleRect); +} + +void InPlaceEditorDragHandle::mousePressEvent(QGraphicsSceneMouseEvent* event) +{ + if (event->button() == Qt::LeftButton && event->modifiers() == Qt::NoModifier) + { + _dragInfo.isDragging = true; + _dragInfo.lastMovementPosition = event->scenePos(); + emit handlePressed(); + event->accept(); + } + else + ImageEditorNode::mousePressEvent(event); +} + +void InPlaceEditorDragHandle::mouseMoveEvent(QGraphicsSceneMouseEvent* event) +{ + if (_dragInfo.isDragging) + { + emit handleMoved(restrictMovement(event->scenePos() - _dragInfo.lastMovementPosition)); + _dragInfo.lastMovementPosition = event->scenePos(); + event->accept(); + } + else + ImageEditorNode::mouseMoveEvent(event); +} + +void InPlaceEditorDragHandle::mouseReleaseEvent(QGraphicsSceneMouseEvent* event) +{ + if (event->button() == Qt::LeftButton) + { + _dragInfo.isDragging = false; + emit handleReleased(); + event->accept(); + } + else + ImageEditorNode::mouseReleaseEvent(event); +} + +void InPlaceEditorDragHandle::updateGeometry() +{ + auto width = _dragHandleStyle.handleSize.width(); + auto height = _dragHandleStyle.handleSize.height(); + + _nodeRect = QRectF{-width / 2.0, -height / 2.0, width, height}; + _nodeRectSelected = _nodeRect; + + ImageEditorNode::updateGeometry(); +} + +QPoint InPlaceEditorDragHandle::restrictMovement(QPointF offset) const +{ + QPoint restrictedDelta{0, 0}; + + if (_allowedDirections.testFlag(AllowedDirection::EastWest)) + restrictedDelta.setX(std::lround(offset.x())); + + if (_allowedDirections.testFlag(AllowedDirection::NorthSouth)) + restrictedDelta.setY(std::lround(offset.y())); + + return restrictedDelta; +} diff --git a/Grinder/ui/image/InPlaceEditorDragHandle.h b/Grinder/ui/image/InPlaceEditorDragHandle.h new file mode 100644 index 0000000000000000000000000000000000000000..12984cefa5261967e34264464ccceeb503878265 --- /dev/null +++ b/Grinder/ui/image/InPlaceEditorDragHandle.h @@ -0,0 +1,66 @@ +/****************************************************************************** + * File: InPlaceEditorDragHandle.h + * Date: 28.3.2018 + *****************************************************************************/ + +#ifndef INPLACEEDITORDRAGHANDLE_H +#define INPLACEEDITORDRAGHANDLE_H + +#include "ImageEditorNode.h" +#include "ImageEditorStyle.h" + +namespace grndr +{ + class InPlaceEditorDragHandle : public ImageEditorNode + { + Q_OBJECT + + public: + enum AllowedDirection + { + NorthSouth = 0x01, + EastWest = 0x02, + + All = NorthSouth|EastWest, + }; + + Q_DECLARE_FLAGS(AllowedDirections, AllowedDirection) + + public: + InPlaceEditorDragHandle(ImageEditorScene* scene, AllowedDirections allowedDirections, QGraphicsItem* parent = nullptr); + + public: + virtual void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) override; + + signals: + void handlePressed(); + void handleMoved(QPoint); + void handleReleased(); + + protected: + virtual void mousePressEvent(QGraphicsSceneMouseEvent* event) override; + virtual void mouseMoveEvent(QGraphicsSceneMouseEvent* event) override; + virtual void mouseReleaseEvent(QGraphicsSceneMouseEvent* event) override; + + protected: + virtual void updateGeometry() override; + + private: + QPoint restrictMovement(QPointF offset) const; + + private: + const ImageEditorStyle::DragHandleStyle& _dragHandleStyle; + + AllowedDirections _allowedDirections{AllowedDirection::All}; + + struct + { + bool isDragging{false}; + QPointF lastMovementPosition; + } _dragInfo; + }; +} + +Q_DECLARE_OPERATORS_FOR_FLAGS(grndr::InPlaceEditorDragHandle::AllowedDirections) + +#endif diff --git a/Grinder/ui/image/LayersListItem.cpp b/Grinder/ui/image/LayersListItem.cpp new file mode 100644 index 0000000000000000000000000000000000000000..3f8a9d12b3a39d5e152400576491b291be4a8f5f --- /dev/null +++ b/Grinder/ui/image/LayersListItem.cpp @@ -0,0 +1,23 @@ +/****************************************************************************** + * File: LayersListItem.cpp + * Date: 17.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "LayersListItem.h" +#include "res/Resources.h" + +LayersListItem::LayersListItem(Layer* layer) : ObjectListItem(layer) +{ + setFlags(Qt::ItemIsEnabled|Qt::ItemIsSelectable|Qt::ItemIsEditable|Qt::ItemIsUserCheckable); + setIcon(QIcon{FILE_ICON_LAYER}); + + updateItem(); +} + +void LayersListItem::updateItem() +{ + setText(getName()); + setCheckState(isVisible() ? Qt::Checked : Qt::Unchecked); + base_type::updateItem(); +} diff --git a/Grinder/ui/image/LayersListItem.h b/Grinder/ui/image/LayersListItem.h new file mode 100644 index 0000000000000000000000000000000000000000..7fb65d6fbd2089798473682a9f172c46829f7707 --- /dev/null +++ b/Grinder/ui/image/LayersListItem.h @@ -0,0 +1,28 @@ +/****************************************************************************** + * File: LayersListItem.h + * Date: 17.3.2018 + *****************************************************************************/ + +#ifndef LAYERSLISTITEM_H +#define LAYERSLISTITEM_H + +#include "ui/widget/ObjectListItem.h" +#include "image/Layer.h" + +namespace grndr +{ + class LayersListItem : public ObjectListItem<Layer> + { + public: + LayersListItem(Layer* layer); + + public: + virtual void updateItem() override; + + public: + QString getName() const { return _object->getName(); } + bool isVisible() const { return _object->isVisible(); } + }; +} + +#endif diff --git a/Grinder/ui/image/LayersListWidget.cpp b/Grinder/ui/image/LayersListWidget.cpp new file mode 100644 index 0000000000000000000000000000000000000000..b7d0962b44a8b8c8dc0934951618db7ba4459b80 --- /dev/null +++ b/Grinder/ui/image/LayersListWidget.cpp @@ -0,0 +1,211 @@ +/****************************************************************************** + * File: LayersListWidget.cpp + * Date: 17.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "LayersListWidget.h" +#include "ImageEditor.h" +#include "image/ImageBuild.h" +#include "ui/widget/ControlBar.h" +#include "util/UIUtils.h" +#include "util/StringUtils.h" +#include "res/Resources.h" + +LayersListWidget::LayersListWidget(QWidget* parent) : MetaWidget(parent) +{ + // Create labels actions + _renameLayerAction = UIUtils::createAction(this, "Rename layer", FILE_ICON_EDIT, SLOT(renameLayer()), "Rename the selected layer", "F2"); + _moveUpAction = UIUtils::createAction(this, "Move up", FILE_ICON_MOVEUP, SLOT(moveLayerUp()), "Move the selected layer up", "Ctrl+Shift+Up"); + _moveDownAction = UIUtils::createAction(this, "Move down", FILE_ICON_MOVEDOWN, SLOT(moveLayerDown()), "Move the selected layer down", "Ctrl+Shift+Down"); + _removeLayerAction = UIUtils::createAction(this, "&Remove layer", FILE_ICON_DELETE, SLOT(removeLayer()), "Remove the selected layer", "Del"); + _removeAllLayersAction = UIUtils::createAction(this, "Remove all layers", "", SLOT(removeAllLayers()), "Remove all layers"); + + _moveUpAction->setData(Qt::ToolButtonIconOnly); + _moveDownAction->setData(Qt::ToolButtonIconOnly); + + // Update the image build if the layer has been (un)checked + connect(this, &LayersListWidget::itemChanged, this, &LayersListWidget::layerCheckChanged); + + // Get notified when a label name has been edited + connect(itemDelegate(), &QAbstractItemDelegate::commitData, this, &LayersListWidget::layerRenamed, Qt::QueuedConnection); // Must be queued to prevent issues if renaming fails + + // Listen for item selections in order to update the active layer and actions + connect(this, &LayersListWidget::itemSelectionChanged, this, &LayersListWidget::selectedItemChanged); + + updateActions(); +} + +void LayersListWidget::assignUiComponents(ImageEditor* imageEditor) +{ + if (!imageEditor) + throw std::invalid_argument{_EXCPT("imageEditor may not be null")}; + + _imageEditor = imageEditor; + + // Listen for controller events in order to update the UI accordingly + connect(&imageEditor->controller(), &ImageEditorController::imageBuildSwitched, this, &LayersListWidget::imageBuildSwitched); + connect(&imageEditor->controller(), &ImageEditorController::layerSwitched, this, &LayersListWidget::layerSwitched); +} + +void LayersListWidget::setupUi(QAction* newLayerAction, QMenu* menu, ControlBar* controlBar) +{ + _newLayerAction = newLayerAction; + + MetaWidget::setupUi(menu, controlBar); +} + +void LayersListWidget::swapLayers(int indexOld, int indexNew) +{ + auto layerSelected = currentObjectItem(); + + // Layers are ordered in a highest-to-lowest fashion in the list, so reverse their indices + indexOld = count() - 1 - indexOld; + indexNew = count() - 1 - indexNew; + + auto item = takeItem(indexOld); + insertItem(indexNew, item); + + switchToObjectItem(layerSelected); +} + +std::vector<QAction*> LayersListWidget::getActions(MetaWidget::AddActionsMode mode) const +{ + std::vector<QAction*> actions; + + if (mode == AddActionsMode::ContextMenu || mode == AddActionsMode::Toolbar) + { + if (mode == AddActionsMode::ContextMenu) + { + actions.push_back(_renameLayerAction); + actions.push_back(nullptr); + } + } + + actions.push_back(_newLayerAction); + actions.push_back(_removeLayerAction); + actions.push_back(nullptr); + actions.push_back(_moveUpAction); + actions.push_back(_moveDownAction); + + if (mode == AddActionsMode::ContextMenu) + { + actions.push_back(nullptr); + actions.push_back(_removeAllLayersAction); + } + + return actions; +} + +void LayersListWidget::switchToObjectItem(LayersListItem* item, bool selectItem) +{ + if (item) + { + _imageEditor->controller().switchLayer(item->object()); + + if (selectItem) + setCurrentItem(item); + } +} + +void LayersListWidget::newLayer() +{ + QString newLayerName = StringUtils::generateUniqueItemName(_imageEditor->controller().activeImageBuild()->layers(), "New layer", &Layer::getName); + bool ok = false; + auto name = QInputDialog::getText(this, "Layer name", "Enter the name of the new layer:", QLineEdit::Normal, newLayerName, &ok); + + if (ok) + { + auto layer = _imageEditor->controller().createLayer(name.trimmed()); + + if (layer) + { + // Find the just-created layer item and switch to it + auto item = findObjectItem(layer.get()); + + if (item) + switchToObjectItem(item); + } + } +} + +void LayersListWidget::moveLayerUp() +{ + if (auto layerSelected = currentObjectItem()) + _imageEditor->controller().moveLayer(layerSelected->object(), true); +} + +void LayersListWidget::moveLayerDown() +{ + if (auto layerSelected = currentObjectItem()) + _imageEditor->controller().moveLayer(layerSelected->object(), false); +} + +void LayersListWidget::removeLayer() +{ + if (auto layer = currentObject()) + { + _imageEditor->controller().removeLayer(layer); + + // Switch to the layer that is currently selected if no active one exists + if (!_imageEditor->controller().activeLayer()) + switchToObjectItem(currentObjectItem()); + } +} + +void LayersListWidget::removeAllLayers() +{ + _imageEditor->controller().removeAllLayers(); + updateActions(); +} + +void LayersListWidget::imageBuildSwitched(ImageBuild* imageBuild) +{ + // A new image build is shown in the editor, so populate its layers + populateList(imageBuild->layers(), true, true); +} + +void LayersListWidget::layerCheckChanged(QListWidgetItem* item) const +{ + // Update the visibility of the layer + if (auto layerItem = dynamic_cast<LayersListItem*>(item)) + _imageEditor->controller().setLayerVisibility(layerItem->object(), item->checkState() == Qt::Checked); +} + +void LayersListWidget::layerSwitched(Layer* layer) +{ + objectSwitched(layer); + updateActions(); +} + +void LayersListWidget::layerRenamed(QWidget* editor) const +{ + if (auto layerItem = currentObjectItem()) + { + viewport()->setUpdatesEnabled(false); + + auto newName = reinterpret_cast<QLineEdit*>(editor)->text(); + _imageEditor->controller().renameLayer(layerItem->object(), newName); + + layerItem->updateItem(); + viewport()->setUpdatesEnabled(true); + } +} + +void LayersListWidget::selectedItemChanged() +{ + // The active layer switches immediately when selecting a layer item + switchLayer(); + updateActions(); +} + +void LayersListWidget::updateActions() +{ + bool layerSelected = (currentObjectItem() != nullptr); + + _renameLayerAction->setEnabled(layerSelected); + _moveUpAction->setEnabled(layerSelected && currentRow() > 0); + _moveDownAction->setEnabled(layerSelected && currentRow() < count() - 1); + _removeLayerAction->setEnabled(layerSelected); + _removeAllLayersAction->setEnabled(count() > 0); +} diff --git a/Grinder/ui/image/LayersListWidget.h b/Grinder/ui/image/LayersListWidget.h new file mode 100644 index 0000000000000000000000000000000000000000..3d5617c941c585e63212033b34fb100916141311 --- /dev/null +++ b/Grinder/ui/image/LayersListWidget.h @@ -0,0 +1,69 @@ +/****************************************************************************** + * File: LayersListWidget.h + * Date: 17.3.2018 + *****************************************************************************/ + +#ifndef LAYERSLISTWIDGET_H +#define LAYERSLISTWIDGET_H + +#include "ui/widget/MetaWidget.h" +#include "ui/widget/ObjectListWidget.h" +#include "ImageEditorComponent.h" +#include "LayersListItem.h" + +namespace grndr +{ + class ImageEditor; + class Layer; + + using LayerObjectListWidget = ObjectListWidget<Layer, LayersListItem>; + + class LayersListWidget : public MetaWidget<LayerObjectListWidget>, public ImageEditorComponent + { + Q_OBJECT + + public: + LayersListWidget(QWidget* parent = nullptr); + + public: + void assignUiComponents(ImageEditor* imageEditor); + void setupUi(QAction* newLayerAction, QMenu* menu, ControlBar* controlBar); + + public: + void swapLayers(int indexOld, int indexNew); + + protected: + virtual std::vector<QAction*> getActions(AddActionsMode mode) const override; + + protected: + virtual void switchToObjectItem(LayersListItem* item, bool selectItem = true) override; + + private slots: + void switchLayer() { switchToObjectItem(currentObjectItem()); } + void renameLayer() { editItem(currentItem()); } + void newLayer(); + void moveLayerUp(); + void moveLayerDown(); + void removeLayer(); + void removeAllLayers(); + + void imageBuildSwitched(ImageBuild* imageBuild); + + void layerCheckChanged(QListWidgetItem* item) const; + void layerSwitched(Layer* layer); + void layerRenamed(QWidget* editor) const; + + void selectedItemChanged(); + void updateActions(); + + private: + QAction* _renameLayerAction{nullptr}; + QAction* _newLayerAction{nullptr}; + QAction* _moveUpAction{nullptr}; + QAction* _moveDownAction{nullptr}; + QAction* _removeLayerAction{nullptr}; + QAction* _removeAllLayersAction{nullptr}; + }; +} + +#endif diff --git a/Grinder/ui/image/draftitems/BoxDraftItemNode.cpp b/Grinder/ui/image/draftitems/BoxDraftItemNode.cpp new file mode 100644 index 0000000000000000000000000000000000000000..8fa9920ddc4540f8521e7e2978450fb7b242885a --- /dev/null +++ b/Grinder/ui/image/draftitems/BoxDraftItemNode.cpp @@ -0,0 +1,37 @@ +/****************************************************************************** + * File: BoxDraftItemNode.cpp + * Date: 23.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "BoxDraftItemNode.h" +#include "ui/image/editors/RectangularInPlaceEditor.h" + +const DraftItemType BoxDraftItemNode::type_value = DraftItemType::Box; + +BoxDraftItemNode::BoxDraftItemNode(ImageEditorScene* scene, const std::shared_ptr<DraftItem>& item, QGraphicsItem* parent) : DraftItemNode(scene, item, parent) +{ + updateNode(); +} + +std::unique_ptr<grndr::InPlaceEditor> BoxDraftItemNode::createInPlaceEditor() +{ + if (auto draftItem = boxDraftItem()) // Make sure that the underlying draft item still exists + return std::make_unique<RectangularInPlaceEditor>(_scene, this, draftItem->position(), draftItem->boxSize()); + else + return nullptr; +} + +QRect BoxDraftItemNode::getBoundingRect() const +{ + if (auto draftItem = boxDraftItem()) // Make sure that the underlying draft item still exists + { + int lineWidth = *draftItem->lineWidth(); + QSize size = *draftItem->boxSize() + QSize{lineWidth, lineWidth}; + + int margin = lineWidth * 2; + return QRect{QPoint{-lineWidth / 2, -lineWidth / 2}, size} + QMargins{margin, margin, margin, margin}; + } + else + return QRect{}; +} diff --git a/Grinder/ui/image/draftitems/BoxDraftItemNode.h b/Grinder/ui/image/draftitems/BoxDraftItemNode.h new file mode 100644 index 0000000000000000000000000000000000000000..3d22be05e396b1e9fe9b89c839db0d338a807900 --- /dev/null +++ b/Grinder/ui/image/draftitems/BoxDraftItemNode.h @@ -0,0 +1,35 @@ +/****************************************************************************** + * File: BoxDraftItemNode.h + * Date: 23.3.2018 + *****************************************************************************/ + +#ifndef BOXDRAFTITEMNODE_H +#define BOXDRAFTITEMNODE_H + +#include "image/draftitems/BoxDraftItem.h" +#include "ui/image/DraftItemNode.h" + +namespace grndr +{ + class BoxDraftItemNode : public DraftItemNode + { + Q_OBJECT + + public: + static const DraftItemType type_value; + + public: + BoxDraftItemNode(ImageEditorScene* scene, const std::shared_ptr<DraftItem>& item, QGraphicsItem* parent = nullptr); + + auto boxDraftItem() { return std::dynamic_pointer_cast<BoxDraftItem>(_draftItem.lock()); } + auto boxDraftItem() const { return std::dynamic_pointer_cast<BoxDraftItem>(_draftItem.lock()); } + + protected: + virtual std::unique_ptr<InPlaceEditor> createInPlaceEditor() override; + + protected: + virtual QRect getBoundingRect() const override; + }; +} + +#endif diff --git a/Grinder/ui/image/draftitems/LineDraftItemNode.cpp b/Grinder/ui/image/draftitems/LineDraftItemNode.cpp new file mode 100644 index 0000000000000000000000000000000000000000..49376ae477b02f66d989b1dac5a054997da35ec5 --- /dev/null +++ b/Grinder/ui/image/draftitems/LineDraftItemNode.cpp @@ -0,0 +1,61 @@ +/****************************************************************************** + * File: LineDraftItemNode.cpp + * Date: 23.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "LineDraftItemNode.h" +#include "ui/image/editors/LinearInPlaceEditor.h" +#include "util/MathUtils.h" + +const DraftItemType LineDraftItemNode::type_value = DraftItemType::Line; + +LineDraftItemNode::LineDraftItemNode(ImageEditorScene* scene, const std::shared_ptr<DraftItem>& item, QGraphicsItem* parent) : DraftItemNode(scene, item, parent) +{ + updateNode(); +} + +std::unique_ptr<grndr::InPlaceEditor> LineDraftItemNode::createInPlaceEditor() +{ + if (auto draftItem = lineDraftItem()) // Make sure that the underlying draft item still exists + return std::make_unique<LinearInPlaceEditor>(_scene, this, draftItem->position(), draftItem->endPosition()); + else + return nullptr; +} + +QRect LineDraftItemNode::getBoundingRect() const +{ + if (auto draftItem = lineDraftItem()) // Make sure that the underlying draft item still exists + { + QPoint position = *draftItem->position(); + QPoint endPosition = *draftItem->endPosition(); + auto width = std::abs(position.x() - endPosition.x()); + auto height = std::abs(position.y() - endPosition.y()); + + int margin = *draftItem->lineWidth() * 2; + return QRect{QPoint{0, 0}, QSize{width, height}} + QMargins{margin, margin, margin, margin}; + } + else + return QRect{}; +} + +QVariant LineDraftItemNode::itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant& value) +{ + QVariant result; + + if (change == QGraphicsItem::ItemPositionHasChanged) + { + // Update the line draft item's end position when the item has been moved around + if (auto draftItem = lineDraftItem()) // Make sure that the underlying draft item still exists + { + QPoint newPosition = MathUtils::round(value.toPointF()); + QPoint oldPosition = *draftItem->position(); + auto movementDelta = newPosition - oldPosition; + auto endPosition = *draftItem->endPosition() + movementDelta; + + draftItem->endPosition()->setValue(endPosition); + } + } + + return DraftItemNode::itemChange(change, value); +} diff --git a/Grinder/ui/image/draftitems/LineDraftItemNode.h b/Grinder/ui/image/draftitems/LineDraftItemNode.h new file mode 100644 index 0000000000000000000000000000000000000000..1c063397ce0f3fec82021e7339c638fe44f6f6c2 --- /dev/null +++ b/Grinder/ui/image/draftitems/LineDraftItemNode.h @@ -0,0 +1,40 @@ +/****************************************************************************** + * File: LineDraftItemNode.h + * Date: 23.3.2018 + *****************************************************************************/ + +#ifndef LINEDRAFTITEMNODE_H +#define LINEDRAFTITEMNODE_H + +#include "image/DraftItemType.h" +#include "ui/image/DraftItemNode.h" +#include "image/draftitems/LineDraftItem.h" + +namespace grndr +{ + class LineDraftItemNode : public DraftItemNode + { + Q_OBJECT + + public: + static const DraftItemType type_value; + + public: + LineDraftItemNode(ImageEditorScene* scene, const std::shared_ptr<DraftItem>& item, QGraphicsItem* parent = nullptr); + + public: + auto lineDraftItem() { return std::dynamic_pointer_cast<LineDraftItem>(_draftItem.lock()); } + auto lineDraftItem() const { return std::dynamic_pointer_cast<LineDraftItem>(_draftItem.lock()); } + + protected: + virtual std::unique_ptr<InPlaceEditor> createInPlaceEditor() override; + + protected: + virtual QVariant itemChange(GraphicsItemChange change, const QVariant& value) override; + + protected: + virtual QRect getBoundingRect() const override; + }; +} + +#endif diff --git a/Grinder/ui/image/editors/LinearInPlaceEditor.cpp b/Grinder/ui/image/editors/LinearInPlaceEditor.cpp new file mode 100644 index 0000000000000000000000000000000000000000..fed21771e9fd88c2abbc32cafd2771a6829ce140 --- /dev/null +++ b/Grinder/ui/image/editors/LinearInPlaceEditor.cpp @@ -0,0 +1,40 @@ +/****************************************************************************** + * File: LinearInPlaceEditor.cpp + * Date: 28.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "LinearInPlaceEditor.h" + +LinearInPlaceEditor::LinearInPlaceEditor(ImageEditorScene* scene, DraftItemNode* draftItemNode, PointProperty* startPosition, PointProperty* endPosition) : InPlaceEditor(scene, draftItemNode), + _startPosition{startPosition}, _endPosition{endPosition} +{ + if (!startPosition) + throw std::invalid_argument{_EXCPT("startPosition may not be null")}; + + if (!endPosition) + throw std::invalid_argument{_EXCPT("endPosition may not be null")}; + + _startDragHandle = createDragHandle(InPlaceEditorDragHandle::AllowedDirection::All); + _endDragHandle = createDragHandle(InPlaceEditorDragHandle::AllowedDirection::All); +} + +void LinearInPlaceEditor::updateEditor() +{ + updateDragHandlePositions(); +} + +void LinearInPlaceEditor::dragHandleMoved(InPlaceEditorDragHandle* dragHandle, QPoint offset) +{ + if (dragHandle == _startDragHandle) // The start drag handle changes the start position of the line + _startPosition->setValue(*_startPosition + offset); + else if (dragHandle == _endDragHandle) // The end drag handle changes the end position of the line + _endPosition->setValue(*_endPosition + offset); +} + +void LinearInPlaceEditor::updateDragHandlePositions() +{ + // Reposition the end drag handle; the start drag handle always stays at 0,0 + auto endPosition = *_endPosition - *_startPosition; + _endDragHandle->setPos(endPosition); +} diff --git a/Grinder/ui/image/editors/LinearInPlaceEditor.h b/Grinder/ui/image/editors/LinearInPlaceEditor.h new file mode 100644 index 0000000000000000000000000000000000000000..8ac1ec6f8e560969c687a522b661a0c4d4ed705e --- /dev/null +++ b/Grinder/ui/image/editors/LinearInPlaceEditor.h @@ -0,0 +1,39 @@ +/****************************************************************************** + * File: LinearInPlaceEditor.h + * Date: 28.3.2018 + *****************************************************************************/ + +#ifndef LINEARINPLACEEDITOR_H +#define LINEARINPLACEEDITOR_H + +#include "ui/image/InPlaceEditor.h" +#include "common/properties/StandardProperties.h" + +namespace grndr +{ + class LinearInPlaceEditor : public InPlaceEditor + { + Q_OBJECT + + public: + LinearInPlaceEditor(ImageEditorScene* scene, DraftItemNode* draftItemNode, PointProperty* startPosition, PointProperty* endPosition); + + public: + virtual void updateEditor() override; + + protected: + virtual void dragHandleMoved(InPlaceEditorDragHandle* dragHandle, QPoint offset) override; + + private: + void updateDragHandlePositions(); + + private: + PointProperty* _startPosition{nullptr}; + PointProperty* _endPosition{nullptr}; + + InPlaceEditorDragHandle* _startDragHandle{nullptr}; + InPlaceEditorDragHandle* _endDragHandle{nullptr}; + }; +} + +#endif diff --git a/Grinder/ui/image/editors/RectangularInPlaceEditor.cpp b/Grinder/ui/image/editors/RectangularInPlaceEditor.cpp new file mode 100644 index 0000000000000000000000000000000000000000..3f2a37563fbf5763cdec4f2c706377f67c40f28d --- /dev/null +++ b/Grinder/ui/image/editors/RectangularInPlaceEditor.cpp @@ -0,0 +1,92 @@ +/****************************************************************************** + * File: RectangularInPlaceEditor.cpp + * Date: 29.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "RectangularInPlaceEditor.h" + +#define DRAG_HANDLE_COUNT 8 + +RectangularInPlaceEditor::RectangularInPlaceEditor(ImageEditorScene* scene, DraftItemNode* draftItemNode, PointProperty* position, SizeProperty* size) : InPlaceEditor(scene, draftItemNode), + _position{position}, _size{size} +{ + if (!position) + throw std::invalid_argument{_EXCPT("position may not be null")}; + + if (!size) + throw std::invalid_argument{_EXCPT("size may not be null")}; + + // Create all 8 drag handles, starting with the top-left one and advancing clockwise + for (int i = 1; i <= DRAG_HANDLE_COUNT; ++i) + { + InPlaceEditorDragHandle::AllowedDirection direction = InPlaceEditorDragHandle::AllowedDirection::All; + + if (i % 4 == 0) + direction = InPlaceEditorDragHandle::AllowedDirection::EastWest; + else if (i % 2 == 0) + direction = InPlaceEditorDragHandle::AllowedDirection::NorthSouth; + + _dragHandles.push_back(createDragHandle(direction)); + } +} + +void RectangularInPlaceEditor::updateEditor() +{ + updateDragHandlePositions(); +} + +void RectangularInPlaceEditor::dragHandleMoved(InPlaceEditorDragHandle* dragHandle, QPoint offset) +{ + QSize sizeOffset{0, 0}; + QPoint posOffset{0, 0}; + + auto it = std::find(_dragHandles.cbegin(), _dragHandles.cend(), dragHandle); + + if (it != _dragHandles.cend()) + { + auto handleIndex = it - _dragHandles.cbegin(); + + // Set the offset value depending on where the drag handle is located + switch (handleIndex) + { + case 0: case 1: case 7: + posOffset = offset; + sizeOffset = QSize{-offset.x(), -offset.y()}; + break; + + case 2: + posOffset = QPoint{0, offset.y()}; + sizeOffset = QSize{offset.x(), -offset.y()}; + break; + + case 3: case 4: case 5: + sizeOffset = QSize{offset.x(), offset.y()}; + break; + + case 6: + posOffset = QPoint{offset.x(), 0}; + sizeOffset = QSize{-offset.x(), offset.y()}; + break; + } + } + + _position->setValue(*_position + posOffset); + _size->setValue(*_size + sizeOffset); +} + +void RectangularInPlaceEditor::updateDragHandlePositions() +{ + // Create a vector containing all relative drag handle positions + static const std::vector<QPointF> relativePositions{{0.0, 0.0}, {0.5, 0.0}, {1.0, 0.0}, {1.0, 0.5}, {1.0, 1.0}, {0.5, 1.0}, {0.0, 1.0}, {0.0, 0.5}}; + + // Move all drag handles to their relative positions + QSize size = *_size; + + for (unsigned int i = 0; i < _dragHandles.size(); ++i) + { + QPointF pos{size.width() * relativePositions[i].x(), size.height() * relativePositions[i].y()}; + _dragHandles[i]->setPos(pos); + } +} + diff --git a/Grinder/ui/image/editors/RectangularInPlaceEditor.h b/Grinder/ui/image/editors/RectangularInPlaceEditor.h new file mode 100644 index 0000000000000000000000000000000000000000..4debf911253150241b9fdc37296a370831db99e2 --- /dev/null +++ b/Grinder/ui/image/editors/RectangularInPlaceEditor.h @@ -0,0 +1,38 @@ +/****************************************************************************** + * File: RectangularInPlaceEditor.h + * Date: 29.3.2018 + *****************************************************************************/ + +#ifndef RECTANGULARINPLACEEDITOR_H +#define RECTANGULARINPLACEEDITOR_H + +#include "ui/image/InPlaceEditor.h" +#include "common/properties/StandardProperties.h" + +namespace grndr +{ + class RectangularInPlaceEditor : public InPlaceEditor + { + Q_OBJECT + + public: + RectangularInPlaceEditor(ImageEditorScene* scene, DraftItemNode* draftItemNode, PointProperty* position, SizeProperty* size); + + public: + virtual void updateEditor() override; + + protected: + virtual void dragHandleMoved(InPlaceEditorDragHandle* dragHandle, QPoint offset) override; + + private: + void updateDragHandlePositions(); + + private: + PointProperty* _position{nullptr}; + SizeProperty* _size{nullptr}; + + std::vector<InPlaceEditorDragHandle*> _dragHandles; + }; +} + +#endif diff --git a/Grinder/ui/image/tools/BoxDraftItemTool.cpp b/Grinder/ui/image/tools/BoxDraftItemTool.cpp new file mode 100644 index 0000000000000000000000000000000000000000..4a52472bb0fe04cf28509e2c01b768deec0a663e --- /dev/null +++ b/Grinder/ui/image/tools/BoxDraftItemTool.cpp @@ -0,0 +1,32 @@ +/****************************************************************************** + * File: BoxDraftItemTool.cpp + * Date: 22.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "BoxDraftItemTool.h" +#include "common/properties/RangeConstraint.h" +#include "res/Resources.h" + +const char* BoxDraftItemTool::tool_type = "BoxDraftItemTool"; + +BoxDraftItemTool::BoxDraftItemTool(ImageEditor* imageEditor) : DraftItemTool(imageEditor, DraftItemType::Box, "Bounding box", FILE_ICON_EDITOR_BOX, "B", QCursor{QPixmap{FILE_CURSOR_EDITOR_BOX}, 0, 0}) +{ + +} + +void BoxDraftItemTool::createProperties() +{ + DraftItemTool::createProperties(); + + // Create specific properties + _boxSize = createProperty<SizeProperty>(PropertyID::Size, "Size", QSize{50, 50}); + boxSize()->setDescription("The size of the box."); + + _lineWidth = createProperty<UIntProperty>(PropertyID::LineWidth, "Line width", 5); + lineWidth()->createConstraint<RangeConstraint>(1, 100); + lineWidth()->setDescription("The width of the box lines."); + + // Override some property defaults + hasDirection()->setValue(true); +} diff --git a/Grinder/ui/image/tools/BoxDraftItemTool.h b/Grinder/ui/image/tools/BoxDraftItemTool.h new file mode 100644 index 0000000000000000000000000000000000000000..38b528734204be9ba5554ea85a9c32d97901ae9b --- /dev/null +++ b/Grinder/ui/image/tools/BoxDraftItemTool.h @@ -0,0 +1,39 @@ +/****************************************************************************** + * File: BoxDraftItemTool.h + * Date: 22.3.2018 + *****************************************************************************/ + +#ifndef BOXDRAFTITEMTOOL_H +#define BOXDRAFTITEMTOOL_H + +#include "DraftItemTool.h" + +namespace grndr +{ + class BoxDraftItemTool : public DraftItemTool + { + public: + static const char* tool_type; + + public: + BoxDraftItemTool(ImageEditor* imageEditor); + + public: + virtual QString getToolType() const override { return tool_type; } + + public: + auto boxSize() { return dynamic_cast<SizeProperty*>(_boxSize.get()); } + auto boxSize() const { return dynamic_cast<SizeProperty*>(_boxSize.get()); } + auto lineWidth() { return dynamic_cast<UIntProperty*>(_lineWidth.get()); } + auto lineWidth() const { return dynamic_cast<UIntProperty*>(_lineWidth.get()); } + + protected: + virtual void createProperties() override; + + private: + std::shared_ptr<PropertyBase> _boxSize; + std::shared_ptr<PropertyBase> _lineWidth; + }; +} + +#endif diff --git a/Grinder/ui/image/tools/ColorPickerTool.cpp b/Grinder/ui/image/tools/ColorPickerTool.cpp new file mode 100644 index 0000000000000000000000000000000000000000..834dbc10f783e44b4a36bec6448c37a59df4b00a --- /dev/null +++ b/Grinder/ui/image/tools/ColorPickerTool.cpp @@ -0,0 +1,61 @@ +/****************************************************************************** + * File: ColorPickerTool.cpp + * Date: 22.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ColorPickerTool.h" +#include "controller/ImageEditorController.h" +#include "ui/image/ImageEditor.h" +#include "res/Resources.h" + +const char* ColorPickerTool::tool_type = "ColorPickerTool"; + +ColorPickerTool::ColorPickerTool(ImageEditor* imageEditor) : ImageEditorTool(imageEditor, "Color picker", FILE_ICON_EDITOR_COLORPICKER, "K", QCursor{QPixmap{FILE_CURSOR_EDITOR_COLORPICKER}, 0, 23}) +{ + +} + +void ColorPickerTool::toolActivated(ImageEditorTool* prevTool) +{ + _previousTool = prevTool; + _switchToPreviousTool = false; +} + +ImageEditorTool::InputEventResult ColorPickerTool::mousePressed(const QGraphicsSceneMouseEvent* event) +{ + if (event->widget()) + { + // Grab the current scene display and get the pixel color under the cursor + auto pixmap = event->widget()->grab(); + auto image = pixmap.toImage(); + auto pos = event->widget()->mapFromGlobal(event->screenPos()); + + // Set the grabbed color as the primary one and switch back to the previous tool + _imageEditor->environment().setPrimaryColor(image.pixelColor(pos)); + _switchToPreviousTool = true; + } + + return InputEventResult::Process; +} + +ImageEditorTool::InputEventResult ColorPickerTool::mouseMoved(const QGraphicsSceneMouseEvent* event) +{ + Q_UNUSED(event); + return InputEventResult::Process; +} + +ImageEditorTool::InputEventResult ColorPickerTool::mouseReleased(const QGraphicsSceneMouseEvent* event) +{ + Q_UNUSED(event); + + if (_switchToPreviousTool) + { + if (_previousTool) + _imageEditor->editorTools().activateTool(_previousTool->getToolType()); + + _switchToPreviousTool = false; + } + + return InputEventResult::Process; +} diff --git a/Grinder/ui/image/tools/ColorPickerTool.h b/Grinder/ui/image/tools/ColorPickerTool.h new file mode 100644 index 0000000000000000000000000000000000000000..0df6229f97b59df754b9466f944cc770d319186c --- /dev/null +++ b/Grinder/ui/image/tools/ColorPickerTool.h @@ -0,0 +1,38 @@ +/****************************************************************************** + * File: ColorPickerTool.h + * Date: 22.3.2018 + *****************************************************************************/ + +#ifndef ColorPickerTool_H +#define ColorPickerTool_H + +#include "ui/image/ImageEditorTool.h" + +namespace grndr +{ + class ColorPickerTool : public ImageEditorTool + { + public: + static const char* tool_type; + + public: + ColorPickerTool(ImageEditor* imageEditor); + + public: + virtual void toolActivated(ImageEditorTool* prevTool) override; + + public: + virtual QString getToolType() const override { return tool_type; } + + protected: + virtual InputEventResult mousePressed(const QGraphicsSceneMouseEvent* event) override; + virtual InputEventResult mouseMoved(const QGraphicsSceneMouseEvent* event) override; + virtual InputEventResult mouseReleased(const QGraphicsSceneMouseEvent* event) override; + + private: + ImageEditorTool* _previousTool{nullptr}; + bool _switchToPreviousTool{false}; + }; +} + +#endif diff --git a/Grinder/ui/image/tools/DefaultImageEditorTool.cpp b/Grinder/ui/image/tools/DefaultImageEditorTool.cpp new file mode 100644 index 0000000000000000000000000000000000000000..0964572af11c33c0352f9248b8b610c8874bf9ee --- /dev/null +++ b/Grinder/ui/image/tools/DefaultImageEditorTool.cpp @@ -0,0 +1,15 @@ +/****************************************************************************** + * File: DefaultImageEditorTool.cpp + * Date: 22.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "DefaultImageEditorTool.h" +#include "res/Resources.h" + +const char* DefaultImageEditorTool::tool_type = "DefaultImageEditorTool"; + +DefaultImageEditorTool::DefaultImageEditorTool(ImageEditor* imageEditor) : ImageEditorTool(imageEditor, "Selection tool", FILE_ICON_EDITOR_DEFAULT, "Esc") +{ + _isActionShortcut = false; +} diff --git a/Grinder/ui/image/tools/DefaultImageEditorTool.h b/Grinder/ui/image/tools/DefaultImageEditorTool.h new file mode 100644 index 0000000000000000000000000000000000000000..f09cb6bb1cc820840919644fb550789f534cd3d5 --- /dev/null +++ b/Grinder/ui/image/tools/DefaultImageEditorTool.h @@ -0,0 +1,28 @@ +/****************************************************************************** + * File: DefaultImageEditorTool.h + * Date: 22.3.2018 + *****************************************************************************/ + +#ifndef DEFAULTIMAGEEDITORTOOL_H +#define DEFAULTIMAGEEDITORTOOL_H + +#include "ui/image/ImageEditorTool.h" + +namespace grndr +{ + class DefaultImageEditorTool : public ImageEditorTool + { + Q_OBJECT + + public: + static const char* tool_type; + + public: + DefaultImageEditorTool(ImageEditor* imageEditor); + + public: + virtual QString getToolType() const override { return tool_type; } + }; +} + +#endif diff --git a/Grinder/ui/image/tools/DraftItemTool.cpp b/Grinder/ui/image/tools/DraftItemTool.cpp new file mode 100644 index 0000000000000000000000000000000000000000..52c2eedb969e8dd22a2f50e0b741acd75f20492e --- /dev/null +++ b/Grinder/ui/image/tools/DraftItemTool.cpp @@ -0,0 +1,163 @@ +/****************************************************************************** + * File: DraftItemTool.cpp + * Date: 22.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "DraftItemTool.h" +#include "common/properties/RangeConstraint.h" +#include "controller/ImageEditorController.h" +#include "ui/image/ImageEditor.h" +#include "util/MathUtils.h" + +#include <cmath> + +DraftItemTool::DraftItemTool(ImageEditor* imageEditor, DraftItemType itemType, QString name, QString icon, QString shortcut, QCursor cursor) : ImageEditorTool(imageEditor, name, icon, shortcut, cursor), + _itemType{itemType} +{ + if (itemType == DraftItemType::Undefined) + throw std::invalid_argument{_EXCPT("itemType may not be DraftItemType::Undefined")}; +} + +std::shared_ptr<DraftItem> DraftItemTool::createDraftItem(QPointF pos, bool selectItem, bool setDefaultProperties) +{ + // If there are no layers, create a default one and activate it + if (!_imageEditor->controller().activeLayer()) + { + auto layer = _imageEditor->controller().createLayer("Default layer"); + _imageEditor->controller().switchLayer(layer.get()); + } + + // Create the draft item + auto draftItem = _imageEditor->controller().createDraftItem(_itemType); + + // Update the position and color properties before copying all properties to the draft item + position()->setValue(MathUtils::round(pos)); + primaryColor()->setValue(_imageEditor->environment().getPrimaryColor()); + + // Apply all property values to the new item + draftItem->properties().copyValues(_properties); + + if (setDefaultProperties) + draftItem->setDefaultPropertyValues(); + + if (selectItem) + selectDraftItem(draftItem); + + return draftItem; +} + +void DraftItemTool::selectDraftItem(const std::shared_ptr<DraftItem>& draftItem) const +{ + // Automatically switch to the default editor tool in order to edit the draft item's properties + _imageEditor->editorTools().activateDefaultTool(); + + if (auto scene = _imageEditor->controller().activeScene()) + { + // Find the just-created draft item node and center on it + auto draftItemNode = scene->findDraftItemNode(draftItem.get()); + + if (draftItemNode) + { + scene->clearSelection(); + draftItemNode->setSelected(true); + draftItemNode->setFocus(); + } + } +} + +void DraftItemTool::createProperties() +{ + ImageEditorTool::createProperties(); + + // Create standard properties + _primaryColor = createProperty<ColorProperty>(PropertyID::PrimaryColor, "Primary color", QColor{255, 255, 255}, PropertyBase::Flag::Hidden); + primaryColor()->setDescription("The primary color to use."); + + _position = createProperty<PointProperty>(PropertyID::Position, "Position", QPoint{0, 0}, PropertyBase::Flag::Hidden); + position()->setDescription("The item position."); + + _hasDirection = createProperty<BoolProperty>(PropertyID::HasDirection, "Has direction", false); + hasDirection()->setDescription("Specifies whether the box has a direction/orientation."); + + _direction = createProperty<AngleProperty>(PropertyID::Direction, "Direction", 0.0); + direction()->createConstraint<RangeConstraint>(0, 360); + direction()->setDescription("The main direction/orientation of the box contents."); +} + +ImageEditorTool::InputEventResult DraftItemTool::mousePressed(const QGraphicsSceneMouseEvent* event) +{ + // Remember the initial position for later use + _dragInfo.dragInitiated = true; + _dragInfo.mousePressPos = event->scenePos(); + + return InputEventResult::Process; +} + +ImageEditorTool::InputEventResult DraftItemTool::mouseMoved(const QGraphicsSceneMouseEvent* event) +{ + if (_dragInfo.dragInitiated) + { + // If the mouse has been moved far enough from the initial position, initiate a drag + if ((event->scenePos() - _dragInfo.mousePressPos).manhattanLength() >= QApplication::startDragDistance()) + { + startDraftItemDrag(); + _dragInfo.dragInitiated = false; + } + } + + // If a drag info item has been created by dragging, update it according to the current mouse position + if (_dragInfo.draftItem) + _dragInfo.draftItem->setDragPropertyValues(MathUtils::round(_dragInfo.mousePressPos), MathUtils::round(event->scenePos())); + + return InputEventResult::Process; +} + +ImageEditorTool::InputEventResult DraftItemTool::mouseReleased(const QGraphicsSceneMouseEvent* event) +{ + if (_dragInfo.draftItem) // Normalize the property values of the click-drag created draft item when finished dragging + { + _dragInfo.draftItem->normalizePropertyValues(); + selectDraftItem(_dragInfo.draftItem); + } + else // If no dragging was initiated, create a draft item with default values + { + if (!_dragInfo.dragCancelled) + createDraftItem(event->scenePos(), !event->modifiers().testFlag(Qt::ControlModifier), true); + } + + _dragInfo.dragInitiated = false; + _dragInfo.draftItem = nullptr; + + return InputEventResult::Process; +} + +VisualSceneInputHandler::InputEventResult DraftItemTool::keyPressed(const QKeyEvent* event) +{ + // When pressing Esc, cancel any click-drag draft item creation + if (event->key() == Qt::Key_Escape && _dragInfo.draftItem) + { + cancelDraftItemDrag(); + return VisualSceneInputHandler::InputEventResult::Process; + } + else + return VisualSceneInputHandler::InputEventResult::Ignore; +} + +void DraftItemTool::startDraftItemDrag() +{ + // The drag has just been initiated, so create a new draft item + _dragInfo.dragCancelled = false; + _dragInfo.draftItem = createDraftItem(_dragInfo.mousePressPos, false, false); +} + +void DraftItemTool::cancelDraftItemDrag() +{ + _dragInfo.dragCancelled = true; + + if (_dragInfo.draftItem) + { + _imageEditor->controller().removeDraftItem(_dragInfo.draftItem.get()); + _dragInfo.draftItem = nullptr; + } +} diff --git a/Grinder/ui/image/tools/DraftItemTool.h b/Grinder/ui/image/tools/DraftItemTool.h new file mode 100644 index 0000000000000000000000000000000000000000..ee67d047db73543b56f8fe33b37315d0841c70c4 --- /dev/null +++ b/Grinder/ui/image/tools/DraftItemTool.h @@ -0,0 +1,72 @@ +/****************************************************************************** + * File: DraftItemTool.h + * Date: 22.3.2018 + *****************************************************************************/ + +#ifndef DRAFTITEMTOOL_H +#define DRAFTITEMTOOL_H + +#include "ui/image/ImageEditorTool.h" +#include "image/DraftItemType.h" + +namespace grndr +{ + class DraftItem; + + class DraftItemTool : public ImageEditorTool + { + Q_OBJECT + + public: + DraftItemTool(ImageEditor* imageEditor, DraftItemType itemType, QString name, QString icon, QString shortcut, QCursor cursor = QCursor{Qt::ArrowCursor}); + + public: + DraftItemType getItemType() const { return _itemType; } + + public: + auto primaryColor() { return dynamic_cast<ColorProperty*>(_primaryColor.get()); } + auto primaryColor() const { return dynamic_cast<ColorProperty*>(_primaryColor.get()); } + auto position() { return dynamic_cast<PointProperty*>(_position.get()); } + auto position() const { return dynamic_cast<PointProperty*>(_position.get()); } + auto hasDirection() { return dynamic_cast<BoolProperty*>(_hasDirection.get()); } + auto hasDirection() const { return dynamic_cast<BoolProperty*>(_hasDirection.get()); } + auto direction() { return dynamic_cast<AngleProperty*>(_direction.get()); } + auto direction() const { return dynamic_cast<AngleProperty*>(_direction.get()); } + + protected: + std::shared_ptr<DraftItem> createDraftItem(QPointF pos, bool selectItem, bool setDefaultProperties); + void selectDraftItem(const std::shared_ptr<DraftItem>& draftItem) const; + + protected: + virtual void createProperties() override; + + virtual InputEventResult mousePressed(const QGraphicsSceneMouseEvent* event) override; + virtual InputEventResult mouseMoved(const QGraphicsSceneMouseEvent* event) override; + virtual InputEventResult mouseReleased(const QGraphicsSceneMouseEvent* event) override; + + virtual InputEventResult keyPressed(const QKeyEvent* event) override; + + private: + void startDraftItemDrag(); + void cancelDraftItemDrag(); + + protected: + DraftItemType _itemType{DraftItemType::Undefined}; + + std::shared_ptr<PropertyBase> _primaryColor; + std::shared_ptr<PropertyBase> _position; + std::shared_ptr<PropertyBase> _hasDirection; + std::shared_ptr<PropertyBase> _direction; + + private: + struct + { + bool dragInitiated{false}; + bool dragCancelled{false}; + QPointF mousePressPos; + std::shared_ptr<DraftItem> draftItem; + } _dragInfo; + }; +} + +#endif diff --git a/Grinder/ui/image/tools/LineDraftItemTool.cpp b/Grinder/ui/image/tools/LineDraftItemTool.cpp new file mode 100644 index 0000000000000000000000000000000000000000..e90b14fd58d456a92d04a8bb3dba146a4f8f4bb8 --- /dev/null +++ b/Grinder/ui/image/tools/LineDraftItemTool.cpp @@ -0,0 +1,26 @@ +/****************************************************************************** + * File: LineDraftItemTool.cpp + * Date: 22.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "LineDraftItemTool.h" +#include "common/properties/RangeConstraint.h" +#include "res/Resources.h" + +const char* LineDraftItemTool::tool_type = "LineDraftItemTool"; + +LineDraftItemTool::LineDraftItemTool(ImageEditor* imageEditor) : DraftItemTool(imageEditor, DraftItemType::Line, "Line", FILE_ICON_EDITOR_LINE, "L", QCursor{QPixmap{FILE_CURSOR_EDITOR_LINE}, 0, 0}) +{ + +} + +void LineDraftItemTool::createProperties() +{ + DraftItemTool::createProperties(); + + // Create specific properties + _lineWidth = createProperty<UIntProperty>(PropertyID::LineWidth, "Line width", 3); + lineWidth()->createConstraint<RangeConstraint>(1, 100); + lineWidth()->setDescription("The width of the line."); +} diff --git a/Grinder/ui/image/tools/LineDraftItemTool.h b/Grinder/ui/image/tools/LineDraftItemTool.h new file mode 100644 index 0000000000000000000000000000000000000000..e0daaa51cc7411b6f2ebe21f78541ea9dbb49768 --- /dev/null +++ b/Grinder/ui/image/tools/LineDraftItemTool.h @@ -0,0 +1,36 @@ +/****************************************************************************** + * File: LineDraftItemTool.h + * Date: 22.3.2018 + *****************************************************************************/ + +#ifndef LINEDRAFTITEMTOOL_H +#define LINEDRAFTITEMTOOL_H + +#include "DraftItemTool.h" + +namespace grndr +{ + class LineDraftItemTool : public DraftItemTool + { + public: + static const char* tool_type; + + public: + LineDraftItemTool(ImageEditor* imageEditor); + + public: + virtual QString getToolType() const override { return tool_type; } + + public: + auto lineWidth() { return dynamic_cast<UIntProperty*>(_lineWidth.get()); } + auto lineWidth() const { return dynamic_cast<UIntProperty*>(_lineWidth.get()); } + + protected: + virtual void createProperties() override; + + private: + std::shared_ptr<PropertyBase> _lineWidth; + }; +} + +#endif diff --git a/Grinder/ui/mainwnd/GrinderWindow.cpp b/Grinder/ui/mainwnd/GrinderWindow.cpp new file mode 100644 index 0000000000000000000000000000000000000000..53cd011d6ce81260b65cfd03b2e3fe036023aef7 --- /dev/null +++ b/Grinder/ui/mainwnd/GrinderWindow.cpp @@ -0,0 +1,372 @@ +/****************************************************************************** + * File: GrinderWindow.cpp + * Date: 11.1.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "GrinderWindow.h" +#include "ui_GrinderWindow.h" +#include "Version.h" +#include "common/AdjacentRange.h" +#include "ui/StyleSheet.h" +#include "ui/graph/GraphView.h" +#include "ui/dlg/OptionsDialog.h" +#include "core/GrinderApplication.h" +#include "pipeline/Pipeline.h" +#include "project/ProjectExceptions.h" +#include "util/UIUtils.h" +#include "res/Resources.h" + +#include <QMetaType> +Q_DECLARE_METATYPE(std::shared_ptr<Block>) + +GrinderWindow::GrinderWindow(QWidget* parent) : QMainWindow(parent), + ui{new Ui::GrinderWindow()} +{ + // Needed to be able to queue blockRemoved signals + qRegisterMetaType<std::shared_ptr<Block>>(); + + setupUi(); + + // Connect signals to update our UI + connect(&grinder()->pipelineController(), &PipelineController::pipelineSwitched, this, &GrinderWindow::pipelineSwitched); + connect(&grinder()->projectController(), &ProjectController::currentProjectFileChanged, this, &GrinderWindow::currentProjectFileChanged); + connect(&grinder()->projectController(), SIGNAL(projectLoaded(QString)), this, SLOT(updateRecentProjects(QString))); + connect(&grinder()->projectController(), SIGNAL(projectSaved(QString)), this, SLOT(updateRecentProjects(QString))); + connect(&grinder()->project(), SIGNAL(imageReferenceCreated(const std::shared_ptr<ImageReference>&)), this, SLOT(updateActions())); + connect(&grinder()->project(), SIGNAL(imageReferenceRemoved(const std::shared_ptr<ImageReference>&)), this, SLOT(updateActions())); + + // Connect image editor manager signals to show/hide the image editors + connect(&grinder()->imageEditorManager(), &ImageEditorManager::editorShown, this, &GrinderWindow::imageEditorShown); + connect(&grinder()->imageEditorManager(), &ImageEditorManager::editorClosed, this, &GrinderWindow::imageEditorClosed); + + updateActions(); + + // If the corresponding option is enabled, load the most recent project; otherwise, create a new project + bool createNewProject = true; + + if (grinder()->settings().startupOptions().loadLastProject) + createNewProject = !loadProjectOnStartup(); + + if (createNewProject) + on_actNewProject_triggered(); +} + +GrinderWindow::~GrinderWindow() +{ + delete ui; +} + +void GrinderWindow::closeEvent(QCloseEvent* event) +{ + if (!handleUnsavedProjectChanges()) + { + event->ignore(); + return; + } + + grinder()->settings().setWindowState("MainWindow", this); + + QMainWindow::closeEvent(event); +} + +void GrinderWindow::on_actNewProject_triggered() +{ + if (!handleUnsavedProjectChanges()) + return; + + grinder()->projectController().clearProject(true); + ui->statusBar->showMessage("Created a new project"); +} + +void GrinderWindow::on_actLoadProject_triggered() +{ + if (!handleUnsavedProjectChanges()) + return; + + QString fileName = UIUtils::askFileName(false, "ProjectFile", this, "Load project", "Grinder projects (*.grndr);;All files (*.*)"); + + if (!fileName.isEmpty()) + loadProject(fileName); +} + +void GrinderWindow::on_actSaveProject_triggered() +{ + QString fileName = grinder()->projectController().getCurrentProjectFile(); + + if (!fileName.isEmpty()) + saveProject(fileName); + else + on_actSaveProjectAs_triggered(); +} + +void GrinderWindow::on_actSaveProjectAs_triggered() +{ + QString fileName = UIUtils::askFileName(true, "ProjectFile", this, "Save project as...", "Grinder projects (*.grndr)"); + + if (!fileName.isEmpty()) + saveProject(fileName); +} + +void GrinderWindow::on_actExecuteLabel_triggered() +{ + if (const auto activeLabel = grinder()->projectController().activeLabel()) + grinder()->engineController().executeLabel(activeLabel, Engine::ExecutionMode::View); +} + +void GrinderWindow::on_actExecuteNextImage_triggered() +{ + ui->imageReferencesList->nextImageReference(); + on_actExecuteLabel_triggered(); +} + +void GrinderWindow::on_actExecutePreviousImage_triggered() +{ + ui->imageReferencesList->previousImageReference(); + on_actExecuteLabel_triggered(); +} + +void GrinderWindow::on_actOptions_triggered() +{ + OptionsDialog dlg; + dlg.exec(); +} + +void GrinderWindow::on_actAbout_triggered() +{ + auto title = QString{"About %1..."}.arg(GRNDR_INFO_TITLE); + auto text = QString{"<b>%1 %2</b><br>%3<br><br><a href='%4'>%4</a>"}.arg(GRNDR_INFO_TITLE).arg(GetVersionString(true)).arg(GRNDR_INFO_COPYRIGHT).arg(GRNDR_INFO_WEBSITE); + + QMessageBox::about(this, title, text); +} + +void GrinderWindow::pipelineSwitched(Pipeline* pipeline) +{ + static std::vector<QMetaObject::Connection> pipelineSignals; + + // Remove old pipeline signals and connect new ones so that the actions can be updated when necessary + for (auto& signal : pipelineSignals) + disconnect(signal); + + pipelineSignals.clear(); + + if (pipeline) + { + pipelineSignals.push_back(connect(pipeline, &Pipeline::blockCreated, this, &GrinderWindow::updateActions)); + pipelineSignals.push_back(connect(pipeline, &Pipeline::blockRemoved, this, &GrinderWindow::updateActions, Qt::QueuedConnection)); // Delay this signal so that the block has been removed from the blocks list + } + + updateActions(); +} + +void GrinderWindow::currentProjectFileChanged(QString fileName) +{ + grinder()->settings().startupOptions().lastProjectFile = fileName; + grinder()->settings().saveSettings(false); + + updateCaption(); +} + +void GrinderWindow::imageEditorShown(ImageEditor* editor) +{ + // Add the editor to the dock area if it hasn't been added yet + if (!findChildren<QDockWidget*>().contains(editor->dockWidget())) + addDockWidget(Qt::BottomDockWidgetArea, editor->dockWidget()); + + if (grinder()->settings().imageEditorOptions().startUndocked) + { + // Show the editor in a floating window + editor->dockWidget()->setFloating(true); + } + else + { + // Group all bottom image editor docks into tabs + if (grinder()->settings().imageEditorOptions().groupIntoTabs) + tabifyImageEditorDocks(); + } +} + +void GrinderWindow::imageEditorClosed(ImageEditor* editor) +{ + // Remove the editor from the dock area + removeDockWidget(editor->dockWidget()); +} + +void GrinderWindow::updateCaption() +{ + QString caption = QString{"%1 %2"}.arg(GRNDR_INFO_TITLE).arg(GetVersionString()); + + if (!grinder()->projectController().getCurrentProjectFile().isEmpty()) + caption += QString{" - %1"}.arg(grinder()->projectController().getCurrentProjectFile()); + else + caption += " - New project"; + + setWindowTitle(caption); +} + +void GrinderWindow::setupUi() +{ + ui->setupUi(this); + + // Assign the UI components to the controllers + grinder()->pipelineController().assignUiComponents(ui->graphWidget->graphView()); + grinder()->projectController().assignUiComponents(ui->labelsList, ui->imageReferencesList); + + // Setup the various UI components + ui->graphWidget->graphView()->setupUi(ui->menu_Graph, ui->graphToolBar); + ui->labelsList->setupUi(ui->menu_Labels, ui->labelsControlBar); + ui->imageReferencesList->setupUi(ui->menu_Images, ui->imageReferencesControlBar); + ui->menu_Recent_Projects->setupUi(&grinder()->settings().recentProjects(), [this](QString fileName) { loadProject(fileName); }); + ui->propertiesTree->setupUi(ui->lblPropertyDesc); + + ui->frmPropertyDesc->setStyleSheet(StyleSheet::loadStyleSheet(FILE_STYLESHEET_PROPERTYDESC)); + + grinder()->settings().getWindowState("MainWindow", this); + + updateCaption(); +} + +void GrinderWindow::updateActions() +{ + auto activePipeline = grinder()->pipelineController().activePipeline(); + bool enableExecute = activePipeline && !activePipeline->blocks().empty(); + + ui->actExecuteLabel->setEnabled(enableExecute); + ui->actExecuteNextImage->setEnabled(enableExecute && ui->imageReferencesList->count() > 1); + ui->actExecutePreviousImage->setEnabled(enableExecute && ui->imageReferencesList->count() > 1); +} + +bool GrinderWindow::loadProject(QString fileName) +{ + bool result = true; + + setEnabled(false); + + try { + grinder()->projectController().loadProject(fileName); + ui->statusBar->showMessage(QString{"Loaded project '%1'"}.arg(fileName)); + } catch (ProjectException& e) { + ui->statusBar->showMessage(GetExceptionMessage(e.what())); + updateRecentProjects(fileName, false); + + result = false; + } + + setEnabled(true); + + return result; +} + +bool GrinderWindow::saveProject(QString fileName) +{ + bool result = true; + + setEnabled(false); + + try { + grinder()->projectController().saveProject(fileName); + ui->statusBar->showMessage(QString{"Saved the current project to '%1'"}.arg(fileName)); + } catch (ProjectException& e) { + ui->statusBar->showMessage(GetExceptionMessage(e.what())); + + result = false; + } + + setEnabled(true); + + return result; +} + +bool GrinderWindow::loadProjectOnStartup() +{ + QString lastProject = grinder()->settings().startupOptions().lastProjectFile; + + if (!lastProject.isEmpty() && QFile::exists(lastProject)) + return loadProject(lastProject); + else + return false; +} + +bool GrinderWindow::handleUnsavedProjectChanges() +{ + if (grinder()->settings().generalOptions().askOnUnsavedChanges) + { + // Save the current graph layout so that any changes will appear in the isProjectDirty call + ui->graphWidget->graphView()->updateCurrentGraphLayout(); + + if (grinder()->projectController().isProjectDirty()) + { + auto answer = QMessageBox::question(this, "Unsaved project changes", "There are unsaved project changes. Do you want to save now?", QMessageBox::Yes|QMessageBox::No|QMessageBox::Cancel, QMessageBox::Yes); + + if (answer == QMessageBox::Yes) + on_actSaveProject_triggered(); + else if (answer == QMessageBox::Cancel) + return false; + } + } + + return true; +} + +void GrinderWindow::updateRecentProjects(QString fileName, bool addEntry) +{ + if (addEntry) + grinder()->settings().recentProjects() += fileName; + else + grinder()->settings().recentProjects() -= fileName; + + ui->menu_Recent_Projects->updateUi(); +} + +void GrinderWindow::tabifyImageEditorDocks() +{ + // Gather all docks of the left, right, top and bottom areas + std::vector<QDockWidget*> leftArea, rightArea, topArea, bottomArea; + std::vector<QDockWidget*> activeDocks; + + for (auto dock : findChildren<QDockWidget*>()) + { + if (dynamic_cast<ImageEditorDockWidget*>(dock) && !dock->isFloating()) + { + switch (dockWidgetArea(dock)) + { + case Qt::LeftDockWidgetArea: + leftArea.push_back(dock); + break; + + case Qt::RightDockWidgetArea: + rightArea.push_back(dock); + break; + + case Qt::TopDockWidgetArea: + topArea.push_back(dock); + break; + + case Qt::BottomDockWidgetArea: + bottomArea.push_back(dock); + break; + + default: + break; + } + + if (!dock->visibleRegion().isEmpty()) + activeDocks.push_back(dock); + } + } + + std::vector<std::vector<QDockWidget*>*> dockAreas{&leftArea, &rightArea, &topArea, &bottomArea}; + + // Sort all docks of each area and tabify them + for (auto& dockArea : dockAreas) + { + std::sort(dockArea->begin(), dockArea->end(), [](auto dock1, auto dock2) { return dock1->windowTitle() < dock2->windowTitle(); }); + + for (const auto& dockPair : make_adjacent_range(*dockArea)) + tabifyDockWidget(dockPair.first, dockPair.second); + } + + // Reactive all previously active tabs + for (auto& dock : activeDocks) + dock->raise(); +} diff --git a/Grinder/ui/mainwnd/GrinderWindow.h b/Grinder/ui/mainwnd/GrinderWindow.h new file mode 100644 index 0000000000000000000000000000000000000000..782c01c71c919cf10a418ba3a3e8615265c8acab --- /dev/null +++ b/Grinder/ui/mainwnd/GrinderWindow.h @@ -0,0 +1,74 @@ +/****************************************************************************** + * File: GrinderWindow.h + * Date: 11.1.2018 + *****************************************************************************/ + +#ifndef GRINDERWINDOW_H +#define GRINDERWINDOW_H + +#include <QMainWindow> +#include <memory> + +#include "ui/image/ImageEditorManager.h" +#include "ui/graph/GraphWidget.h" + +namespace Ui +{ + class GrinderWindow; +} + +namespace grndr +{ + class Pipeline; + + class GrinderWindow : public QMainWindow + { + Q_OBJECT + + public: + GrinderWindow(QWidget* parent = nullptr); + ~GrinderWindow(); + + protected: + virtual void closeEvent(QCloseEvent* event) override; + + private slots: + void on_actNewProject_triggered(); + void on_actLoadProject_triggered(); + void on_actSaveProject_triggered(); + void on_actSaveProjectAs_triggered(); + + void on_actExecuteLabel_triggered(); + void on_actExecuteNextImage_triggered(); + void on_actExecutePreviousImage_triggered(); + + void on_actOptions_triggered(); + void on_actAbout_triggered(); + + private slots: + void pipelineSwitched(Pipeline* pipeline); + void currentProjectFileChanged(QString fileName); + + void imageEditorShown(ImageEditor* editor); + void imageEditorClosed(ImageEditor* editor); + + void updateCaption(); + void updateActions(); + void updateRecentProjects(QString fileName) { updateRecentProjects(fileName, true); } + + private: + bool loadProject(QString fileName); + bool saveProject(QString fileName); + bool loadProjectOnStartup(); + bool handleUnsavedProjectChanges(); + void updateRecentProjects(QString fileName, bool addEntry); + + void tabifyImageEditorDocks(); + + private: + void setupUi(); + Ui::GrinderWindow* ui; + }; +} + +#endif diff --git a/Grinder/ui/mainwnd/GrinderWindow.ui b/Grinder/ui/mainwnd/GrinderWindow.ui new file mode 100644 index 0000000000000000000000000000000000000000..151dbb8e43e3703c3762ef44ff488b2ffae03c98 --- /dev/null +++ b/Grinder/ui/mainwnd/GrinderWindow.ui @@ -0,0 +1,682 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>GrinderWindow</class> + <widget class="QMainWindow" name="GrinderWindow"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>1200</width> + <height>800</height> + </rect> + </property> + <property name="minimumSize"> + <size> + <width>800</width> + <height>600</height> + </size> + </property> + <property name="windowTitle"> + <string>Grinder</string> + </property> + <property name="dockOptions"> + <set>QMainWindow::AllowNestedDocks|QMainWindow::AllowTabbedDocks|QMainWindow::AnimatedDocks|QMainWindow::GroupedDragging</set> + </property> + <widget class="QWidget" name="centralWidget"> + <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>0</number> + </property> + <property name="spacing"> + <number>0</number> + </property> + <item row="0" column="0"> + <widget class="GraphWidget" name="graphWidget"> + <property name="frameShape"> + <enum>QFrame::WinPanel</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Sunken</enum> + </property> + </widget> + </item> + </layout> + </widget> + <widget class="QMenuBar" name="menuBar"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>1200</width> + <height>21</height> + </rect> + </property> + <widget class="QMenu" name="menu_File"> + <property name="title"> + <string>&File</string> + </property> + <widget class="RecentProjectsMenu" name="menu_Recent_Projects"> + <property name="title"> + <string>&Recent projects</string> + </property> + <addaction name="separator"/> + </widget> + <addaction name="actNewProject"/> + <addaction name="actLoadProject"/> + <addaction name="menu_Recent_Projects"/> + <addaction name="separator"/> + <addaction name="actSaveProject"/> + <addaction name="actSaveProjectAs"/> + <addaction name="separator"/> + <addaction name="actExit"/> + </widget> + <widget class="QMenu" name="menu_Help"> + <property name="title"> + <string>&Help</string> + </property> + <addaction name="actAbout"/> + </widget> + <widget class="QMenu" name="menu_Graph"> + <property name="title"> + <string>&Graph</string> + </property> + </widget> + <widget class="QMenu" name="menu_Labels"> + <property name="title"> + <string>&Labels</string> + </property> + </widget> + <widget class="QMenu" name="menu_Images"> + <property name="title"> + <string>&Images</string> + </property> + </widget> + <widget class="QMenu" name="menu_Run"> + <property name="title"> + <string>&Run</string> + </property> + <addaction name="actExecuteLabel"/> + <addaction name="separator"/> + <addaction name="actExecuteNextImage"/> + <addaction name="actExecutePreviousImage"/> + </widget> + <widget class="QMenu" name="menu_Tools"> + <property name="title"> + <string>&Tools</string> + </property> + <addaction name="actOptions"/> + </widget> + <addaction name="menu_File"/> + <addaction name="menu_Run"/> + <addaction name="menu_Labels"/> + <addaction name="menu_Images"/> + <addaction name="menu_Graph"/> + <addaction name="menu_Tools"/> + <addaction name="menu_Help"/> + </widget> + <widget class="QToolBar" name="mainToolBar"> + <property name="contextMenuPolicy"> + <enum>Qt::PreventContextMenu</enum> + </property> + <property name="windowTitle"> + <string>Project</string> + </property> + <property name="allowedAreas"> + <set>Qt::BottomToolBarArea|Qt::TopToolBarArea</set> + </property> + <property name="iconSize"> + <size> + <width>24</width> + <height>24</height> + </size> + </property> + <property name="floatable"> + <bool>false</bool> + </property> + <attribute name="toolBarArea"> + <enum>TopToolBarArea</enum> + </attribute> + <attribute name="toolBarBreak"> + <bool>false</bool> + </attribute> + <addaction name="actNewProject"/> + <addaction name="actLoadProject"/> + <addaction name="separator"/> + <addaction name="actSaveProject"/> + <addaction name="separator"/> + <addaction name="actExecuteLabel"/> + <addaction name="separator"/> + <addaction name="actExecutePreviousImage"/> + <addaction name="actExecuteNextImage"/> + <addaction name="separator"/> + <addaction name="actExit"/> + </widget> + <widget class="QStatusBar" name="statusBar"/> + <widget class="QToolBar" name="graphToolBar"> + <property name="contextMenuPolicy"> + <enum>Qt::PreventContextMenu</enum> + </property> + <property name="windowTitle"> + <string>Graph view</string> + </property> + <property name="allowedAreas"> + <set>Qt::BottomToolBarArea|Qt::TopToolBarArea</set> + </property> + <property name="floatable"> + <bool>false</bool> + </property> + <attribute name="toolBarArea"> + <enum>TopToolBarArea</enum> + </attribute> + <attribute name="toolBarBreak"> + <bool>false</bool> + </attribute> + </widget> + <widget class="GrinderDockWidget" name="labelsDock"> + <property name="font"> + <font> + <weight>75</weight> + <bold>true</bold> + </font> + </property> + <property name="features"> + <set>QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovable</set> + </property> + <property name="allowedAreas"> + <set>Qt::LeftDockWidgetArea|Qt::RightDockWidgetArea</set> + </property> + <property name="windowTitle"> + <string>Labels</string> + </property> + <attribute name="dockWidgetArea"> + <number>1</number> + </attribute> + <widget class="QWidget" name="dockWidgetContents_2"> + <layout class="QVBoxLayout" name="verticalLayout"> + <property name="spacing"> + <number>2</number> + </property> + <property name="leftMargin"> + <number>6</number> + </property> + <property name="topMargin"> + <number>6</number> + </property> + <property name="rightMargin"> + <number>6</number> + </property> + <property name="bottomMargin"> + <number>6</number> + </property> + <item> + <widget class="LabelsListWidget" name="labelsList"> + <property name="editTriggers"> + <set>QAbstractItemView::EditKeyPressed</set> + </property> + <property name="sortingEnabled"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="ControlBar" name="labelsControlBar"> + <property name="minimumSize"> + <size> + <width>0</width> + <height>20</height> + </size> + </property> + <property name="frameShape"> + <enum>QFrame::StyledPanel</enum> + </property> + </widget> + </item> + </layout> + </widget> + </widget> + <widget class="GrinderDockWidget" name="imagesDock"> + <property name="font"> + <font> + <weight>75</weight> + <bold>true</bold> + </font> + </property> + <property name="features"> + <set>QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovable</set> + </property> + <property name="windowTitle"> + <string>Images</string> + </property> + <attribute name="dockWidgetArea"> + <number>1</number> + </attribute> + <widget class="QWidget" name="dockWidgetContents"> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <property name="spacing"> + <number>2</number> + </property> + <property name="leftMargin"> + <number>6</number> + </property> + <property name="topMargin"> + <number>6</number> + </property> + <property name="rightMargin"> + <number>6</number> + </property> + <property name="bottomMargin"> + <number>6</number> + </property> + <item> + <widget class="ImageReferencesListWidget" name="imageReferencesList"> + <property name="acceptDrops"> + <bool>true</bool> + </property> + <property name="editTriggers"> + <set>QAbstractItemView::NoEditTriggers</set> + </property> + <property name="sortingEnabled"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="ControlBar" name="imageReferencesControlBar"> + <property name="minimumSize"> + <size> + <width>0</width> + <height>20</height> + </size> + </property> + <property name="frameShape"> + <enum>QFrame::StyledPanel</enum> + </property> + </widget> + </item> + </layout> + </widget> + </widget> + <widget class="GrinderDockWidget" name="propertiesDock"> + <property name="font"> + <font> + <weight>75</weight> + <bold>true</bold> + </font> + </property> + <property name="features"> + <set>QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovable</set> + </property> + <property name="windowTitle"> + <string>Properties</string> + </property> + <attribute name="dockWidgetArea"> + <number>2</number> + </attribute> + <widget class="QWidget" name="dockWidgetContents_3"> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <property name="spacing"> + <number>2</number> + </property> + <property name="leftMargin"> + <number>6</number> + </property> + <property name="topMargin"> + <number>6</number> + </property> + <property name="rightMargin"> + <number>6</number> + </property> + <property name="bottomMargin"> + <number>6</number> + </property> + <item> + <widget class="PropertyTreeWidget" name="propertiesTree"> + <property name="editTriggers"> + <set>QAbstractItemView::NoEditTriggers</set> + </property> + <property name="uniformRowHeights"> + <bool>true</bool> + </property> + <property name="sortingEnabled"> + <bool>true</bool> + </property> + <property name="columnCount"> + <number>2</number> + </property> + <attribute name="headerVisible"> + <bool>false</bool> + </attribute> + <attribute name="headerHighlightSections"> + <bool>true</bool> + </attribute> + <attribute name="headerMinimumSectionSize"> + <number>35</number> + </attribute> + <column> + <property name="text"> + <string>Property</string> + </property> + <property name="font"> + <font> + <weight>75</weight> + <bold>true</bold> + </font> + </property> + </column> + <column> + <property name="text"> + <string>Value</string> + </property> + <property name="font"> + <font> + <weight>75</weight> + <bold>true</bold> + </font> + </property> + </column> + </widget> + </item> + <item> + <widget class="QFrame" name="frmPropertyDesc"> + <property name="frameShape"> + <enum>QFrame::StyledPanel</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Plain</enum> + </property> + <layout class="QVBoxLayout" name="verticalLayout_4"> + <property name="spacing"> + <number>5</number> + </property> + <property name="leftMargin"> + <number>6</number> + </property> + <property name="topMargin"> + <number>6</number> + </property> + <property name="rightMargin"> + <number>6</number> + </property> + <property name="bottomMargin"> + <number>6</number> + </property> + <item> + <widget class="QLabel" name="lblPropertyDesc"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Minimum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>0</width> + <height>80</height> + </size> + </property> + <property name="font"> + <font> + <weight>50</weight> + <bold>false</bold> + </font> + </property> + <property name="text"> + <string>PropertyDescription</string> + </property> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + </widget> + <action name="actExit"> + <property name="icon"> + <iconset resource="../../res/Grinder.qrc"> + <normaloff>:/icons/icons/door-exit.png</normaloff>:/icons/icons/door-exit.png</iconset> + </property> + <property name="text"> + <string>E&xit</string> + </property> + <property name="toolTip"> + <string>Exit the application</string> + </property> + <property name="statusTip"> + <string>Exit the application</string> + </property> + <property name="shortcut"> + <string>Ctrl+Q</string> + </property> + </action> + <action name="actAbout"> + <property name="icon"> + <iconset resource="../../res/Grinder.qrc"> + <normaloff>:/icons/icons/question-mark-in-a-circle.png</normaloff>:/icons/icons/question-mark-in-a-circle.png</iconset> + </property> + <property name="text"> + <string>&About...</string> + </property> + <property name="toolTip"> + <string>About this application</string> + </property> + <property name="statusTip"> + <string>About this application</string> + </property> + </action> + <action name="actExecuteLabel"> + <property name="icon"> + <iconset resource="../../res/Grinder.qrc"> + <normaloff>:/icons/icons/start.png</normaloff>:/icons/icons/start.png</iconset> + </property> + <property name="text"> + <string>E&xecute current label</string> + </property> + <property name="toolTip"> + <string>Execute the currently active label</string> + </property> + <property name="statusTip"> + <string>Execute the currently active label</string> + </property> + <property name="shortcut"> + <string>F5</string> + </property> + </action> + <action name="actSaveProject"> + <property name="icon"> + <iconset resource="../../res/Grinder.qrc"> + <normaloff>:/icons/icons/floppy-disk-digital-data-storage-or-save-interface-symbol.png</normaloff>:/icons/icons/floppy-disk-digital-data-storage-or-save-interface-symbol.png</iconset> + </property> + <property name="text"> + <string>&Save project</string> + </property> + <property name="toolTip"> + <string>Save the current project</string> + </property> + <property name="statusTip"> + <string>Save the current project</string> + </property> + <property name="shortcut"> + <string>Ctrl+S</string> + </property> + </action> + <action name="actSaveProjectAs"> + <property name="text"> + <string>Save project &as...</string> + </property> + <property name="toolTip"> + <string>Save the project to a file</string> + </property> + <property name="statusTip"> + <string>Save the project to a file</string> + </property> + <property name="shortcut"> + <string>F12</string> + </property> + </action> + <action name="actLoadProject"> + <property name="icon"> + <iconset resource="../../res/Grinder.qrc"> + <normaloff>:/icons/icons/folder-with-information.png</normaloff>:/icons/icons/folder-with-information.png</iconset> + </property> + <property name="text"> + <string>&Load project...</string> + </property> + <property name="toolTip"> + <string>Load a saved project</string> + </property> + <property name="statusTip"> + <string>Load a saved project</string> + </property> + <property name="shortcut"> + <string>Ctrl+O</string> + </property> + </action> + <action name="actNewProject"> + <property name="icon"> + <iconset resource="../../res/Grinder.qrc"> + <normaloff>:/icons/icons/document-empty.png</normaloff>:/icons/icons/document-empty.png</iconset> + </property> + <property name="text"> + <string>&New project</string> + </property> + <property name="toolTip"> + <string>Create an empty project</string> + </property> + <property name="statusTip"> + <string>Create an empty project</string> + </property> + <property name="shortcut"> + <string>Ctrl+N</string> + </property> + </action> + <action name="actOptions"> + <property name="icon"> + <iconset resource="../../res/Grinder.qrc"> + <normaloff>:/icons/icons/open-window-with-gear-sign.png</normaloff>:/icons/icons/open-window-with-gear-sign.png</iconset> + </property> + <property name="text"> + <string>Options...</string> + </property> + <property name="toolTip"> + <string>Open the application options</string> + </property> + <property name="statusTip"> + <string>Open the application options</string> + </property> + </action> + <action name="actExecuteNextImage"> + <property name="icon"> + <iconset resource="../../res/Grinder.qrc"> + <normaloff>:/icons/icons/right-thin-arrowheads.png</normaloff>:/icons/icons/right-thin-arrowheads.png</iconset> + </property> + <property name="text"> + <string>Execute &next image</string> + </property> + <property name="toolTip"> + <string>Go to the next image and execute the currently active label</string> + </property> + <property name="statusTip"> + <string>Execute the currently active label</string> + </property> + <property name="shortcut"> + <string>Space</string> + </property> + </action> + <action name="actExecutePreviousImage"> + <property name="icon"> + <iconset resource="../../res/Grinder.qrc"> + <normaloff>:/icons/icons/arrowheads-of-thin-outline-to-the-left.png</normaloff>:/icons/icons/arrowheads-of-thin-outline-to-the-left.png</iconset> + </property> + <property name="text"> + <string>Execute &previous image</string> + </property> + <property name="toolTip"> + <string>Go to the previous image and execute the currently active label</string> + </property> + <property name="statusTip"> + <string>Execute the currently active label</string> + </property> + <property name="shortcut"> + <string>Ctrl+Space</string> + </property> + </action> + </widget> + <layoutdefault spacing="6" margin="11"/> + <customwidgets> + <customwidget> + <class>ControlBar</class> + <extends>QFrame</extends> + <header>ui/widget/ControlBar.h</header> + <container>1</container> + </customwidget> + <customwidget> + <class>GraphWidget</class> + <extends>QFrame</extends> + <header>ui/graph/GraphWidget.h</header> + <container>1</container> + </customwidget> + <customwidget> + <class>LabelsListWidget</class> + <extends>QListWidget</extends> + <header>ui/mainwnd/LabelsListWidget.h</header> + </customwidget> + <customwidget> + <class>ImageReferencesListWidget</class> + <extends>QListWidget</extends> + <header>ui/mainwnd/ImageReferencesListWidget.h</header> + </customwidget> + <customwidget> + <class>RecentProjectsMenu</class> + <extends>QMenu</extends> + <header>ui/mainwnd/RecentProjectsMenu.h</header> + </customwidget> + <customwidget> + <class>PropertyTreeWidget</class> + <extends>QTreeWidget</extends> + <header>ui/property/PropertyTreeWidget.h</header> + </customwidget> + <customwidget> + <class>GrinderDockWidget</class> + <extends>QDockWidget</extends> + <header>ui/widget/GrinderDockWidget.h</header> + <container>1</container> + </customwidget> + </customwidgets> + <resources> + <include location="../../res/Grinder.qrc"/> + </resources> + <connections> + <connection> + <sender>actExit</sender> + <signal>triggered()</signal> + <receiver>GrinderWindow</receiver> + <slot>close()</slot> + <hints> + <hint type="sourcelabel"> + <x>-1</x> + <y>-1</y> + </hint> + <hint type="destinationlabel"> + <x>199</x> + <y>149</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/Grinder/ui/mainwnd/ImageReferencesListItem.cpp b/Grinder/ui/mainwnd/ImageReferencesListItem.cpp new file mode 100644 index 0000000000000000000000000000000000000000..0c1fa6e12011b5d4a99291d002e836b4fb32df4f --- /dev/null +++ b/Grinder/ui/mainwnd/ImageReferencesListItem.cpp @@ -0,0 +1,55 @@ +/****************************************************************************** + * File: ImageReferencesListItem.cpp + * Date: 10.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ImageReferencesListItem.h" +#include "util/StringUtils.h" +#include "res/Resources.h" + +#include <opencv2/highgui.hpp> + +ImageReferencesListItem::ImageReferencesListItem(ImageReference* imageRef) : ObjectListItem(imageRef) +{ + setFlags(Qt::ItemIsEnabled|Qt::ItemIsSelectable); + setIcon(QIcon{FILE_ICON_IMAGEREFERENCE}); + + updateItem(); +} + +bool ImageReferencesListItem::operator <(const QListWidgetItem& other) const +{ + auto imageRef = dynamic_cast<const ImageReferencesListItem&>(other); + return *_object < *imageRef._object; +} + +void ImageReferencesListItem::viewImage() const +{ + // Show the image using OpenCV + if (_object->isValid()) + { + auto image = cv::imread(_object->getImageFilePath().toStdString()); + + if (!image.empty()) + cv::imshow(_object->getImageFileName().toStdString(), image); + } +} + +void ImageReferencesListItem::updateItem() +{ + // Create the tooltip string + auto imageInfo = _object->getImageInfo(); + QFileInfo fileInfo{_object->getImageFilePath()}; + QString toolTip = QString{"<b>%1</b><br><em>Location: </em>%6<br><em>File size: </em>%2<br><em>File date: </em>%3<br><em>Dimensions: </em>%4 x %5 pixels"} + .arg(_object->getImageFileName()) + .arg(StringUtils::fileSizeToString(imageInfo.fileSize)) + .arg(imageInfo.fileDate.toString(Qt::SystemLocaleShortDate)) + .arg(imageInfo.imageSize.width()).arg(imageInfo.imageSize.height()) + .arg(fileInfo.path()); + + setText(getName()); + setToolTip(toolTip); + + base_type::updateItem(); +} diff --git a/Grinder/ui/mainwnd/ImageReferencesListItem.h b/Grinder/ui/mainwnd/ImageReferencesListItem.h new file mode 100644 index 0000000000000000000000000000000000000000..18e9fb08f26269320e4440ba6fa59814792b30e1 --- /dev/null +++ b/Grinder/ui/mainwnd/ImageReferencesListItem.h @@ -0,0 +1,33 @@ +/****************************************************************************** + * File: ImageReferencesListItem.h + * Date: 10.2.2018 + *****************************************************************************/ + +#ifndef IMAGEREFERENCESLISTITEM_H +#define IMAGEREFERENCESLISTITEM_H + +#include "ui/widget/ObjectListItem.h" +#include "project/ImageReference.h" + +namespace grndr +{ + class ImageReference; + + class ImageReferencesListItem : public ObjectListItem<ImageReference> + { + public: + ImageReferencesListItem(ImageReference* imageRef); + + virtual bool operator <(const QListWidgetItem &other) const override; + + public: + void viewImage() const; + + virtual void updateItem() override; + + public: + QString getName() const { return _object->getImageFileName(); } + }; +} + +#endif diff --git a/Grinder/ui/mainwnd/ImageReferencesListWidget.cpp b/Grinder/ui/mainwnd/ImageReferencesListWidget.cpp new file mode 100644 index 0000000000000000000000000000000000000000..8359d9af6bd4539bf2c3fe4ea8947620e1471d62 --- /dev/null +++ b/Grinder/ui/mainwnd/ImageReferencesListWidget.cpp @@ -0,0 +1,216 @@ +/****************************************************************************** + * File: ImageReferencesListWidget.cpp + * Date: 10.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ImageReferencesListWidget.h" +#include "core/GrinderApplication.h" +#include "project/Project.h" +#include "ui/widget/ControlBar.h" +#include "util/UIUtils.h" +#include "util/ImageUtils.h" +#include "util/FileUtils.h" +#include "res/Resources.h" + +ImageReferencesListWidget::ImageReferencesListWidget(QWidget* parent) : MetaWidget(parent) +{ + setAcceptDrops(true); + + // Create labels actions + _switchImageReferenceAction = UIUtils::createAction(this, "&Activate", FILE_ICON_ACTIVATE, SLOT(switchImageReference()), "Activate the selected image", "Return"); + _nextImageReferenceAction = UIUtils::createAction(this, "Switch to &next image", "", SLOT(nextImageReference()), "Activate the next image", "Ctrl+PgDown", Qt::WindowShortcut); + _previousImageReferenceAction = UIUtils::createAction(this, "Switch to &previous image", "", SLOT(previousImageReference()), "Activate the previous image", "Ctrl+PgUp", Qt::WindowShortcut); + _viewImageReferenceAction = UIUtils::createAction(this, "&View image", FILE_ICON_VIEWIMAGE, SLOT(viewImageReference()), "View the selected image"); + _addImageReferencesAction = UIUtils::createAction(this, "Add &images", FILE_ICON_ADD, SLOT(addImageReferences()), "Add one or more images to the project", "Ctrl+I", Qt::WindowShortcut); + _removeImageReferenceAction = UIUtils::createAction(this, "&Remove image", FILE_ICON_DELETE, SLOT(removeImageReference()), "Remove the selected image", "Del"); + _removeAllImageReferencesAction = UIUtils::createAction(this, "Remove all images", "", SLOT(removeAllImageReferences()), "Remove all images"); + + // Listen for item selections in order to update the actions + connect(this, &ImageReferencesListWidget::itemSelectionChanged, this, &ImageReferencesListWidget::updateActions); + + // Listen for label switching in order to update the active state flags + connect(&grinder()->projectController(), &ProjectController::imageReferenceSwitched, this, &ImageReferencesListWidget::imageReferenceSwitched); + + updateActions(); +} + +std::unique_ptr<QMenu> ImageReferencesListWidget::createContextMenu() const +{ + auto menu = widget_type::createContextMenu(); + menu->setDefaultAction(_switchImageReferenceAction); + return menu; +} + +std::vector<QAction*> ImageReferencesListWidget::getActions(AddActionsMode mode) const +{ + std::vector<QAction*> actions; + + if (mode == AddActionsMode::ContextMenu || mode == AddActionsMode::Toolbar) + { + actions.push_back(_switchImageReferenceAction); + + if (mode == AddActionsMode::ContextMenu) + actions.push_back(_viewImageReferenceAction); + + actions.push_back(nullptr); + } + + if (mode == AddActionsMode::MainMenu || mode == AddActionsMode::ContextMenu) + { + actions.push_back(_nextImageReferenceAction); + actions.push_back(_previousImageReferenceAction); + actions.push_back(nullptr); + } + + actions.push_back(_addImageReferencesAction); + actions.push_back(_removeImageReferenceAction); + + if (mode == AddActionsMode::ContextMenu) + { + actions.push_back(nullptr); + actions.push_back(_removeAllImageReferencesAction); + } + + return actions; +} + +void ImageReferencesListWidget::switchToObjectItem(ImageReferencesListItem* item, bool selectItem) +{ + if (item) + { + grinder()->projectController().switchImageReference(item->object()); + + if (selectItem) + setCurrentItem(item); + } +} + +void ImageReferencesListWidget::dragEnterEvent(QDragEnterEvent* event) +{ + if (event->mimeData()->hasUrls()) + { + event->setDropAction(Qt::CopyAction); + event->accept(); + } + else + event->ignore(); +} + +void ImageReferencesListWidget::dragMoveEvent(QDragMoveEvent* event) +{ + if (event->mimeData()->hasUrls()) + { + event->setDropAction(Qt::CopyAction); + event->accept(); + } + else + event->ignore(); +} + +void ImageReferencesListWidget::dropEvent(QDropEvent* event) +{ + if (event->mimeData()->hasUrls()) + { + auto urls = event->mimeData()->urls(); + QStringList files; + + for (auto url : urls) + files << url.toLocalFile(); + + addImageReferences(files); + + event->setDropAction(Qt::CopyAction); + event->accept(); + } + else + event->ignore(); +} + +void ImageReferencesListWidget::viewImageReference() +{ + if (auto item = currentObjectItem()) + item->viewImage(); +} + +void ImageReferencesListWidget::addImageReferences() +{ + auto files = UIUtils::askFileNames("ImageReferences", this, "Add images", QString{"Images (%1);;All files (*.*)"}.arg(ImageUtils::getSupportedFormatFilters().join(" "))); + addImageReferences(files); +} + +void ImageReferencesListWidget::addImageReferences(QStringList files) +{ + // Expand the file list, recursively finding all image files, and create image references for them + files = FileUtils::expandFileList(files, ImageUtils::getSupportedFormatFilters()); + auto imageRefs = grinder()->projectController().createImageReferences(files); + + // If no active image exists, switch to the last added one + if (!grinder()->projectController().activeImageReference()) + { + if (!imageRefs.empty()) + { + auto item = findObjectItem(imageRefs[imageRefs.size() - 1].get()); + + if (item) + switchToObjectItem(item); + } + } +} + +void ImageReferencesListWidget::removeImageReference() +{ + if (auto imageRef = currentObject()) + { + grinder()->projectController().removeImageReference(imageRef); + + // Switch to the image reference that is currently selected if no active one exists + if (!grinder()->projectController().activeImageReference()) + switchToObjectItem(currentObjectItem()); + } +} + +void ImageReferencesListWidget::removeAllImageReferences() +{ + grinder()->projectController().removeAllImageReferences(); + updateActions(); +} + +void ImageReferencesListWidget::imageReferenceSwitched(ImageReference* imageRef) +{ + objectSwitched(imageRef); + updateActions(); +} + +void ImageReferencesListWidget::updateActions() +{ + bool imageSelected = (currentObjectItem() != nullptr); + + _switchImageReferenceAction->setEnabled(imageSelected && !currentObjectItem()->isActive()); + _nextImageReferenceAction->setEnabled(count() > 1); + _previousImageReferenceAction->setEnabled(count() > 1); + _viewImageReferenceAction->setEnabled(imageSelected); + _removeImageReferenceAction->setEnabled(imageSelected); + _removeAllImageReferencesAction->setEnabled(count() > 0); +} + +void ImageReferencesListWidget::advanceCurrentImageReference(bool goUp) +{ + int index = findObjectItemIndex(activeObjectItem()); + + if (index != -1) + { + if (goUp) + { + if (--index < 0) + index = count() - 1; + } + else + { + if (++index >= count()) + index = 0; + } + + switchToObjectItem(objectItem(index), false); + } +} diff --git a/Grinder/ui/mainwnd/ImageReferencesListWidget.h b/Grinder/ui/mainwnd/ImageReferencesListWidget.h new file mode 100644 index 0000000000000000000000000000000000000000..00108585c447fc7784c95de3dbd33b1eeace77fb --- /dev/null +++ b/Grinder/ui/mainwnd/ImageReferencesListWidget.h @@ -0,0 +1,70 @@ +/****************************************************************************** + * File: ImageReferencesListWidget.h + * Date: 10.2.2018 + *****************************************************************************/ + +#ifndef IMAGEREFERENCESLISTWIDGET_H +#define IMAGEREFERENCESLISTWIDGET_H + +#include "ui/widget/MetaWidget.h" +#include "ui/widget/ObjectListWidget.h" +#include "ImageReferencesListItem.h" + +namespace grndr +{ + class ControlBar; + class ImageReference; + + using ImageReferenceObjectListWidget = ObjectListWidget<ImageReference, ImageReferencesListItem>; + + class ImageReferencesListWidget : public MetaWidget<ImageReferenceObjectListWidget> + { + Q_OBJECT + + public: + ImageReferencesListWidget(QWidget* parent = nullptr); + + public slots: + void nextImageReference() { advanceCurrentImageReference(); } + void previousImageReference() { advanceCurrentImageReference(true); } + + protected: + virtual std::unique_ptr<QMenu> createContextMenu() const override; + + virtual std::vector<QAction*> getActions(AddActionsMode mode) const override; + + protected: + virtual void switchToObjectItem(ImageReferencesListItem* item, bool selectItem = true) override; + + protected: + virtual void dragEnterEvent(QDragEnterEvent* event) override; + virtual void dragMoveEvent(QDragMoveEvent* event) override; + virtual void dropEvent(QDropEvent* event) override; + + private slots: + void switchImageReference() { switchToObjectItem(currentObjectItem()); } + void viewImageReference(); + void addImageReferences(); + void addImageReferences(QStringList files); + void removeImageReference(); + void removeAllImageReferences(); + + void imageReferenceSwitched(ImageReference* imageRef); + + void updateActions(); + + private: + void advanceCurrentImageReference(bool goUp = false); + + private: + QAction* _switchImageReferenceAction{nullptr}; + QAction* _nextImageReferenceAction{nullptr}; + QAction* _previousImageReferenceAction{nullptr}; + QAction* _viewImageReferenceAction{nullptr}; + QAction* _addImageReferencesAction{nullptr}; + QAction* _removeImageReferenceAction{nullptr}; + QAction* _removeAllImageReferencesAction{nullptr}; + }; +} + +#endif diff --git a/Grinder/ui/mainwnd/LabelsListItem.cpp b/Grinder/ui/mainwnd/LabelsListItem.cpp new file mode 100644 index 0000000000000000000000000000000000000000..065694bdec8c5a22f254fa9c276dc8fb4b8f31a3 --- /dev/null +++ b/Grinder/ui/mainwnd/LabelsListItem.cpp @@ -0,0 +1,22 @@ +/****************************************************************************** + * File: LabelsListItem.cpp + * Date: 07.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "LabelsListItem.h" +#include "res/Resources.h" + +LabelsListItem::LabelsListItem(Label* label) : ObjectListItem(label) +{ + setFlags(Qt::ItemIsEnabled|Qt::ItemIsSelectable|Qt::ItemIsEditable); + setIcon(QIcon{FILE_ICON_LABEL}); + + updateItem(); +} + +void LabelsListItem::updateItem() +{ + setText(getName()); + base_type::updateItem(); +} diff --git a/Grinder/ui/mainwnd/LabelsListItem.h b/Grinder/ui/mainwnd/LabelsListItem.h new file mode 100644 index 0000000000000000000000000000000000000000..ac73efebf82ffb933d1912befb36654c7a780c33 --- /dev/null +++ b/Grinder/ui/mainwnd/LabelsListItem.h @@ -0,0 +1,27 @@ +/****************************************************************************** + * File: LabelsListItem.h + * Date: 07.2.2018 + *****************************************************************************/ + +#ifndef LABELSLISTITEM_H +#define LABELSLISTITEM_H + +#include "ui/widget/ObjectListItem.h" +#include "project/Label.h" + +namespace grndr +{ + class LabelsListItem : public ObjectListItem<Label> + { + public: + LabelsListItem(Label* label); + + public: + virtual void updateItem() override; + + public: + QString getName() const { return _object->getName(); } + }; +} + +#endif diff --git a/Grinder/ui/mainwnd/LabelsListWidget.cpp b/Grinder/ui/mainwnd/LabelsListWidget.cpp new file mode 100644 index 0000000000000000000000000000000000000000..90d020eda625a9111847e06fa721141ef304ff11 --- /dev/null +++ b/Grinder/ui/mainwnd/LabelsListWidget.cpp @@ -0,0 +1,146 @@ +/****************************************************************************** + * File: LabelsListWidget.cpp + * Date: 07.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "LabelsListWidget.h" +#include "core/GrinderApplication.h" +#include "project/Project.h" +#include "util/StringUtils.h" +#include "util/UIUtils.h" +#include "res/Resources.h" + +LabelsListWidget::LabelsListWidget(QWidget* parent) : MetaWidget(parent) +{ + // Create labels actions + _switchLabelAction = UIUtils::createAction(this, "&Activate", FILE_ICON_ACTIVATE, SLOT(switchLabel()), "Activate the selected label", "Return"); + _renameLabelAction = UIUtils::createAction(this, "Rename label", FILE_ICON_EDIT, SLOT(renameLabel()), "Rename the selected label", "F2"); + _newLabelAction = UIUtils::createAction(this, "&New label", FILE_ICON_ADD, SLOT(newLabel()), "Add a new label", "Ctrl+L", Qt::WindowShortcut); + _removeLabelAction = UIUtils::createAction(this, "&Remove label", FILE_ICON_DELETE, SLOT(removeLabel()), "Remove the selected label", "Del"); + _removeAllLabelsAction = UIUtils::createAction(this, "Remove all labels", "", SLOT(removeAllLabels()), "Remove all labels"); + + // Get notified when a label name has been edited + connect(itemDelegate(), &QAbstractItemDelegate::commitData, this, &LabelsListWidget::labelRenamed, Qt::QueuedConnection); // Must be queued to prevent issues if renaming fails + + // Listen for item selections in order to update the actions + connect(this, &LabelsListWidget::itemSelectionChanged, this, &LabelsListWidget::updateActions); + + // Listen for label switching in order to update the active state flags + connect(&grinder()->projectController(), &ProjectController::labelSwitched, this, &LabelsListWidget::labelSwitched); + + updateActions(); +} + +std::unique_ptr<QMenu> LabelsListWidget::createContextMenu() const +{ + auto menu = widget_type::createContextMenu(); + menu->setDefaultAction(_switchLabelAction); + return menu; +} + +std::vector<QAction*> LabelsListWidget::getActions(AddActionsMode mode) const +{ + std::vector<QAction*> actions; + + if (mode == AddActionsMode::ContextMenu || mode == AddActionsMode::Toolbar) + { + actions.push_back(_switchLabelAction); + + if (mode == AddActionsMode::ContextMenu) + actions.push_back(_renameLabelAction); + + actions.push_back(nullptr); + } + + actions.push_back(_newLabelAction); + actions.push_back(_removeLabelAction); + + if (mode == AddActionsMode::ContextMenu) + { + actions.push_back(nullptr); + actions.push_back(_removeAllLabelsAction); + } + + return actions; +} + +void LabelsListWidget::switchToObjectItem(LabelsListItem* item, bool selectItem) +{ + if (item) + { + grinder()->projectController().switchLabel(item->object()); + + if (selectItem) + setCurrentItem(item); + } +} + +void LabelsListWidget::newLabel() +{ + QString newLabelName = StringUtils::generateUniqueItemName(grinder()->project().labels(), "New label", &Label::getName); + bool ok = false; + auto name = QInputDialog::getText(this, "Label name", "Enter the name of the new label:", QLineEdit::Normal, newLabelName, &ok); + + if (ok) + { + auto label = grinder()->projectController().createLabel(name.trimmed()); + + if (label) + { + // Find the just-created label item and switch to it + auto item = findObjectItem(label.get()); + + if (item) + switchToObjectItem(item); + } + } +} + +void LabelsListWidget::removeLabel() +{ + if (auto label = currentObject()) + { + grinder()->projectController().removeLabel(label); + + // Switch to the label that is currently selected if no active one exists + if (!grinder()->projectController().activeLabel()) + switchToObjectItem(currentObjectItem()); + } +} + +void LabelsListWidget::removeAllLabels() +{ + grinder()->projectController().removeAllLabels(); + updateActions(); +} + +void LabelsListWidget::labelSwitched(Label* label) +{ + objectSwitched(label); + updateActions(); +} + +void LabelsListWidget::labelRenamed(QWidget* editor) const +{ + if (auto labelItem = currentObjectItem()) + { + viewport()->setUpdatesEnabled(false); + + auto newName = reinterpret_cast<QLineEdit*>(editor)->text(); + grinder()->projectController().renameLabel(labelItem->object(), newName); + + labelItem->updateItem(); + viewport()->setUpdatesEnabled(true); + } +} + +void LabelsListWidget::updateActions() +{ + bool labelSelected = (currentObjectItem() != nullptr); + + _switchLabelAction->setEnabled(labelSelected && !currentObjectItem()->isActive()); + _renameLabelAction->setEnabled(labelSelected); + _removeLabelAction->setEnabled(labelSelected); + _removeAllLabelsAction->setEnabled(count() > 0); +} diff --git a/Grinder/ui/mainwnd/LabelsListWidget.h b/Grinder/ui/mainwnd/LabelsListWidget.h new file mode 100644 index 0000000000000000000000000000000000000000..abb61f15d113d36b793150e7d8eac3b71c89e050 --- /dev/null +++ b/Grinder/ui/mainwnd/LabelsListWidget.h @@ -0,0 +1,55 @@ +/****************************************************************************** + * File: LabelsListWidget.h + * Date: 07.2.2018 + *****************************************************************************/ + +#ifndef LABELSLISTWIDGET_H +#define LABELSLISTWIDGET_H + +#include "ui/widget/MetaWidget.h" +#include "ui/widget/ObjectListWidget.h" +#include "LabelsListItem.h" + +namespace grndr +{ + class Label; + + using LabelObjectListWidget = ObjectListWidget<Label, LabelsListItem>; + + class LabelsListWidget : public MetaWidget<LabelObjectListWidget> + { + Q_OBJECT + + public: + LabelsListWidget(QWidget* parent = nullptr); + + protected: + virtual std::unique_ptr<QMenu> createContextMenu() const override; + + virtual std::vector<QAction*> getActions(AddActionsMode mode) const override; + + protected: + virtual void switchToObjectItem(LabelsListItem* item, bool selectItem = true) override; + + private slots: + void switchLabel() { switchToObjectItem(currentObjectItem()); } + void renameLabel() { editItem(currentItem()); } + void newLabel(); + void removeLabel(); + void removeAllLabels(); + + void labelSwitched(Label* label); + void labelRenamed(QWidget* editor) const; + + void updateActions(); + + private: + QAction* _switchLabelAction{nullptr}; + QAction* _renameLabelAction{nullptr}; + QAction* _newLabelAction{nullptr}; + QAction* _removeLabelAction{nullptr}; + QAction* _removeAllLabelsAction{nullptr}; + }; +} + +#endif diff --git a/Grinder/ui/mainwnd/RecentProjectsMenu.cpp b/Grinder/ui/mainwnd/RecentProjectsMenu.cpp new file mode 100644 index 0000000000000000000000000000000000000000..fa083db7f888585b584521f1c30fd7959c701564 --- /dev/null +++ b/Grinder/ui/mainwnd/RecentProjectsMenu.cpp @@ -0,0 +1,57 @@ +/****************************************************************************** + * File: RecentProjectsMenu.cpp + * Date: 01.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "RecentProjectsMenu.h" + +void RecentProjectsMenu::setupUi(MRUStringList* recentProjects, std::function<void(QString)> loadProjectCallback) +{ + if (!recentProjects) + throw std::invalid_argument{_EXCPT("recentProjects may not be null")}; + + if (!loadProjectCallback) + throw std::invalid_argument{_EXCPT("loadProjectCallback may not be null")}; + + _recentProjects = recentProjects; + _loadProjectCallback = loadProjectCallback; + + updateUi(); +} + +void RecentProjectsMenu::updateUi() +{ + clear(); + + if (!_recentProjects->empty()) + { + for (int i = 0; i < _recentProjects->size(); ++i) + { + auto fileName = _recentProjects->at(i); + auto actionText = fileName; + + if (i < 9) + actionText.insert(0, QString{"&%1 | "}.arg(i + 1)); + + QAction* action = new QAction{actionText}; + action->setData(fileName); // Save the file name to retrieve it when executing + addAction(action); + + connect(action, &QAction::triggered, this, &RecentProjectsMenu::loadRecentProject); + } + } + else + addAction("No items to display")->setEnabled(false); +} + +void RecentProjectsMenu::loadRecentProject() +{ + if (auto action = dynamic_cast<QAction*>(sender())) + { + QString fileName = action->data().toString(); + + if (!fileName.isEmpty()) + _loadProjectCallback(fileName); + } +} diff --git a/Grinder/ui/mainwnd/RecentProjectsMenu.h b/Grinder/ui/mainwnd/RecentProjectsMenu.h new file mode 100644 index 0000000000000000000000000000000000000000..b3a7dc25005916fd907b34c31181face1ec84fb8 --- /dev/null +++ b/Grinder/ui/mainwnd/RecentProjectsMenu.h @@ -0,0 +1,35 @@ +/****************************************************************************** + * File: RecentProjectsMenu.h + * Date: 01.3.2018 + *****************************************************************************/ + +#ifndef RECENTPROJECTSMENU_H +#define RECENTPROJECTSMENU_H + +#include <QMenu> + +#include "common/MRUStringList.h" + +namespace grndr +{ + class RecentProjectsMenu : public QMenu + { + Q_OBJECT + + public: + using QMenu::QMenu; + + public: + void setupUi(MRUStringList* recentProjects, std::function<void(QString)> loadProjectCallback); + void updateUi(); + + private slots: + void loadRecentProject(); + + private: + MRUStringList* _recentProjects{nullptr}; + std::function<void(QString)> _loadProjectCallback{nullptr}; + }; +} + +#endif diff --git a/Grinder/ui/property/BlockPropertyTreeItem.cpp b/Grinder/ui/property/BlockPropertyTreeItem.cpp new file mode 100644 index 0000000000000000000000000000000000000000..2901f1848a1dd7ae4773ea6fb99177dc232f8726 --- /dev/null +++ b/Grinder/ui/property/BlockPropertyTreeItem.cpp @@ -0,0 +1,40 @@ +/****************************************************************************** + * File: BlockPropertyTreeItem.cpp + * Date: 06.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "BlockPropertyTreeItem.h" +#include "pipeline/Block.h" +#include "res/Resources.h" + +BlockPropertyTreeItem::BlockPropertyTreeItem(const std::shared_ptr<grndr::Block>& block) : + _block{block} +{ + if (!block) + throw std::invalid_argument{_EXCPT("block may not be null")}; + + setFlags(Qt::ItemIsEnabled|Qt::ItemIsSelectable); + + QFont fontBold = font(0); + fontBold.setBold(true); + setFont(0, fontBold); + + setBackgroundColor(0, QPalette().color(QPalette::Button)); + + updateItem(); + + // Update this item if the pipeline item has been renamed; use the internal helper since BlockPropertyTreeItem can't be a QObject + if (auto block = _block.lock()) + block->connect(block.get(), &PipelineItem::itemRenamed, _helper.get(), &_PropertyTreeItemHelper::updateItem); +} + +void BlockPropertyTreeItem::updateItem() +{ + if (auto block = _block.lock()) // Make sure that the underlying block still exists + { + setText(0, block->getName()); + setToolTip(0, text(0)); + setIcon(0, QIcon{FILE_ICON_BLOCK}); + } +} diff --git a/Grinder/ui/property/BlockPropertyTreeItem.h b/Grinder/ui/property/BlockPropertyTreeItem.h new file mode 100644 index 0000000000000000000000000000000000000000..567bb6fe321e8f6d13e9fd810199e7c267740e5d --- /dev/null +++ b/Grinder/ui/property/BlockPropertyTreeItem.h @@ -0,0 +1,31 @@ +/****************************************************************************** + * File: BlockPropertyTreeItem.h + * Date: 06.3.2018 + *****************************************************************************/ + +#ifndef BLOCKPROPERTYTREEITEM_H +#define BLOCKPROPERTYTREEITEM_H + +#include "PropertyTreeItem.h" + +namespace grndr +{ + class Block; + + class BlockPropertyTreeItem : public PropertyTreeItem + { + public: + BlockPropertyTreeItem(const std::shared_ptr<Block>& block); + + public: + virtual void updateItem() override; + + public: + const std::weak_ptr<Block>& pipelineItem() const { return _block; } + + private: + std::weak_ptr<Block> _block; + }; +} + +#endif diff --git a/Grinder/ui/property/PropertyEditor.h b/Grinder/ui/property/PropertyEditor.h new file mode 100644 index 0000000000000000000000000000000000000000..fca800260d4381abbdf0d3de9046f6151ded9f7c --- /dev/null +++ b/Grinder/ui/property/PropertyEditor.h @@ -0,0 +1,44 @@ +/****************************************************************************** + * File: PropertyEditor.h + * Date: 07.3.2018 + *****************************************************************************/ + +#ifndef PROPERTYEDITOR_H +#define PROPERTYEDITOR_H + +#include <QWidget> + +namespace grndr +{ + class PropertyBase; + + template<typename Base, typename PropertyType = PropertyBase> + class PropertyEditor : public Base + { + static_assert(std::is_base_of<QWidget, Base>::value, "Base must be derived from QWidget"); + + public: + using widget_type = PropertyEditor<Base, PropertyType>; + using base_type = Base; + using property_type = PropertyType; + + public: + PropertyEditor(PropertyType* property, QWidget* parent = nullptr); + + protected: + virtual void showEvent(QShowEvent* event) override; + + protected: + QString getPropertyValue() const; + void setPropertyValue(const QString& value); + + virtual void applyPropertyValue() = 0; + + protected: + PropertyType* _property{nullptr}; + }; +} + +#include "PropertyEditor.impl.h" + +#endif diff --git a/Grinder/ui/property/PropertyEditor.impl.h b/Grinder/ui/property/PropertyEditor.impl.h new file mode 100644 index 0000000000000000000000000000000000000000..0746a7fc48cf9fff1afcd672c3d7e1ae0a8eae94 --- /dev/null +++ b/Grinder/ui/property/PropertyEditor.impl.h @@ -0,0 +1,45 @@ +/****************************************************************************** + * File: PropertyEditor.impl.h + * Date: 07.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "PropertyEditor.h" +#include "common/PropertyBase.h" + +template<typename Base, typename PropertyType> +PropertyEditor<Base, PropertyType>::PropertyEditor(PropertyType* property, QWidget* parent) : Base(parent), + _property{property} +{ + if (!property) + throw std::invalid_argument{_EXCPT("property may not be null")}; + + this->setFocusPolicy(Qt::StrongFocus); + + // If the property value changes, reflect the new value + _property->connect(_property, &PropertyBase::valueChanged, this, &widget_type::applyPropertyValue); +} + +template<typename Base, typename PropertyType> +void PropertyEditor<Base, PropertyType>::showEvent(QShowEvent* event) +{ + base_type::showEvent(event); + applyPropertyValue(); +} + +template<typename Base, typename PropertyType> +QString PropertyEditor<Base, PropertyType>::getPropertyValue() const +{ + return _property->toString(); +} + +template<typename Base, typename PropertyType> +void PropertyEditor<Base, PropertyType>::setPropertyValue(const QString& value) +{ + try { + _property->fromString(value); + } catch (...) { + // If the string-value is invalid, revert to the previous value in the editor by simulating a property value change + applyPropertyValue(); + } +} diff --git a/Grinder/ui/property/PropertyTreeItem.cpp b/Grinder/ui/property/PropertyTreeItem.cpp new file mode 100644 index 0000000000000000000000000000000000000000..e7bdf2421cb132593b96f609ba9f2b4a5140eede --- /dev/null +++ b/Grinder/ui/property/PropertyTreeItem.cpp @@ -0,0 +1,18 @@ +/****************************************************************************** + * File: PropertyTreeItem.cpp + * Date: 06.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "PropertyTreeItem.h" + +void _PropertyTreeItemHelper::updateItem() +{ + _treeItem->updateItem(); +} + +PropertyTreeItem::PropertyTreeItem() : + _helper{new _PropertyTreeItemHelper{this}} +{ + +} diff --git a/Grinder/ui/property/PropertyTreeItem.h b/Grinder/ui/property/PropertyTreeItem.h new file mode 100644 index 0000000000000000000000000000000000000000..4b531b43fa19ec4aae88f3c62704be3abca30db7 --- /dev/null +++ b/Grinder/ui/property/PropertyTreeItem.h @@ -0,0 +1,45 @@ +/****************************************************************************** + * File: PropertyTreeItem.h + * Date: 06.3.2018 + *****************************************************************************/ + +#ifndef PROPERTYTREEITEM_H +#define PROPERTYTREEITEM_H + +#include <QTreeWidgetItem> +#include <memory> + +namespace grndr +{ + class PipelineItem; + class PropertyBase; + class PropertyTreeItem; + + class _PropertyTreeItemHelper : public QObject + { + Q_OBJECT + + friend class PropertyTreeItem; + + public slots: + void updateItem(); + + private: + _PropertyTreeItemHelper(PropertyTreeItem* treeItem) : _treeItem{treeItem} { } + PropertyTreeItem* _treeItem{nullptr}; + }; + + class PropertyTreeItem : public QTreeWidgetItem + { + public: + PropertyTreeItem(); + + public: + virtual void updateItem() = 0; + + protected: + std::unique_ptr<_PropertyTreeItemHelper> _helper; + }; +} + +#endif diff --git a/Grinder/ui/property/PropertyTreeItemDelegate.cpp b/Grinder/ui/property/PropertyTreeItemDelegate.cpp new file mode 100644 index 0000000000000000000000000000000000000000..657e77173a6f8323632f206c66a51a84b2902161 --- /dev/null +++ b/Grinder/ui/property/PropertyTreeItemDelegate.cpp @@ -0,0 +1,48 @@ +/****************************************************************************** + * File: PropertyTreeItemDelegate.cpp + * Date: 07.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "PropertyTreeItemDelegate.h" +#include "PropertyTreeWidget.h" +#include "ValuePropertyTreeItem.h" + +#include "PropertyEditor.h" + +PropertyTreeItemDelegate::PropertyTreeItemDelegate(PropertyTreeWidget* widget, QObject* parent) : QStyledItemDelegate(parent), + _widget{widget} +{ + if (!widget) + throw std::invalid_argument{_EXCPT("widget may not be null")}; +} + +QWidget* PropertyTreeItemDelegate::createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const +{ + Q_UNUSED(parent); + Q_UNUSED(option); + + if (auto item = _widget->valuePropertyItem(index)) + return item->createEditor(parent); + else + return nullptr; +} + +void PropertyTreeItemDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const +{ + Q_UNUSED(editor); + Q_UNUSED(model); + + // The model data has been changed, so update the corresponding item + if (auto item = _widget->valuePropertyItem(index)) + item->updateItem(); +} + +void PropertyTreeItemDelegate::initStyleOption(QStyleOptionViewItem* option, const QModelIndex& index) const +{ + QStyledItemDelegate::initStyleOption(option, index); + + // If editing the given item, hide its text so that it doesn't appear below the editor + if (_widget->isEditing() && index.row() == _widget->currentIndex().row()) + option->text = ""; +} diff --git a/Grinder/ui/property/PropertyTreeItemDelegate.h b/Grinder/ui/property/PropertyTreeItemDelegate.h new file mode 100644 index 0000000000000000000000000000000000000000..b6dfc52c3a532c685fb240a983468e9cf0e4c959 --- /dev/null +++ b/Grinder/ui/property/PropertyTreeItemDelegate.h @@ -0,0 +1,35 @@ +/****************************************************************************** + * File: PropertyTreeItemDelegate.h + * Date: 07.3.2018 + *****************************************************************************/ + +#ifndef PROPERTYTREEITEMDELEGATE_H +#define PROPERTYTREEITEMDELEGATE_H + +#include <QStyledItemDelegate> + +namespace grndr +{ + class PropertyTreeWidget; + + class PropertyTreeItemDelegate : public QStyledItemDelegate + { + Q_OBJECT + + public: + PropertyTreeItemDelegate(PropertyTreeWidget* widget, QObject* parent = nullptr); + + public: + virtual QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const override; + + virtual void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const override; + + protected: + virtual void initStyleOption(QStyleOptionViewItem* option, const QModelIndex& index) const override; + + private: + PropertyTreeWidget* _widget{nullptr}; + }; +} + +#endif diff --git a/Grinder/ui/property/PropertyTreeWidget.cpp b/Grinder/ui/property/PropertyTreeWidget.cpp new file mode 100644 index 0000000000000000000000000000000000000000..e1e0ae4016d393d46930aab80fc4f9762e8b50b5 --- /dev/null +++ b/Grinder/ui/property/PropertyTreeWidget.cpp @@ -0,0 +1,247 @@ +/****************************************************************************** + * File: PropertyTreeWidget.cpp + * Date: 06.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "PropertyTreeWidget.h" +#include "BlockPropertyTreeItem.h" +#include "ValuePropertyTreeItem.h" +#include "PropertyTreeItemDelegate.h" +#include "core/GrinderApplication.h" +#include "ui/graph/GraphBlockNode.h" +#include "util/UIUtils.h" + +PropertyTreeWidget::PropertyTreeWidget(QWidget *parent) : QTreeWidget(parent) +{ + // Set the delegate for the second column which handles editing of the properties + setItemDelegateForColumn(1, new PropertyTreeItemDelegate{this}); + + // Listen for pipeline switches to clear all shown properties and to listen for selection changes + connect(&grinder()->pipelineController(), &PipelineController::pipelineSwitching, this, &PropertyTreeWidget::pipelineSwitching); + connect(&grinder()->pipelineController(), &PipelineController::pipelineSwitched, this, &PropertyTreeWidget::pipelineSwitched); + + // Update the actions whenever the selection changes + connect(this, &QTreeWidget::itemSelectionChanged, this, &PropertyTreeWidget::itemSelectionChanged); + + // Create actions + _editItem = UIUtils::createAction(this, "&Edit property", "", SLOT(editCurrentItem()), "Edit the selected property", "F2"); + _expandAllAction = UIUtils::createAction(this, "E&xpand all", "", SLOT(expandAllItems()), "Expand all items"); + _collapseAllAction = UIUtils::createAction(this, "&Collapse all", "", SLOT(collapseAllItems()), "Collapse all items"); + + updateActions(); +} + +void PropertyTreeWidget::setupUi(QLabel* propertyDescLabel) +{ + if (!propertyDescLabel) + throw std::invalid_argument{_EXCPT("propertyDescLabel may not be null")}; + + _propertyDescLabel = propertyDescLabel; + updatePropertyDescription(); +} + +void PropertyTreeWidget::showEvent(QShowEvent* event) +{ + QTreeWidget::showEvent(event); + + header()->setDefaultAlignment(Qt::AlignCenter); + header()->setSortIndicator(0, Qt::AscendingOrder); +} + +void PropertyTreeWidget::contextMenuEvent(QContextMenuEvent* event) +{ + QMenu menu; + + menu.addAction(_editItem); + menu.addSeparator(); + menu.addAction(_expandAllAction); + menu.addAction(_collapseAllAction); + + menu.exec(event->globalPos()); +} + +void PropertyTreeWidget::mouseDoubleClickEvent(QMouseEvent* event) +{ + QTreeWidget::mouseDoubleClickEvent(event); + + if (event->button() == Qt::LeftButton) + editCurrentItem(); +} + +void PropertyTreeWidget::keyPressEvent(QKeyEvent* event) +{ + if (event->modifiers() == Qt::NoModifier && (event->key() == Qt::Key_Return || event->key() == Qt::Key_Space)) // Also edit the property when pressing Return or Space + editCurrentItem(); + else + QTreeWidget::keyPressEvent(event); +} + +void PropertyTreeWidget::addBlocks(const std::vector<std::shared_ptr<grndr::Block> >& blocks) +{ + // Create a top-level item for every block + for (auto block : blocks) + { + BlockPropertyTreeItem* treeItem = new BlockPropertyTreeItem{block}; + addTopLevelItem(treeItem); + + addProperties(block.get(), treeItem); + + treeItem->setExpanded(true); + treeItem->setFirstColumnSpanned(true); + } + + updateColumnWidths(); +} + +void PropertyTreeWidget::addProperties(const PipelineItem* pipelineItem, QTreeWidgetItem* parentItem) +{ + std::vector<std::shared_ptr<PropertyBase>> properties; + + // Ignore read-only properties + for (auto& property : pipelineItem->properties()) + { + if (!property->hasFlag(PropertyBase::Flag::ReadOnly) && !property->hasFlag(PropertyBase::Flag::Hidden)) + properties.push_back(property); + } + + if (!properties.empty()) + { + for (auto& property : properties) + { + ValuePropertyTreeItem* treeItem = new ValuePropertyTreeItem{property}; + parentItem->addChild(treeItem); + } + } + else + { + // Add an item showing that the pipeline item has no properties + QTreeWidgetItem* emptyItem = new QTreeWidgetItem({"No properties to display"}); + parentItem->addChild(emptyItem); + + emptyItem->setFlags(Qt::NoItemFlags); + emptyItem->setFirstColumnSpanned(true); + } +} + +void PropertyTreeWidget::itemSelectionChanged() +{ + updateActions(); + updatePropertyDescription(); +} + +void PropertyTreeWidget::editCurrentItem() +{ + if (auto item = currentValuePropertyItem()) + editItem(item, 1); +} + +void PropertyTreeWidget::pipelineSwitching(Pipeline* pipeline) +{ + Q_UNUSED(pipeline); + + clear(); + + if (auto scene = grinder()->pipelineController().activeScene()) + disconnect(scene, &GraphScene::selectionChanged, this, &PropertyTreeWidget::sceneSelectionChanged); +} + +void PropertyTreeWidget::pipelineSwitched(Pipeline* pipeline) +{ + Q_UNUSED(pipeline); + + // Listen for selection changes in the current graph scene + if (auto scene = grinder()->pipelineController().activeScene()) + connect(scene, &GraphScene::selectionChanged, this, &PropertyTreeWidget::sceneSelectionChanged); +} + +void PropertyTreeWidget::sceneSelectionChanged() +{ + clear(); + + if (auto scene = grinder()->pipelineController().activeScene()) + { + // Add all selected blocks + std::vector<std::shared_ptr<Block>> blocks; + + for (auto& blockNode : scene->getNodes<GraphBlockNode>(true)) + { + if (auto block = blockNode->block().lock()) + blocks.push_back(block); + } + + addBlocks(blocks); + } + + updateActions(); +} + +void PropertyTreeWidget::updateActions() +{ + auto item = currentValuePropertyItem(); + + _editItem->setEnabled(item != nullptr); + _expandAllAction->setEnabled(topLevelItemCount() > 0); + _collapseAllAction->setEnabled(topLevelItemCount() > 0); +} + +void PropertyTreeWidget::updatePropertyDescription() +{ + QString desc; + + if (auto item = currentValuePropertyItem()) + { + if (auto property = item->property().lock()) + { + QString propertyName = QString{"<b>%1</b>"}.arg(property->getName()); + QString propertyDesc = property->getDescription(); + + if (propertyDesc.isEmpty()) + propertyDesc = "No description available"; + + desc = QString{"<div style='margin-bottom: 6px'>%1</div><div>%2</div>"}.arg(propertyName).arg(propertyDesc); + } + } + + _propertyDescLabel->setText(desc); +} + +ValuePropertyTreeItem* PropertyTreeWidget::valuePropertyItem(const QModelIndex& index) const +{ + return dynamic_cast<ValuePropertyTreeItem*>(itemFromIndex(index)); +} + +ValuePropertyTreeItem* PropertyTreeWidget::currentValuePropertyItem() const +{ + return valuePropertyItem(currentIndex()); +} + +void PropertyTreeWidget::updateColumnWidths() +{ + // Measure all level-1 items to get the minimum width of the first column + int minWidth = header()->defaultSectionSize(); + + for (int i = 0; i < topLevelItemCount(); ++i) + { + auto rootItem = topLevelItem(i); + + for (int j = 0; j < rootItem->childCount(); ++j) + { + auto childItem = rootItem->child(j); + + if (!childItem->isFirstColumnSpanned()) // Spanned items mean that the parent has no properties, so skip these + { + QFontMetrics fontMetrics{childItem->font(0)}; + minWidth = std::max(fontMetrics.width(childItem->text(0)) + 3 * indentation(), minWidth); + } + } + } + + setColumnWidth(0, minWidth); +} + +void PropertyTreeWidget::expandAllItems(bool expand) +{ + for (int i = 0; i < topLevelItemCount(); ++i) + topLevelItem(i)->setExpanded(expand); +} diff --git a/Grinder/ui/property/PropertyTreeWidget.h b/Grinder/ui/property/PropertyTreeWidget.h new file mode 100644 index 0000000000000000000000000000000000000000..1d44a59a1ce4dfc4f3f4e420cb2567a218648dfe --- /dev/null +++ b/Grinder/ui/property/PropertyTreeWidget.h @@ -0,0 +1,82 @@ +/****************************************************************************** + * File: PropertyTreeWidget.h + * Date: 06.3.2018 + *****************************************************************************/ + +#ifndef PROPERTYTREEWIDGET_H +#define PROPERTYTREEWIDGET_H + +#include <QTreeWidget> +#include <QLabel> +#include <memory> + +namespace grndr +{ + class Pipeline; + class PipelineItem; + class Block; + class GraphBlockNode; + class GraphConnectionNode; + class ValuePropertyTreeItem; + + class PropertyTreeWidget : public QTreeWidget + { + Q_OBJECT + + friend class PropertyTreeItemDelegate; + + public: + PropertyTreeWidget(QWidget *parent = nullptr); + + public: + void setupUi(QLabel* propertyDescLabel); + + void clear() { QTreeWidget::clear(); updateActions(); updatePropertyDescription(); } + + protected: + virtual void showEvent(QShowEvent* event) override; + + virtual void contextMenuEvent(QContextMenuEvent* event) override; + virtual void mouseDoubleClickEvent(QMouseEvent* event) override; + virtual void keyPressEvent(QKeyEvent* event) override; + + private: + void addBlocks(const std::vector<std::shared_ptr<Block>>& blocks); + void addProperties(const PipelineItem* pipelineItem, QTreeWidgetItem* parentItem); + + private slots: + void itemSelectionChanged(); + void editCurrentItem(); + + void pipelineSwitching(Pipeline* pipeline); + void pipelineSwitched(Pipeline* pipeline); + + void sceneSelectionChanged(); + + void expandAllItems() { expandAllItems(true); } + void collapseAllItems() { expandAllItems(false); } + + void updateActions(); + void updatePropertyDescription(); + + private: + ValuePropertyTreeItem* valuePropertyItem(const QModelIndex& index) const; + ValuePropertyTreeItem* currentValuePropertyItem() const; + + private: + void updateColumnWidths(); + + void expandAllItems(bool expand); + + bool isEditing() { return state() == EditingState; } + + private: + QAction* _editItem{nullptr}; + QAction* _expandAllAction{nullptr}; + QAction* _collapseAllAction{nullptr}; + + QLabel* _propertyDescLabel; + }; +} + +#endif diff --git a/Grinder/ui/property/ValuePropertyTreeItem.cpp b/Grinder/ui/property/ValuePropertyTreeItem.cpp new file mode 100644 index 0000000000000000000000000000000000000000..a602ea8d160232bb0bad4e2dce0ede7c232c147c --- /dev/null +++ b/Grinder/ui/property/ValuePropertyTreeItem.cpp @@ -0,0 +1,45 @@ +/****************************************************************************** + * File: ValuePropertyTreeItem.cpp + * Date: 07.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ValuePropertyTreeItem.h" +#include "common/PropertyBase.h" + +ValuePropertyTreeItem::ValuePropertyTreeItem(const std::shared_ptr<PropertyBase>& property) : + _property{property} +{ + if (!property) + throw std::invalid_argument{_EXCPT("property may not be null")}; + + setFlags(Qt::ItemIsEnabled|Qt::ItemIsSelectable|Qt::ItemIsEditable); + + setTextColor(1, QColor{20, 105, 140}); + + updateItem(); + + // Update this item if the property value has changed; use the internal helper since PropertyTreeItem can't be a QObject + if (auto property = _property.lock()) + property->connect(property.get(), &PropertyBase::valueChanged, _helper.get(), &_PropertyTreeItemHelper::updateItem); +} + +QWidget* ValuePropertyTreeItem::createEditor(QWidget* parent) const +{ + if (auto property = _property.lock()) // Make sure that the underlying property still exists + return property->createEditor(parent); + else + return nullptr; +} + +void ValuePropertyTreeItem::updateItem() +{ + if (auto property = _property.lock()) // Make sure that the underlying property still exists + { + setText(0, property->getName()); + setToolTip(0, text(0)); + + setText(1, property->toString()); + setToolTip(1, text(1)); + } +} diff --git a/Grinder/ui/property/ValuePropertyTreeItem.h b/Grinder/ui/property/ValuePropertyTreeItem.h new file mode 100644 index 0000000000000000000000000000000000000000..fd879fdf4a1e27a0b8061f67ec3e98f0dbca57c7 --- /dev/null +++ b/Grinder/ui/property/ValuePropertyTreeItem.h @@ -0,0 +1,36 @@ +/****************************************************************************** + * File: ValuePropertyTreeItem.h + * Date: 07.3.2018 + *****************************************************************************/ + +#ifndef VALUEPROPERTYTREEITEM_H +#define VALUEPROPERTYTREEITEM_H + +#include "PropertyTreeItem.h" + +namespace grndr +{ + class PipelineItem; + class PropertyBase; + + class ValuePropertyTreeItem : public PropertyTreeItem + { + public: + ValuePropertyTreeItem(const std::shared_ptr<PropertyBase>& property); + + public: + QWidget* createEditor(QWidget* parent) const; + + public: + virtual void updateItem() override; + + public: + std::weak_ptr<PropertyBase>& property() { return _property; } + const std::weak_ptr<PropertyBase>& property() const { return _property; } + + private: + std::weak_ptr<PropertyBase> _property; + }; +} + +#endif diff --git a/Grinder/ui/property/editors/AnglePropertyEditor.cpp b/Grinder/ui/property/editors/AnglePropertyEditor.cpp new file mode 100644 index 0000000000000000000000000000000000000000..ac882f9acefa234c3beb9faaea3ecb74e0002196 --- /dev/null +++ b/Grinder/ui/property/editors/AnglePropertyEditor.cpp @@ -0,0 +1,63 @@ +/****************************************************************************** + * File: AnglePropertyEditor.cpp + * Date: 26.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "AnglePropertyEditor.h" + +AnglePropertyEditor::AnglePropertyEditor(AngleProperty* property, QWidget* parent) : PropertyEditor(property, parent), + _angleDial{new QDial{}}, _angleEdit{new AutoFocusLineEdit{}} +{ + setFocusPolicy(Qt::NoFocus); + + _angleDial->setFixedWidth(50); + _angleDial->setFixedHeight(50); + _angleDial->setRange(0, 360); + _angleDial->setWrapping(true); + _angleDial->setNotchesVisible(true); + _angleDial->setNotchTarget(45); + _angleDial->setInvertedAppearance(true); + _angleDial->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + + _angleEdit->setValidator(new QRegExpValidator{QRegExp{"\\d+(.?\\d+)?"}}); + _angleEdit->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + + // Create a horizontal layout and add all controls to it + auto layout = new QHBoxLayout{}; + layout->setContentsMargins(0, 0, 0, 0); + layout->addWidget(_angleDial); + layout->addWidget(_angleEdit); + layout->addWidget(new QLabel{"deg"}); + setLayout(layout); + + // Keep the dial and the text edit in sync; this also updates the property + connect(_angleDial, &QDial::valueChanged, this, &AnglePropertyEditor::angleDialChanged); + connect(_angleEdit, &QLineEdit::textChanged, this, &AnglePropertyEditor::angleValueChanged); + + applyPropertyValue(); +} + +void AnglePropertyEditor::applyPropertyValue() +{ + _angleEdit->setText(getPropertyValue()); +} + +void AnglePropertyEditor::angleDialChanged(int value) +{ + auto angle = (value + 270) % 360; + QString text = QString{"%1"}.arg(angle); + + if (text != _angleEdit->text()) + _angleEdit->setText(text); +} + +void AnglePropertyEditor::angleValueChanged(const QString& value) +{ + auto angle = (std::lround(value.toDouble()) + 90) % 360; + + if (angle != _angleDial->value()) + _angleDial->setValue(angle); + + setPropertyValue(_angleEdit->text()); +} diff --git a/Grinder/ui/property/editors/AnglePropertyEditor.h b/Grinder/ui/property/editors/AnglePropertyEditor.h new file mode 100644 index 0000000000000000000000000000000000000000..56b4b1b80ac88a4ab1e51caab46bb6f9f26194d1 --- /dev/null +++ b/Grinder/ui/property/editors/AnglePropertyEditor.h @@ -0,0 +1,35 @@ +/****************************************************************************** + * File: AnglePropertyEditor.h + * Date: 26.3.2018 + *****************************************************************************/ + +#ifndef ANGLEPROPERTYEDITOR_H +#define ANGLEPROPERTYEDITOR_H + +#include "common/properties/AngleProperty.h" +#include "ui/widget/AutoFocusLineEdit.h" +#include "ui/property/PropertyEditor.h" + +namespace grndr +{ + class AnglePropertyEditor : public PropertyEditor<QWidget, AngleProperty> + { + Q_OBJECT + + public: + AnglePropertyEditor(AngleProperty* property, QWidget *parent = nullptr); + + protected: + virtual void applyPropertyValue() override; + + private slots: + void angleDialChanged(int value); + void angleValueChanged(const QString& value); + + private: + QDial* _angleDial{nullptr}; + AutoFocusLineEdit* _angleEdit{nullptr}; + }; +} + +#endif diff --git a/Grinder/ui/property/editors/BoolPropertyEditor.cpp b/Grinder/ui/property/editors/BoolPropertyEditor.cpp new file mode 100644 index 0000000000000000000000000000000000000000..3ffbdd41809acdd303bb4689e503e8075ae4cdc8 --- /dev/null +++ b/Grinder/ui/property/editors/BoolPropertyEditor.cpp @@ -0,0 +1,27 @@ +/****************************************************************************** + * File: BoolPropertyEditor.cpp + * Date: 07.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "BoolPropertyEditor.h" + +BoolPropertyEditor::BoolPropertyEditor(BoolProperty* property, QWidget *parent) : PropertyEditor(property, parent) +{ + setText("Enable"); + + applyPropertyValue(); + + // Update the property whenever the checkbox has been toggled + connect(this, &QCheckBox::toggled, this, &BoolPropertyEditor::itemChecked); +} + +void BoolPropertyEditor::applyPropertyValue() +{ + setChecked(_property->getValue()); +} + +void BoolPropertyEditor::itemChecked(bool checked) +{ + _property->setValue(checked); +} diff --git a/Grinder/ui/property/editors/BoolPropertyEditor.h b/Grinder/ui/property/editors/BoolPropertyEditor.h new file mode 100644 index 0000000000000000000000000000000000000000..71264345e17f1e9bcb8586d7e0be9013c551eb22 --- /dev/null +++ b/Grinder/ui/property/editors/BoolPropertyEditor.h @@ -0,0 +1,31 @@ +/****************************************************************************** + * File: BoolPropertyEditor.h + * Date: 07.3.2018 + *****************************************************************************/ + +#ifndef BOOLPROPERTYEDITOR_H +#define BOOLPROPERTYEDITOR_H + +#include <QCheckBox> + +#include "common/properties/BoolProperty.h" +#include "ui/property/PropertyEditor.h" + +namespace grndr +{ + class BoolPropertyEditor : public PropertyEditor<QCheckBox, BoolProperty> + { + Q_OBJECT + + public: + BoolPropertyEditor(BoolProperty* property, QWidget *parent = nullptr); + + protected: + virtual void applyPropertyValue() override; + + private slots: + void itemChecked(bool checked); + }; +} + +#endif diff --git a/Grinder/ui/property/editors/DualTextPropertyEditor.h b/Grinder/ui/property/editors/DualTextPropertyEditor.h new file mode 100644 index 0000000000000000000000000000000000000000..9707afd3596aa6943aa21d46993f7ffd3f955d4a --- /dev/null +++ b/Grinder/ui/property/editors/DualTextPropertyEditor.h @@ -0,0 +1,31 @@ +/****************************************************************************** + * File: DualTextPropertyEditor.h + * Date: 23.3.2018 + *****************************************************************************/ + +#ifndef DUALTEXTPROPERTYEDITOR_H +#define DUALTEXTPROPERTYEDITOR_H + +#include "ui/widget/AutoFocusLineEdit.h" +#include "ui/property/PropertyEditor.h" + +namespace grndr +{ + template<class PropertyType> + class DualTextPropertyEditor : public PropertyEditor<QWidget, PropertyType> + { + public: + DualTextPropertyEditor(PropertyType* property, QString name1, QString name2, QString inputMask1, QString inputMask2, QWidget *parent = nullptr); + + protected: + virtual void editingFinished() = 0; + + protected: + AutoFocusLineEdit* _lineEdit1{nullptr}; + AutoFocusLineEdit* _lineEdit2{nullptr}; + }; +} + +#include "DualTextPropertyEditor.impl.h" + +#endif diff --git a/Grinder/ui/property/editors/DualTextPropertyEditor.impl.h b/Grinder/ui/property/editors/DualTextPropertyEditor.impl.h new file mode 100644 index 0000000000000000000000000000000000000000..0bb8e25c795f39b28cd9d5887be73bb4cff300e9 --- /dev/null +++ b/Grinder/ui/property/editors/DualTextPropertyEditor.impl.h @@ -0,0 +1,36 @@ +/****************************************************************************** + * File: DualTextPropertyEditor.impl.h + * Date: 23.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "DualTextPropertyEditor.h" + +template<class PropertyType> +DualTextPropertyEditor<PropertyType>::DualTextPropertyEditor(PropertyType* property, QString name1, QString name2, QString inputMask1, QString inputMask2, QWidget* parent) : PropertyEditor<QWidget, PropertyType>(property, parent), + _lineEdit1{new AutoFocusLineEdit{}}, _lineEdit2{new AutoFocusLineEdit{}} +{ + this->setFocusPolicy(Qt::NoFocus); + + if (!inputMask1.isEmpty()) + _lineEdit1->setValidator(new QRegExpValidator{QRegExp{inputMask1}}); + + if (!inputMask2.isEmpty()) + _lineEdit2->setValidator(new QRegExpValidator{QRegExp{inputMask2}}); + + // Create a horizontal layout and add all controls to it + auto layout = new QHBoxLayout{}; + layout->setContentsMargins(0, 0, 0, 0); + layout->addWidget(new QLabel{name1}); + layout->addWidget(_lineEdit1); + layout->addWidget(new QLabel{name2}); + layout->addWidget(_lineEdit2); + this->setLayout(layout); + + _lineEdit1->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + _lineEdit2->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + + // Update the property whenever editing has been finished + this->connect(_lineEdit1, &QLineEdit::editingFinished, this, &DualTextPropertyEditor<PropertyType>::editingFinished); + this->connect(_lineEdit2, &QLineEdit::editingFinished, this, &DualTextPropertyEditor<PropertyType>::editingFinished); +} diff --git a/Grinder/ui/property/editors/PointPropertyEditor.cpp b/Grinder/ui/property/editors/PointPropertyEditor.cpp new file mode 100644 index 0000000000000000000000000000000000000000..77e39b9265e049089ac901512a774a368898ffdd --- /dev/null +++ b/Grinder/ui/property/editors/PointPropertyEditor.cpp @@ -0,0 +1,31 @@ +/****************************************************************************** + * File: PointPropertyEditor.cpp + * Date: 07.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "PointPropertyEditor.h" +#include "util/StringConv.h" + +PointPropertyEditor::PointPropertyEditor(PointProperty* property, QWidget* parent) : DualTextPropertyEditor(property, "x:", "y:", "-?\\d+", "-?\\d+", parent) +{ + applyPropertyValue(); +} + +void PointPropertyEditor::applyPropertyValue() +{ + auto point = _property->getValue(); + + _lineEdit1->setText(QString{"%1"}.arg(point.x())); + _lineEdit2->setText(QString{"%1"}.arg(point.y())); +} + +void PointPropertyEditor::editingFinished() +{ + QPoint point; + + point.setX(_lineEdit1->text().toInt()); + point.setY(_lineEdit2->text().toInt()); + + setPropertyValue(StringConv::convertValue(point)); +} diff --git a/Grinder/ui/property/editors/PointPropertyEditor.h b/Grinder/ui/property/editors/PointPropertyEditor.h new file mode 100644 index 0000000000000000000000000000000000000000..21e2b041d84f9aba6f26c0ea7a3ca9903cf9a17b --- /dev/null +++ b/Grinder/ui/property/editors/PointPropertyEditor.h @@ -0,0 +1,31 @@ +/****************************************************************************** + * File: PointPropertyEditor.h + * Date: 23.3.2018 + *****************************************************************************/ + +#ifndef POINTPROPERTYEDITOR_H +#define POINTPROPERTYEDITOR_H + +#include <QLineEdit> + +#include "DualTextPropertyEditor.h" +#include "common/properties/PointProperty.h" + +namespace grndr +{ + class PointPropertyEditor : public DualTextPropertyEditor<PointProperty> + { + Q_OBJECT + + public: + PointPropertyEditor(PointProperty* property, QWidget *parent = nullptr); + + protected: + virtual void applyPropertyValue() override; + + protected: + virtual void editingFinished() override; + }; +} + +#endif diff --git a/Grinder/ui/property/editors/SizePropertyEditor.cpp b/Grinder/ui/property/editors/SizePropertyEditor.cpp new file mode 100644 index 0000000000000000000000000000000000000000..0a22a61a916f19129dea177987907ea6827ed327 --- /dev/null +++ b/Grinder/ui/property/editors/SizePropertyEditor.cpp @@ -0,0 +1,31 @@ +/****************************************************************************** + * File: SizePropertyEditor.cpp + * Date: 07.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "SizePropertyEditor.h" +#include "util/StringConv.h" + +SizePropertyEditor::SizePropertyEditor(SizeProperty* property, QWidget* parent) : DualTextPropertyEditor(property, "w:", "h:", "\\d+", "\\d+", parent) +{ + applyPropertyValue(); +} + +void SizePropertyEditor::applyPropertyValue() +{ + auto size = _property->getValue(); + + _lineEdit1->setText(QString{"%1"}.arg(size.width())); + _lineEdit2->setText(QString{"%1"}.arg(size.height())); +} + +void SizePropertyEditor::editingFinished() +{ + QSize size; + + size.setWidth(_lineEdit1->text().toInt()); + size.setHeight(_lineEdit2->text().toInt()); + + setPropertyValue(StringConv::convertValue(size)); +} diff --git a/Grinder/ui/property/editors/SizePropertyEditor.h b/Grinder/ui/property/editors/SizePropertyEditor.h new file mode 100644 index 0000000000000000000000000000000000000000..1d326df973aa89c608ee207806922e7ff51568f9 --- /dev/null +++ b/Grinder/ui/property/editors/SizePropertyEditor.h @@ -0,0 +1,31 @@ +/****************************************************************************** + * File: SizePropertyEditor.h + * Date: 23.3.2018 + *****************************************************************************/ + +#ifndef SIZEPROPERTYEDITOR_H +#define SIZEPROPERTYEDITOR_H + +#include <QLineEdit> + +#include "DualTextPropertyEditor.h" +#include "common/properties/SizeProperty.h" + +namespace grndr +{ + class SizePropertyEditor : public DualTextPropertyEditor<SizeProperty> + { + Q_OBJECT + + public: + SizePropertyEditor(SizeProperty* property, QWidget *parent = nullptr); + + protected: + virtual void applyPropertyValue() override; + + protected: + virtual void editingFinished() override; + }; +} + +#endif diff --git a/Grinder/ui/property/editors/TextPropertyEditor.cpp b/Grinder/ui/property/editors/TextPropertyEditor.cpp new file mode 100644 index 0000000000000000000000000000000000000000..4e5532ea311836f9bba52d0aef9302a594a37c3e --- /dev/null +++ b/Grinder/ui/property/editors/TextPropertyEditor.cpp @@ -0,0 +1,28 @@ +/****************************************************************************** + * File: TextPropertyEditor.cpp + * Date: 07.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "TextPropertyEditor.h" + +TextPropertyEditor::TextPropertyEditor(PropertyBase* property, QString inputMask, QWidget* parent) : PropertyEditor(property, parent) +{ + if (!inputMask.isEmpty()) + setValidator(new QRegExpValidator{QRegExp{inputMask}}); + + applyPropertyValue(); + + // Update the property whenever editing has been finished + connect(this, &QLineEdit::editingFinished, this, &TextPropertyEditor::editingFinished); +} + +void TextPropertyEditor::applyPropertyValue() +{ + setText(getPropertyValue()); +} + +void TextPropertyEditor::editingFinished() +{ + setPropertyValue(text()); +} diff --git a/Grinder/ui/property/editors/TextPropertyEditor.h b/Grinder/ui/property/editors/TextPropertyEditor.h new file mode 100644 index 0000000000000000000000000000000000000000..35179e6ff97a772f513edd13e635d7d57d47d404 --- /dev/null +++ b/Grinder/ui/property/editors/TextPropertyEditor.h @@ -0,0 +1,29 @@ +/****************************************************************************** + * File: TextPropertyEditor.h + * Date: 07.3.2018 + *****************************************************************************/ + +#ifndef TEXTPROPERTYEDITOR_H +#define TEXTPROPERTYEDITOR_H + +#include "ui/widget/AutoFocusLineEdit.h" +#include "ui/property/PropertyEditor.h" + +namespace grndr +{ + class TextPropertyEditor : public PropertyEditor<AutoFocusLineEdit> + { + Q_OBJECT + + public: + TextPropertyEditor(PropertyBase* property, QString inputMask, QWidget *parent = nullptr); + + protected: + virtual void applyPropertyValue() override; + + private slots: + void editingFinished(); + }; +} + +#endif diff --git a/Grinder/ui/visscene/VisualNode.cpp b/Grinder/ui/visscene/VisualNode.cpp new file mode 100644 index 0000000000000000000000000000000000000000..dfcf513a91b60c2185e517dcfdd034493d9f8d9b --- /dev/null +++ b/Grinder/ui/visscene/VisualNode.cpp @@ -0,0 +1,88 @@ +/****************************************************************************** + * File: VisualNode.cpp + * Date: 21.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "VisualNode.h" +#include "util/UIUtils.h" + +#include <QMenu> + +VisualNode::VisualNode(QGraphicsScene* scene, QGraphicsItem* parent) : QGraphicsObject(parent), + _graphicsScene{scene}, _nodeRect{0, 0, 1, 1}, _nodeRectSelected{0, 0, 1, 1} +{ + if (!scene) + throw std::invalid_argument{_EXCPT("scene may not be null")}; + + setFlags(ItemIsFocusable|ItemIsSelectable|ItemSendsGeometryChanges); +} + +QRectF VisualNode::boundingRect(bool selectedRect) const +{ + if (selectedRect) + return _nodeRectSelected; + else + return _nodeRect; +} + +QVariant VisualNode::itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant& value) +{ + if (change == QGraphicsItem::ItemSelectedChange) + { + prepareGeometryChange(); + + if (value == true) + { + setFocus(); + emit nodeSelected(this); + } + else + emit nodeDeselected(this); + } + else if (change == QGraphicsItem::ItemPositionHasChanged) + { + emit nodeMoved(this); + } + + return QGraphicsItem::itemChange(change, value); +} + +void VisualNode::contextMenuEvent(QGraphicsSceneContextMenuEvent* event) +{ + // Select the node exclusively if it hasn't been selected + if (!isSelected()) + { + _graphicsScene->clearSelection(); + setSelected(true); + } + + showContextMenu(event->screenPos()); + + event->accept(); +} + +QAction* VisualNode::createNodeAction(QString name, QString icon, const char* callback, QString help, QString shortcut) +{ + return UIUtils::createAction(this, name, icon, callback, help, shortcut); +} + +void VisualNode::showContextMenu(QPoint pos) const +{ + QMenu menu; + auto actions = (_graphicsScene->selectedItems().size() > 1 ? getNodesActions(menu) : getNodeActions(menu)); + + // If at least one action was provided, create and show a popup menu + if (!actions.empty()) + { + for (auto action : actions) + { + if (action) + menu.addAction(action); + else // A nullptr indicates a separator + menu.addSeparator(); + } + } + + menu.exec(pos); +} diff --git a/Grinder/ui/visscene/VisualNode.h b/Grinder/ui/visscene/VisualNode.h new file mode 100644 index 0000000000000000000000000000000000000000..f88e1ee74d2a698b3ae5c734d2c5a10650930427 --- /dev/null +++ b/Grinder/ui/visscene/VisualNode.h @@ -0,0 +1,52 @@ +/****************************************************************************** + * File: VisualNode.h + * Date: 21.3.2018 + *****************************************************************************/ + +#ifndef VISUALNODE_H +#define VISUALNODE_H + +#include <QGraphicsObject> +#include <QAction> + +namespace grndr +{ + class VisualNode : public QGraphicsObject + { + Q_OBJECT + + public: + VisualNode(QGraphicsScene* scene, QGraphicsItem* parent = nullptr); + + public: + virtual QRectF boundingRect() const override { return boundingRect(isSelected()); } + QRectF boundingRect(bool selectedRect) const; + + signals: + void nodeSelected(VisualNode*); + void nodeDeselected(VisualNode*); + void nodeMoved(VisualNode*); + void nodeGeometryUpdated(VisualNode*); + + protected: + virtual QVariant itemChange(GraphicsItemChange change, const QVariant& value) override; + virtual void contextMenuEvent(QGraphicsSceneContextMenuEvent* event) override; + + protected: + virtual void updateGeometry() { emit nodeGeometryUpdated(this); } + + virtual std::vector<QAction*> getNodeActions(QMenu& menu) const { Q_UNUSED(menu); return {}; } + virtual std::vector<QAction*> getNodesActions(QMenu& menu) const { Q_UNUSED(menu); return {}; } + QAction* createNodeAction(QString name, QString icon = "", const char* callback = nullptr, QString help = "", QString shortcut = ""); + + void showContextMenu(QPoint pos) const; + + protected: + QGraphicsScene* _graphicsScene{nullptr}; + + QRectF _nodeRect; + QRectF _nodeRectSelected; + }; +} + +#endif diff --git a/Grinder/ui/visscene/VisualNodeFactory.h b/Grinder/ui/visscene/VisualNodeFactory.h new file mode 100644 index 0000000000000000000000000000000000000000..d3634860154eb2a9172aafc222484c98368e59eb --- /dev/null +++ b/Grinder/ui/visscene/VisualNodeFactory.h @@ -0,0 +1,51 @@ +/****************************************************************************** + * File: VisualNodeFactory.h + * Date: 21.3.2018 + *****************************************************************************/ + +#ifndef VISUALNODEFACTORY_H +#define VISUALNODEFACTORY_H + +#include <memory> +#include <functional> +#include <map> + +#define REGISTER_NODEFACTORY_TYPE(cls) registerType(cls::type_value, [scene = _scene](const std::shared_ptr<object_type>& object) { return new cls(scene, object); }) + +namespace grndr +{ + template<typename NodeType, typename ObjType, typename KeyType, typename SceneType> + class VisualNodeFactory + { + public: + using node_type = NodeType; + using object_type = ObjType; + using key_type = KeyType; + using scene_type = SceneType; + + private: + using node_creator_type = std::function<node_type*(const std::shared_ptr<object_type>&)>; + + public: + VisualNodeFactory(scene_type* scene); + + public: + void registerType(key_type type, node_creator_type creator); + void unregisterType(key_type type); + + node_type* createNode(const std::shared_ptr<object_type>& object) const; + + protected: + virtual node_type* createDefaultNode(const std::shared_ptr<object_type>& object) const = 0; + + protected: + scene_type* _scene{nullptr}; + + private: + std::map<key_type, node_creator_type> _nodeCreators; + }; +} + +#include "VisualNodeFactory.impl.h" + +#endif diff --git a/Grinder/ui/visscene/VisualNodeFactory.impl.h b/Grinder/ui/visscene/VisualNodeFactory.impl.h new file mode 100644 index 0000000000000000000000000000000000000000..c4391dd2671107fbbe05fd03919a520551aa68eb --- /dev/null +++ b/Grinder/ui/visscene/VisualNodeFactory.impl.h @@ -0,0 +1,59 @@ +/****************************************************************************** + * File: VisualNodeFactory.impl.h + * Date: 21.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "VisualNodeFactory.h" + +template<typename NodeType, typename ObjType, typename KeyType, typename SceneType> +VisualNodeFactory<NodeType, ObjType, KeyType, SceneType>::VisualNodeFactory(scene_type* scene) : + _scene{scene} +{ + if (!scene) + throw std::invalid_argument{_EXCPT("scene may not be null")}; +} + +template<typename NodeType, typename ObjType, typename KeyType, typename SceneType> +void VisualNodeFactory<NodeType, ObjType, KeyType, SceneType>::registerType(key_type type, node_creator_type creator) +{ + if (type == key_type::Undefined) + throw std::invalid_argument{_EXCPT("type may not be Undefined")}; + + if (!creator) + throw std::invalid_argument{_EXCPT("creator may not be null")}; + + _nodeCreators[type] = creator; +} + +template<typename NodeType, typename ObjType, typename KeyType, typename SceneType> +void VisualNodeFactory<NodeType, ObjType, KeyType, SceneType>::unregisterType(key_type type) +{ + _nodeCreators.erase(type); +} + +template<typename NodeType, typename ObjType, typename KeyType, typename SceneType> +NodeType* VisualNodeFactory<NodeType, ObjType, KeyType, SceneType>::createNode(const std::shared_ptr<object_type>& object) const +{ + if (!object) + throw std::invalid_argument{_EXCPT("object may not be null")}; + + key_type type = object->getType(); + + if (type == key_type::Undefined) + throw std::invalid_argument{_EXCPT("type may not be Undefined")}; + + // Create a node for the given object (type); if no creator exists, create a basic GraphBlockNode + if (_nodeCreators.find(type) != _nodeCreators.end()) + { + node_creator_type creator = _nodeCreators.at(type); + node_type* node = creator(object); + + if (!node) + throw std::runtime_error{_EXCPT(QString{"Failed to create an object node of type %1"}.arg(type))}; + + return node; + } + else // Use the default block node type + return createDefaultNode(object); +} diff --git a/Grinder/ui/visscene/VisualScene.h b/Grinder/ui/visscene/VisualScene.h new file mode 100644 index 0000000000000000000000000000000000000000..dbcf84b3707fa6e5d8b70a8fba1c64ac73b3544f --- /dev/null +++ b/Grinder/ui/visscene/VisualScene.h @@ -0,0 +1,56 @@ +/****************************************************************************** + * File: VisualScene.h + * Date: 15.3.2018 + *****************************************************************************/ + +#ifndef VISUALSCENE_H +#define VISUALSCENE_H + +#include <QGraphicsScene> + +namespace grndr +{ + class VisualSceneView; + + template<typename ViewType> + class VisualScene : public QGraphicsScene + { + static_assert(std::is_base_of<VisualSceneView, ViewType>::value, "ViewType must be derived from VisualSceneView"); + + public: + using view_type = ViewType; + + public: + VisualScene(view_type* view, QRectF sceneRect = QRectF{}); + virtual ~VisualScene(); + + public: + template<typename NodeType> + std::vector<NodeType*> getNodes(bool selectedOnly = false) { return _getNodes<NodeType>(selectedOnly); } + template<typename NodeType> + std::vector<const NodeType*> getNodes(bool selectedOnly = false) const; + + public: + view_type* view() { return _view; } + const view_type* view() const { return _view; } + + protected: + virtual void drawBackground(QPainter* painter, const QRectF& rect) override; + + private: + void drawGrid(QPainter* painter, const QRectF& rect, int interval, QColor color); + + private: + template<typename NodeType> + std::vector<NodeType*> _getNodes(bool selectedOnly = false) const; + + protected: + view_type* _view{nullptr}; + + bool _drawGrid{true}; + }; +} + +#include "VisualScene.impl.h" + +#endif diff --git a/Grinder/ui/visscene/VisualScene.impl.h b/Grinder/ui/visscene/VisualScene.impl.h new file mode 100644 index 0000000000000000000000000000000000000000..0585986242b526e601b406846427521ac62aa2fb --- /dev/null +++ b/Grinder/ui/visscene/VisualScene.impl.h @@ -0,0 +1,96 @@ +/****************************************************************************** + * File: VisualScene.impl.h + * Date: 15.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "VisualScene.h" + +template<typename ViewType> +VisualScene<ViewType>::VisualScene(view_type* view, QRectF sceneRect) : QGraphicsScene (sceneRect), + _view{view} +{ + if (!view) + throw std::invalid_argument{_EXCPT("view may not be null")}; + + // Force redraw if the selection has changed + connect(this, SIGNAL(selectionChanged()), this, SLOT(update())); +} + +template<typename ViewType> +VisualScene<ViewType>::~VisualScene() +{ + // Prevent selection change events from firing when a scene is destroyed + disconnect(this, &VisualScene::selectionChanged, nullptr, nullptr); +} + +template<typename ViewType> +template<typename NodeType> +std::vector<const NodeType*> VisualScene<ViewType>::getNodes(bool selectedOnly) const +{ + auto selNodes = _getNodes<NodeType>(selectedOnly); + std::vector<const NodeType*> nodes; + + for (auto node : selNodes) + nodes.push_back(node); + + return nodes; +} + +template<typename ViewType> +template<typename NodeType> +std::vector<NodeType*> VisualScene<ViewType>::_getNodes(bool selectedOnly) const +{ + std::vector<NodeType*> nodes; + + for (auto item : selectedOnly ? selectedItems() : items()) + { + if (auto node = dynamic_cast<NodeType*>(item)) + nodes.push_back(node); + } + + return nodes; +} + +template<typename ViewType> +void VisualScene<ViewType>::drawBackground(QPainter* painter, const QRectF& rect) +{ + QGraphicsScene::drawBackground(painter, rect); + + if (_drawGrid) + { + auto gridStyle = _view->sceneStyle().getGridStyle(); + + // Draw small grid (only if not zoomed in too much) + if (_view->getZoom() < gridStyle.size * gridStyle.maxZoomFactor) + drawGrid(painter, rect, gridStyle.size, gridStyle.colorLight); + + // Draw large grid + drawGrid(painter, rect, gridStyle.size * gridStyle.darkGridInterval, gridStyle.colorDark); + } +} + +template<typename ViewType> +void VisualScene<ViewType>::drawGrid(QPainter* painter, const QRectF& rect, int interval, QColor color) +{ + QVector<QLineF> lines; + + qreal x = static_cast<int>(rect.left()) - (static_cast<int>(rect.left()) % interval);; + + while (x < rect.right()) + { + lines << QLineF{x, rect.top(), x, rect.bottom()}; + x += interval; + } + + qreal y = static_cast<int>(rect.top()) - (static_cast<int>(rect.top()) % interval);; + + while (y < rect.bottom()) + { + lines << QLineF{rect.left(), y, rect.right(), y}; + y += interval; + } + + painter->setPen(QPen{color}); + painter->drawLines(lines); +} diff --git a/Grinder/ui/visscene/VisualSceneInputHandler.cpp b/Grinder/ui/visscene/VisualSceneInputHandler.cpp new file mode 100644 index 0000000000000000000000000000000000000000..f167c754ea05945d707e9a8f5248508690483cae --- /dev/null +++ b/Grinder/ui/visscene/VisualSceneInputHandler.cpp @@ -0,0 +1,47 @@ +/****************************************************************************** + * File: VisualSceneInputHandler.cpp + * Date: 28.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "VisualSceneInputHandler.h" + +VisualSceneInputHandler::InputEventResult VisualSceneInputHandler::handleMousePressEvent(QGraphicsSceneMouseEvent* event) +{ + if (event->button() == Qt::LeftButton && !event->modifiers().testFlag(Qt::ShiftModifier)) // Shift-clicks should always be passed to the parent (to support shift-dragging of the scene) + { + _mouseButtonPressed = true; + return handleInputEvent<QGraphicsSceneMouseEvent>(event, &VisualSceneInputHandler::mousePressed); + } + else + return InputEventResult::Ignore; +} + +VisualSceneInputHandler::InputEventResult VisualSceneInputHandler::handleMouseMoveEvent(QGraphicsSceneMouseEvent* event) +{ + if (_mouseButtonPressed) + return handleInputEvent<QGraphicsSceneMouseEvent>(event, &VisualSceneInputHandler::mouseMoved); + else + return InputEventResult::Ignore; +} + +VisualSceneInputHandler::InputEventResult VisualSceneInputHandler::handleMouseReleaseEvent(QGraphicsSceneMouseEvent* event) +{ + if (event->button() == Qt::LeftButton) + { + _mouseButtonPressed = false; + return handleInputEvent<QGraphicsSceneMouseEvent>(event, &VisualSceneInputHandler::mouseReleased); + } + else + return InputEventResult::Ignore; +} + +VisualSceneInputHandler::InputEventResult VisualSceneInputHandler::handleKeyPressEvent(QKeyEvent* event) +{ + return handleInputEvent<QKeyEvent>(event, &VisualSceneInputHandler::keyPressed); +} + +VisualSceneInputHandler::InputEventResult VisualSceneInputHandler::handleKeyReleaseEvent(QKeyEvent* event) +{ + return handleInputEvent<QKeyEvent>(event, &VisualSceneInputHandler::keyReleased); +} diff --git a/Grinder/ui/visscene/VisualSceneInputHandler.h b/Grinder/ui/visscene/VisualSceneInputHandler.h new file mode 100644 index 0000000000000000000000000000000000000000..8300c8eb542ab393cab231293c4648f050241584 --- /dev/null +++ b/Grinder/ui/visscene/VisualSceneInputHandler.h @@ -0,0 +1,51 @@ +/****************************************************************************** + * File: VisualSceneInputHandler.h + * Date: 28.3.2018 + *****************************************************************************/ + +#ifndef VISUALSCENEINPUTHANDLER_H +#define VISUALSCENEINPUTHANDLER_H + +#include <QGraphicsSceneMouseEvent> +#include <QKeyEvent> +#include <functional> + +namespace grndr +{ + class VisualSceneInputHandler + { + public: + enum class InputEventResult + { + Ignore, + Process, + }; + + public: + InputEventResult handleMousePressEvent(QGraphicsSceneMouseEvent* event); + InputEventResult handleMouseMoveEvent(QGraphicsSceneMouseEvent* event); + InputEventResult handleMouseReleaseEvent(QGraphicsSceneMouseEvent* event); + + InputEventResult handleKeyPressEvent(QKeyEvent* event); + InputEventResult handleKeyReleaseEvent(QKeyEvent* event); + + protected: + virtual InputEventResult mousePressed(const QGraphicsSceneMouseEvent* event) { Q_UNUSED(event); return InputEventResult::Ignore; } + virtual InputEventResult mouseMoved(const QGraphicsSceneMouseEvent* event) { Q_UNUSED(event); return InputEventResult::Ignore; } + virtual InputEventResult mouseReleased(const QGraphicsSceneMouseEvent* event) { Q_UNUSED(event); return InputEventResult::Ignore; } + + virtual InputEventResult keyPressed(const QKeyEvent* event) { Q_UNUSED(event); return InputEventResult::Ignore; } + virtual InputEventResult keyReleased(const QKeyEvent* event) { Q_UNUSED(event); return InputEventResult::Ignore; } + + private: + template<typename EventType> + InputEventResult handleInputEvent(EventType* event, std::function<InputEventResult(VisualSceneInputHandler*, const EventType*)> handler); + + private: + bool _mouseButtonPressed{false}; + }; +} + +#include "VisualSceneInputHandler.impl.h" + +#endif diff --git a/Grinder/ui/visscene/VisualSceneInputHandler.impl.h b/Grinder/ui/visscene/VisualSceneInputHandler.impl.h new file mode 100644 index 0000000000000000000000000000000000000000..04febc1d6a1dbc07b4142dada1a241d3644be368 --- /dev/null +++ b/Grinder/ui/visscene/VisualSceneInputHandler.impl.h @@ -0,0 +1,15 @@ +/****************************************************************************** + * File: VisualSceneInputHandler.impl.h + * Date: 30.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "VisualSceneInputHandler.h" + +template<typename EventType> +VisualSceneInputHandler::InputEventResult VisualSceneInputHandler::handleInputEvent(EventType* event, std::function<InputEventResult(VisualSceneInputHandler*, const EventType*)> handler) +{ + InputEventResult result = handler(this, event); + event->setAccepted(result == InputEventResult::Process); + return result; +} diff --git a/Grinder/ui/visscene/VisualSceneStyle.cpp b/Grinder/ui/visscene/VisualSceneStyle.cpp new file mode 100644 index 0000000000000000000000000000000000000000..39c6bc421cd9278657539a70d72ae75591523418 --- /dev/null +++ b/Grinder/ui/visscene/VisualSceneStyle.cpp @@ -0,0 +1,7 @@ +/****************************************************************************** + * File: VisualSceneStyle.cpp + * Date: 15.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "VisualSceneStyle.h" diff --git a/Grinder/ui/visscene/VisualSceneStyle.h b/Grinder/ui/visscene/VisualSceneStyle.h new file mode 100644 index 0000000000000000000000000000000000000000..8750adb3de16adc56f3aaeb2b0d216b255c41f9b --- /dev/null +++ b/Grinder/ui/visscene/VisualSceneStyle.h @@ -0,0 +1,47 @@ +/****************************************************************************** + * File: VisualSceneStyle.h + * Date: 15.3.2018 + *****************************************************************************/ + +#ifndef VISUALSCENESTYLE_H +#define VISUALSCENESTYLE_H + +#include <QSizeF> +#include <QColor> + +namespace grndr +{ + class VisualSceneStyle + { + public: + struct ViewStyle + { + QSizeF defaultSceneSize{0.0f, 0.0f}; + + float zoomFactor{1.1}; + float minZoomLevel{0.2}; + float maxZoomLevel{5.0}; + }; + + struct GridStyle + { + int size{20}; + + QColor colorDark{200, 200, 200}; + QColor colorLight{230, 230, 230}; + int darkGridInterval{5}; + + float maxZoomFactor{0.1f}; + }; + + public: + const ViewStyle& getViewStyle() const { return _viewStyle; } + const GridStyle& getGridStyle() const { return _gridStyle; } + + protected: + ViewStyle _viewStyle; + GridStyle _gridStyle; + }; +} + +#endif diff --git a/Grinder/ui/visscene/VisualSceneView.cpp b/Grinder/ui/visscene/VisualSceneView.cpp new file mode 100644 index 0000000000000000000000000000000000000000..cb036364bbe7ce9b21d811b5447f8be8bcb34f79 --- /dev/null +++ b/Grinder/ui/visscene/VisualSceneView.cpp @@ -0,0 +1,229 @@ +/****************************************************************************** + * File: VisualSceneView.cpp + * Date: 15.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "VisualSceneView.h" +#include "VisualScene.h" +#include "util/UIUtils.h" +#include "res/Resources.h" + +VisualSceneView::VisualSceneView(QWidget* parent) : MetaWidget(parent) +{ + setInteractive(true); + + setDragMode(QGraphicsView::RubberBandDrag); + setTransformationAnchor(QGraphicsView::AnchorUnderMouse); + setViewportUpdateMode(QGraphicsView::FullViewportUpdate); + + // Create view actions + _selectAllAction = UIUtils::createAction(this, "Select &all", FILE_ICON_SELECTALL, SLOT(selectAllItems()), "Select all items", "Ctrl+A"); + _deleteSelectedAction = UIUtils::createAction(this, "&Delete selected items", FILE_ICON_DELETE_SELECTED, SLOT(removeSelectedItems()), "Delete the selected items", "Del"); + + _zoomInAction = UIUtils::createAction(this, "Zoom &in", FILE_ICON_ZOOMIN, SLOT(zoomIn()), "Zoom the view in", "Ctrl++"); + _zoomOutAction = UIUtils::createAction(this, "Zoom &out", FILE_ICON_ZOOMOUT, SLOT(zoomOut()), "Zoom the view out", "Ctrl+-"); + _zoomFullAction = UIUtils::createAction(this, "Zoom to &100%", FILE_ICON_ZOOMFULL, SLOT(zoomFull()), "Zoom the view to 100%", "Ctrl+Shift+A"); +} + +void VisualSceneView::setScene(QGraphicsScene* scene) +{ + if (_scene) + disconnect(_scene, &QGraphicsScene::selectionChanged, this, &VisualSceneView::updateActions); + + _scene = scene; + QGraphicsView::setScene(scene); + + // Listen for selection changes in order to update the view actions + if (_scene) + connect(_scene, &QGraphicsScene::selectionChanged, this, &VisualSceneView::updateActions); + + updateActions(); + centerOn(0, 0); +} + +qreal VisualSceneView::getZoom() const +{ + return transform().m11(); +} + +void VisualSceneView::setZoom(qreal zoomVal) +{ + zoom(zoomVal / getZoom()); +} + +void VisualSceneView::zoom(qreal factor) +{ + auto zoom = getZoom() * factor; + + // Keep zoom in range + if (zoom > sceneStyle().getViewStyle().maxZoomLevel) + factor = (1.0 / getZoom()) * sceneStyle().getViewStyle().maxZoomLevel; + else if (zoom < sceneStyle().getViewStyle().minZoomLevel) + factor = (1.0 / getZoom()) * sceneStyle().getViewStyle().minZoomLevel; + + scale(factor, factor); + updateActions(); + + emit zoomChanged(getZoom()); +} + +void VisualSceneView::updateActions() +{ + _selectAllAction->setEnabled(_scene && !scene()->items().isEmpty()); + _deleteSelectedAction->setEnabled(_scene && !scene()->selectedItems().isEmpty()); + + auto zoom = getZoom(); + + _zoomInAction->setEnabled(_scene && (zoom < sceneStyle().getViewStyle().maxZoomLevel)); + _zoomOutAction->setEnabled(_scene && (zoom > sceneStyle().getViewStyle().minZoomLevel)); + _zoomFullAction->setEnabled(_scene && (zoom != 1.0)); +} + +void VisualSceneView::fitToWindow(QGraphicsItem* item) +{ + if (item) + { + fitInView(item, Qt::KeepAspectRatio); + } + else + { + if (_scene) + fitInView(_scene->sceneRect(), Qt::KeepAspectRatio); + } + + emit zoomChanged(getZoom()); + updateActions(); +} + +void VisualSceneView::selectAllItems() const +{ + if (_scene) + { + for (auto item : _scene->items()) + item->setSelected(true); + } +} + +void VisualSceneView::moveSelectedItems(QPoint delta) +{ + if (_scene) + { + for (auto item : _scene->selectedItems()) + item->setPos(item->pos() + delta); + } +} + +void VisualSceneView::wheelEvent(QWheelEvent* event) +{ + // Only zoom if CTRL is pressed + if (!(event->modifiers() & Qt::ControlModifier)) + { + QGraphicsView::wheelEvent(event); + return; + } + + auto oldZoomPos = mapToScene(event->pos()); + + if (event->angleDelta().y() > 0) + zoom(sceneStyle().getViewStyle().zoomFactor); + else + zoom(1.0f / sceneStyle().getViewStyle().zoomFactor); + + auto trans = mapToScene(event->pos()) - oldZoomPos; + translate(trans.x(), trans.y()); + + event->accept(); +} + +void VisualSceneView::keyPressEvent(QKeyEvent* event) +{ + if (event->key() == Qt::Key_Shift) // When holding shift, the view can be dragged around + { + setDragMode(QGraphicsView::ScrollHandDrag); + setInteractive(false); + return; + } + else if (event->key() == Qt::Key_Escape) // Deselect all items when pressing Esc (but do not suppress event propagation) + { + if (_scene) + _scene->clearSelection(); + } + else if (event->key() == Qt::Key_Up || event->key() == Qt::Key_Down || event->key() == Qt::Key_Left || event->key() == Qt::Key_Right) // Enable item movement via cursor keys + { + QPoint delta; + int offset = 1; + + if (event->modifiers().testFlag(Qt::ControlModifier)) + { + if (event->modifiers().testFlag(Qt::AltModifier)) + offset = 25; + else + offset = 10; + } + + switch (event->key()) + { + case Qt::Key_Left: + delta.setX(-offset); + break; + + case Qt::Key_Right: + delta.setX(offset); + break; + + case Qt::Key_Up: + delta.setY(-offset); + break; + + case Qt::Key_Down: + delta.setY(offset); + break; + + default: + break; + } + + if (!delta.isNull()) + { + moveSelectedItems(delta); + return; + } + } + + QGraphicsView::keyPressEvent(event); +} + +void VisualSceneView::keyReleaseEvent(QKeyEvent* event) +{ + if (event->key() == Qt::Key_Shift) // When release shift, revert back to rubber band selection + { + setDragMode(QGraphicsView::RubberBandDrag); + setInteractive(true); + return; + } + + QGraphicsView::keyReleaseEvent(event); +} + +void VisualSceneView::mouseMoveEvent(QMouseEvent* event) +{ + // Just in case someone switched applications while holding shift + if (!QApplication::queryKeyboardModifiers().testFlag(Qt::ShiftModifier)) + { + setDragMode(QGraphicsView::RubberBandDrag); + setInteractive(true); + } + + QGraphicsView::mouseMoveEvent(event); +} + +void VisualSceneView::contextMenuEvent(QContextMenuEvent* event) +{ + event->setAccepted(false); + QGraphicsView::contextMenuEvent(event); + + // Only show the global popup if we didn't right-click a node + if (_scene && !event->isAccepted()) + widget_type::contextMenuEvent(event); +} diff --git a/Grinder/ui/visscene/VisualSceneView.h b/Grinder/ui/visscene/VisualSceneView.h new file mode 100644 index 0000000000000000000000000000000000000000..e540c18265d844d65c624681662ec1024f26dee7 --- /dev/null +++ b/Grinder/ui/visscene/VisualSceneView.h @@ -0,0 +1,69 @@ +/****************************************************************************** + * File: VisualSceneView.h + * Date: 15.3.2018 + *****************************************************************************/ + +#ifndef VISUALSCENEVIEW_H +#define VISUALSCENEVIEW_H + +#include <QGraphicsView> + +#include "ui/widget/MetaWidget.h" +#include "VisualSceneStyle.h" + +namespace grndr +{ + class VisualSceneView : public MetaWidget<QGraphicsView> + { + Q_OBJECT + + public: + VisualSceneView(QWidget* parent = nullptr); + + public: + void setScene(QGraphicsScene* scene); + + qreal getZoom() const; + void setZoom(qreal zoomVal); + void zoom(qreal factor); + + public slots: + virtual void selectAllItems() const; + virtual void moveSelectedItems(QPoint delta); + virtual void removeSelectedItems() const { } + + void zoomIn() { zoom(sceneStyle().getViewStyle().zoomFactor); } + void zoomOut() { zoom(1.0 / sceneStyle().getViewStyle().zoomFactor); } + void zoomFull() { setZoom(1.0); } + void fitToWindow(QGraphicsItem* item = nullptr); + + public: + virtual const VisualSceneStyle& sceneStyle() const = 0; + + signals: + void zoomChanged(qreal); + + protected: + virtual void contextMenuEvent(QContextMenuEvent* event) override; + virtual void wheelEvent(QWheelEvent* event) override; + virtual void keyPressEvent(QKeyEvent* event) override; + virtual void keyReleaseEvent(QKeyEvent* event) override; + virtual void mouseMoveEvent(QMouseEvent* event) override; + + protected slots: + virtual void updateActions(); + + protected: + QGraphicsScene* _scene{nullptr}; + + protected: + QAction* _selectAllAction{nullptr}; + QAction* _deleteSelectedAction{nullptr}; + + QAction* _zoomInAction{nullptr}; + QAction* _zoomOutAction{nullptr}; + QAction* _zoomFullAction{nullptr}; + }; +} + +#endif diff --git a/Grinder/ui/widget/AutoFocusLineEdit.cpp b/Grinder/ui/widget/AutoFocusLineEdit.cpp new file mode 100644 index 0000000000000000000000000000000000000000..024471be7565bdf1ea37824614fbb9e63ead7ba5 --- /dev/null +++ b/Grinder/ui/widget/AutoFocusLineEdit.cpp @@ -0,0 +1,13 @@ +/****************************************************************************** + * File: AutoFocusLineEdit.cpp + * Date: 23.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "AutoFocusLineEdit.h" + +void AutoFocusLineEdit::focusInEvent(QFocusEvent* event) +{ + QLineEdit::focusInEvent(event); + QTimer::singleShot(0, [this]() { selectAll(); }); +} diff --git a/Grinder/ui/widget/AutoFocusLineEdit.h b/Grinder/ui/widget/AutoFocusLineEdit.h new file mode 100644 index 0000000000000000000000000000000000000000..934d4ca74def3710c288aa4107d816e1c8dddfc3 --- /dev/null +++ b/Grinder/ui/widget/AutoFocusLineEdit.h @@ -0,0 +1,25 @@ +/****************************************************************************** + * File: AutoFocusLineEdit.h + * Date: 23.3.2018 + *****************************************************************************/ + +#ifndef AUTOFOCUSLINEEDIT_H +#define AUTOFOCUSLINEEDIT_H + +#include <QLineEdit> + +namespace grndr +{ + class AutoFocusLineEdit : public QLineEdit + { + Q_OBJECT + + public: + using QLineEdit::QLineEdit; + + protected: + virtual void focusInEvent(QFocusEvent* event) override; + }; +} + +#endif diff --git a/Grinder/ui/widget/ColorWidget.cpp b/Grinder/ui/widget/ColorWidget.cpp new file mode 100644 index 0000000000000000000000000000000000000000..9ae76c4aa3c30493231db0e22d5cf497bd490879 --- /dev/null +++ b/Grinder/ui/widget/ColorWidget.cpp @@ -0,0 +1,120 @@ +/****************************************************************************** + * File: ColorWidget.cpp + * Date: 24.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ColorWidget.h" + +ColorWidget::ColorWidget(QWidget* parent) : QWidget(parent), + _color{255, 255, 255} +{ + setFocusPolicy(Qt::NoFocus); + updateWidget(); +} + +ColorWidget::ColorWidget(Flags flags, QWidget* parent) : ColorWidget(parent) +{ + _flags = flags; +} + +QSize ColorWidget::sizeHint() const +{ + // Hardcoded size hints + if (_flags.testFlag(Flag::SmallBox)) + return QSize{22, 15}; + else + return QSize{50, 35}; +} + +void ColorWidget::paintEvent(QPaintEvent* event) +{ + Q_UNUSED(event); + + QRect rc = rect() + QMargins{0, 0, -1, -1}; + QPainter painter{this}; + painter.setRenderHints(QPainter::Antialiasing, false); + + // Draw the background + QPen pen{Qt::black, 1}; + painter.setPen(pen); + + painter.setBrush(Qt::white); + painter.drawRect(rc); + + // Draw the current color(s) + rc -= QMargins{2, 2, 1, 1}; + painter.fillRect(rc, _color); + + if (_showColorComp) + { + rc.setLeft(rc.left() + std::ceil(rc.width() / 2.0)); + painter.fillRect(rc, _colorComp); + } +} + +void ColorWidget::mouseReleaseEvent(QMouseEvent* event) +{ + if (event->button() == Qt::LeftButton && event->modifiers() == 0) + { + emit colorClicked(_color); + + if (!_flags.testFlag(Flag::SelectColorOnDoubleClick)) + { + selectColor(); + return; + } + } + + QWidget::mouseReleaseEvent(event); +} + +void ColorWidget::mouseDoubleClickEvent(QMouseEvent* event) +{ + if (event->button() == Qt::LeftButton && event->modifiers() == 0) + { + if (_flags.testFlag(Flag::SelectColorOnDoubleClick)) + { + selectColor(); + return; + } + } + + QWidget::mouseDoubleClickEvent(event); +} + +void ColorWidget::selectColor() +{ + bool prevShowColorComp = _showColorComp; + QColor prevColorComp = _colorComp; + + setColorComp(true, _color); + + QColorDialog colorDlg{_color}; + colorDlg.setOptions(QColorDialog::DontUseNativeDialog); + connect(&colorDlg, SIGNAL(currentColorChanged(const QColor&)), this, SLOT(colorDialogColorChanged(const QColor&))); + + if (colorDlg.exec() == QDialog::Accepted) + { + setColor(colorDlg.currentColor()); + emit colorChanged(_color, true); + } + + setColorComp(prevShowColorComp, prevColorComp); +} + +void ColorWidget::updateWidget() +{ + QString toolTip = QString{"R: %1; G: %2; B: %3<br>%4"}.arg(_color.red()).arg(_color.green()).arg(_color.blue()).arg(_color.name()); + + if (!_extraToolTip.isEmpty()) + toolTip.insert(0, QString{"%1<br>"}.arg(_extraToolTip)); + + setToolTip(toolTip); + update(); +} + +void ColorWidget::colorDialogColorChanged(const QColor& color) +{ + setColorComp(true, color); +} diff --git a/Grinder/ui/widget/ColorWidget.h b/Grinder/ui/widget/ColorWidget.h new file mode 100644 index 0000000000000000000000000000000000000000..b2eed7f8bda73ee4a282640e20408dc13a8f284f --- /dev/null +++ b/Grinder/ui/widget/ColorWidget.h @@ -0,0 +1,74 @@ +/****************************************************************************** + * File: ColorWidget.h + * Date: 24.3.2018 + *****************************************************************************/ + +#ifndef COLORWIDGET_H +#define COLORWIDGET_H + +#include <QWidget> + +namespace grndr +{ + class ColorWidget : public QWidget + { + Q_OBJECT + + public: + enum class Flag + { + NoFlag = 0x0000, + SmallBox = 0x0001, + SelectColorOnDoubleClick = 0x0002, + }; + + Q_DECLARE_FLAGS(Flags, Flag) + + public: + ColorWidget(QWidget* parent = nullptr); + ColorWidget(Flags flags, QWidget* parent = nullptr); + + public: + QColor getColor() const { return _color; } + void setColor(QColor color) { _color = color; updateWidget(); emit colorChanged(color, false); } + std::pair<bool, QColor> getColorComp() const { return std::make_pair(_showColorComp, _colorComp); } + void setColorComp(bool show, QColor color = QColor{}) { _showColorComp = show; _colorComp = color; updateWidget(); } + + QString getExtraToolTip() const { return _extraToolTip; } + void setExtraToolTip(QString toolTip) { _extraToolTip = toolTip; updateWidget(); } + + public: + virtual QSize sizeHint() const override; + + signals: + void colorChanged(QColor, bool); + void colorClicked(QColor); + + protected: + virtual void paintEvent(QPaintEvent* event) override; + virtual void mouseReleaseEvent(QMouseEvent* event) override; + virtual void mouseDoubleClickEvent(QMouseEvent* event) override; + + private: + void selectColor(); + + void updateWidget(); + + private slots: + void colorDialogColorChanged(const QColor& color); + + private: + Flags _flags{Flag::NoFlag}; + + QColor _color; + + bool _showColorComp{false}; + QColor _colorComp; + + QString _extraToolTip{""}; + }; +} + +Q_DECLARE_OPERATORS_FOR_FLAGS(grndr::ColorWidget::Flags) + +#endif diff --git a/Grinder/ui/widget/ControlBar.cpp b/Grinder/ui/widget/ControlBar.cpp new file mode 100644 index 0000000000000000000000000000000000000000..16ce3dfa9a5178261996300a2ca4063c18951b57 --- /dev/null +++ b/Grinder/ui/widget/ControlBar.cpp @@ -0,0 +1,58 @@ +/****************************************************************************** + * File: ControlBar.cpp + * Date: 07.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ControlBar.h" +#include "ui/StyleSheet.h" +#include "res/Resources.h" + +ControlBar::ControlBar(QWidget *parent) : QFrame(parent), + _layout{new QHBoxLayout{this}} +{ + _layout->setContentsMargins(2, 2, 2, 2); + _layout->addStretch(); + + setStyleSheet(StyleSheet::loadStyleSheet(FILE_STYLESHEET_CONTROLBAR)); +} + +void ControlBar::addWidget(QWidget* widget, Qt::Alignment alignment) +{ + if (alignment == 0) + alignment = _defaultAlignment; + + if (alignment == Qt::AlignRight) + _layout->addWidget(widget); + else if (alignment == Qt::AlignLeft) + _layout->insertWidget(_layout->count() - 1, widget); + + setMinimumSize(0, sizeHint().height()); +} + +void ControlBar::addAction(QAction* action, Qt::ToolButtonStyle buttonStyle, Qt::Alignment alignment) +{ + if (buttonStyle == Qt::ToolButtonFollowStyle) + buttonStyle = _defaultToolButtonStyle; + + if (action->data().isValid()) // Actions can specify a tool button style in their data field + buttonStyle = static_cast<Qt::ToolButtonStyle>(action->data().toInt()); + + auto toolButton = new QToolButton{}; + + toolButton->setDefaultAction(action); + toolButton->setToolButtonStyle(buttonStyle); + toolButton->setAutoRaise(true); + + addWidget(toolButton, alignment); +} + +void ControlBar::addSeparator(Qt::Alignment alignment) +{ + auto line = new QFrame{}; + + line->setFrameShape(QFrame::VLine); + line->setFrameShadow(QFrame::Plain); + + addWidget(line, alignment); +} diff --git a/Grinder/ui/widget/ControlBar.h b/Grinder/ui/widget/ControlBar.h new file mode 100644 index 0000000000000000000000000000000000000000..ea161a4d1cc9a0045d594025b1a0f47cf37eef5b --- /dev/null +++ b/Grinder/ui/widget/ControlBar.h @@ -0,0 +1,38 @@ +/****************************************************************************** + * File: ControlBar.h + * Date: 07.2.2018 + *****************************************************************************/ + +#ifndef CONTROLBAR_H +#define CONTROLBAR_H + +#include <QFrame> +#include <QHBoxLayout> + +namespace grndr +{ + class ControlBar : public QFrame + { + Q_OBJECT + + public: + ControlBar(QWidget *parent = nullptr); + + public: + void addWidget(QWidget* widget, Qt::Alignment alignment = 0); + void addAction(QAction* action, Qt::ToolButtonStyle buttonStyle = Qt::ToolButtonFollowStyle, Qt::Alignment alignment = 0); + void addSeparator(Qt::Alignment alignment = 0); + + public: + void setDefaultAlignment(Qt::Alignment alignment) { _defaultAlignment = alignment; } + void setDefaultToolButtonStyle(Qt::ToolButtonStyle style) { _defaultToolButtonStyle = style; } + + private: + QHBoxLayout* _layout{nullptr}; + + Qt::Alignment _defaultAlignment{Qt::AlignLeft}; + Qt::ToolButtonStyle _defaultToolButtonStyle{Qt::ToolButtonTextBesideIcon}; + }; +} + +#endif diff --git a/Grinder/ui/widget/GrinderDockWidget.cpp b/Grinder/ui/widget/GrinderDockWidget.cpp new file mode 100644 index 0000000000000000000000000000000000000000..e0832512e8e9c4eb6fb2a8917d1e19c96b781d7a --- /dev/null +++ b/Grinder/ui/widget/GrinderDockWidget.cpp @@ -0,0 +1,15 @@ +/****************************************************************************** + * File: GrinderDockWidget.cpp + * Date: 24.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "GrinderDockWidget.h" + +void GrinderDockWidget::closeEvent(QCloseEvent* event) +{ + if (!features().testFlag(QDockWidget::DockWidgetClosable)) + event->setAccepted(!event->spontaneous()); + else + QDockWidget::closeEvent(event); +} diff --git a/Grinder/ui/widget/GrinderDockWidget.h b/Grinder/ui/widget/GrinderDockWidget.h new file mode 100644 index 0000000000000000000000000000000000000000..1a1653ee4e17638f88ab32e01271853e0aba202b --- /dev/null +++ b/Grinder/ui/widget/GrinderDockWidget.h @@ -0,0 +1,25 @@ +/****************************************************************************** + * File: GrinderDockWidget.h + * Date: 24.3.2018 + *****************************************************************************/ + +#ifndef GRINDERDOCKWIDGET_H +#define GRINDERDOCKWIDGET_H + +#include <QDockWidget> + +namespace grndr +{ + class GrinderDockWidget : public QDockWidget + { + Q_OBJECT + + public: + using QDockWidget::QDockWidget; + + protected: + virtual void closeEvent(QCloseEvent* event) override; + }; +} + +#endif diff --git a/Grinder/ui/widget/MetaWidget.h b/Grinder/ui/widget/MetaWidget.h new file mode 100644 index 0000000000000000000000000000000000000000..305618b0f47f5faa129e1976092e6883fd317ef0 --- /dev/null +++ b/Grinder/ui/widget/MetaWidget.h @@ -0,0 +1,50 @@ +/****************************************************************************** + * File: MetaWidget.h + * Date: 08.2.2018 + *****************************************************************************/ + +#ifndef METAWIDGET_H +#define METAWIDGET_H + +#include <QMenu> +#include <memory> + +namespace grndr +{ + template<typename Base> + class MetaWidget : public Base + { + public: + using widget_type = MetaWidget<Base>; + using base_type = Base; + + using Base::Base; + + public: + enum class AddActionsMode + { + MainMenu, + ContextMenu, + Toolbar, + }; + + public: + template<typename T> + int setupActions(T* widget, AddActionsMode mode) const; + + template<typename MenuType, typename BarType> + void setupUi(MenuType* menu, BarType* toolbar) const; + + protected: + virtual void contextMenuEvent(QContextMenuEvent*event) override; + + protected: + virtual std::unique_ptr<QMenu> createContextMenu() const; + + virtual std::vector<QAction*> getActions(AddActionsMode mode) const { Q_UNUSED(mode); return {}; } + }; +} + +#include "MetaWidget.impl.h" + +#endif diff --git a/Grinder/ui/widget/MetaWidget.impl.h b/Grinder/ui/widget/MetaWidget.impl.h new file mode 100644 index 0000000000000000000000000000000000000000..d4d462a7437eaf430572f5e5f394db4d8dd2dfcf --- /dev/null +++ b/Grinder/ui/widget/MetaWidget.impl.h @@ -0,0 +1,56 @@ +/****************************************************************************** + * File: MetaWidget.impl.h + * Date: 08.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "MetaWidget.h" + +template<typename Base> +template<typename T> +int MetaWidget<Base>::setupActions(T* widget, AddActionsMode mode) const +{ + auto actions = getActions(mode); + + for (auto action : actions) + { + if (action) + widget->addAction(action); + else + widget->addSeparator(); + } + + return actions.size(); +} + +template<typename Base> +template<typename MenuType, typename BarType> +void MetaWidget<Base>::setupUi(MenuType* menu, BarType* toolbar) const +{ + // Add actions to the provided widgets + if (menu) + setupActions(menu, AddActionsMode::MainMenu); + + if (toolbar) + setupActions(toolbar, AddActionsMode::Toolbar); +} + +template<typename Base> +std::unique_ptr<QMenu> MetaWidget<Base>::createContextMenu() const +{ + auto menu = std::make_unique<QMenu>(); + setupActions(menu.get(), AddActionsMode::ContextMenu); + return menu; +} + +template<typename Base> +void MetaWidget<Base>::contextMenuEvent(QContextMenuEvent* event) +{ + auto menu = createContextMenu(); + + if (menu) + { + menu->exec(event->globalPos()); + event->accept(); + } +} diff --git a/Grinder/ui/widget/ObjectListItem.h b/Grinder/ui/widget/ObjectListItem.h new file mode 100644 index 0000000000000000000000000000000000000000..234e13a75e39841f3a06a01415d0724ca78ac985 --- /dev/null +++ b/Grinder/ui/widget/ObjectListItem.h @@ -0,0 +1,45 @@ +/****************************************************************************** + * File: ObjectListItem.h + * Date: 10.2.2018 + *****************************************************************************/ + +#ifndef OBJECTLISTITEM_H +#define OBJECTLISTITEM_H + +#include <QListWidgetItem> + +namespace grndr +{ + template<typename ObjectType> + class ObjectListItem : public QListWidgetItem + { + public: + using base_type = ObjectListItem<ObjectType>; + + public: + ObjectListItem(ObjectType* object); + + public: + virtual void updateItem(); + + public: + ObjectType* object() { return _object; } + const ObjectType* object() const { return _object; } + + bool isActive() const { return _isActive; } + void setActive(bool active) { _isActive = active; updateItem(); } + + protected: + ObjectType* _object{nullptr}; + + bool _isActive{false}; + + private: + QFont _regularFont; + QFont _activeFont; + }; +} + +#include "ObjectListItem.impl.h" + +#endif diff --git a/Grinder/ui/widget/ObjectListItem.impl.h b/Grinder/ui/widget/ObjectListItem.impl.h new file mode 100644 index 0000000000000000000000000000000000000000..d8c3931b153a2247608ba9c22ec2d1af42e27d53 --- /dev/null +++ b/Grinder/ui/widget/ObjectListItem.impl.h @@ -0,0 +1,26 @@ +/****************************************************************************** + * File: ObjectListItem.impl.h + * Date: 10.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ObjectListItem.h" + +template<typename ObjectType> +ObjectListItem<ObjectType>::ObjectListItem(ObjectType* object) : + _object{object} +{ + if (!object) + throw std::invalid_argument{_EXCPT("object may not be null")}; + + _activeFont.setBold(true); +} + +template<typename ObjectType> +void ObjectListItem<ObjectType>::updateItem() +{ + if (_isActive) + setFont(_activeFont); + else + setFont(_regularFont); +} diff --git a/Grinder/ui/widget/ObjectListWidget.h b/Grinder/ui/widget/ObjectListWidget.h new file mode 100644 index 0000000000000000000000000000000000000000..df8fc755f8fd637ff5e94324618b32cceed37638 --- /dev/null +++ b/Grinder/ui/widget/ObjectListWidget.h @@ -0,0 +1,56 @@ +/****************************************************************************** + * File: ObjectListWidget.h + * Date: 10.2.2018 + *****************************************************************************/ + +#ifndef OBJECTLISTWIDGET_H +#define OBJECTLISTWIDGET_H + +#include <QListWidget> + +#include "ObjectListItem.h" + +namespace grndr +{ + class ControlBar; + class Label; + + template<typename ObjectType, typename ItemType> + class ObjectListWidget : public QListWidget + { + static_assert(std::is_base_of<ObjectListItem<ObjectType>, ItemType>::value, "ItemType must be derived from ObjectListItem<ObjectType>"); + + public: + using object_type = ObjectType; + using item_type = ItemType; + + public: + using QListWidget::QListWidget; + + public: + item_type* addObject(object_type* obj, bool autoSelect = true, bool insertAtFront = false); + void removeObject(const object_type* obj, bool autoSelect = true); + object_type* currentObject() const; + + template<typename ContainerType> + void populateList(const ContainerType& container, bool selectFirst = true, bool reverseOrder = false); + + protected: + virtual void mouseDoubleClickEvent(QMouseEvent* event) override; + + protected: + virtual void switchToObjectItem(item_type* item, bool selectItem = true) { Q_UNUSED(item); Q_UNUSED(selectItem); } + virtual void objectSwitched(object_type* obj); + + item_type* objectItem(int index) const; + item_type* currentObjectItem() const { return objectItem(currentRow()); } + item_type* activeObjectItem() const; + + item_type* findObjectItem(const object_type* obj) const; + int findObjectItemIndex(const item_type* item); + }; +} + +#include "ObjectListWidget.impl.h" + +#endif diff --git a/Grinder/ui/widget/ObjectListWidget.impl.h b/Grinder/ui/widget/ObjectListWidget.impl.h new file mode 100644 index 0000000000000000000000000000000000000000..efdafd38ce4d283a34f2a9ba1c540cb066870d01 --- /dev/null +++ b/Grinder/ui/widget/ObjectListWidget.impl.h @@ -0,0 +1,141 @@ +/****************************************************************************** + * File: ObjectListWidget.impl.h + * Date: 10.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ObjectListWidget.h" + +template<typename ObjectType, typename ItemType> +ItemType* ObjectListWidget<ObjectType, ItemType>::addObject(object_type* obj, bool autoSelect, bool insertAtFront) +{ + auto item = new ItemType{obj}; + + if (insertAtFront) + insertItem(0, item); + else + addItem(item); + + if (autoSelect) + setCurrentItem(item); + + return item; +} + +template<typename ObjectType, typename ItemType> +void ObjectListWidget<ObjectType, ItemType>::removeObject(const object_type* obj, bool autoSelect) +{ + int curRow = currentRow(); + + if (auto item = findObjectItem(obj)) + { + auto taken = takeItem(indexFromItem(item).row()); + delete taken; + + if (autoSelect) + setCurrentRow(std::min(curRow, count() - 1)); + } +} + +template<typename ObjectType, typename ItemType> +ObjectType* ObjectListWidget<ObjectType, ItemType>::currentObject() const +{ + auto item = currentObjectItem(); + + if (item) + return item->object(); + else + return nullptr; +} + +template<typename ObjectType, typename ItemType> +template<typename ContainerType> +void ObjectListWidget<ObjectType, ItemType>::populateList(const ContainerType& container, bool selectFirst, bool reverseOrder) +{ + clear(); + + bool firstItem = true; + + for (const auto& item : container) + { + addObject(item.get(), firstItem, reverseOrder); + firstItem = false; + } + + if (!firstItem && selectFirst) // There's at least one item, so select the first one + switchToObjectItem(objectItem(0)); +} + +template<typename ObjectType, typename ItemType> +void ObjectListWidget<ObjectType, ItemType>::mouseDoubleClickEvent(QMouseEvent* event) +{ + auto item = dynamic_cast<item_type*>(itemAt(event->pos())); + + if (item) + switchToObjectItem(item); + else + QListWidget::mouseDoubleClickEvent(event); +} + +template<typename ObjectType, typename ItemType> +void ObjectListWidget<ObjectType, ItemType>::objectSwitched(object_type* obj) +{ + for (int i = 0; i < count(); ++i) + { + if (auto item = objectItem(i)) + item->setActive(item->object() == obj); + } + + update(); +} + +template<typename ObjectType, typename ItemType> +ItemType* ObjectListWidget<ObjectType, ItemType>::objectItem(int index) const +{ + if (index < 0 || index >= count()) + return nullptr; + + return dynamic_cast<item_type*>(item(index)); +} + +template<typename ObjectType, typename ItemType> +ItemType* ObjectListWidget<ObjectType, ItemType>::activeObjectItem() const +{ + for (int i = 0; i < count(); ++i) + { + if (auto item = objectItem(i)) + { + if (item->isActive()) + return item; + } + } + + return nullptr; +} + +template<typename ObjectType, typename ItemType> +ItemType* ObjectListWidget<ObjectType, ItemType>::findObjectItem(const object_type* obj) const +{ + for (int i = 0; i < count(); ++i) + { + if (auto item = objectItem(i)) + { + if (item->object() == obj) + return item; + } + } + + return nullptr; +} + +template<typename ObjectType, typename ItemType> +int ObjectListWidget<ObjectType, ItemType>::findObjectItemIndex(const item_type* item) +{ + for (int i = 0; i < count(); ++i) + { + if (objectItem(i) == item) + return i; + } + + return -1; +} diff --git a/Grinder/util/CVUtils.cpp b/Grinder/util/CVUtils.cpp new file mode 100644 index 0000000000000000000000000000000000000000..f38bb6738f115149cc4da45b21aa54790ffdfc57 --- /dev/null +++ b/Grinder/util/CVUtils.cpp @@ -0,0 +1,40 @@ +/****************************************************************************** + * File: CVUtils.cpp + * Date: 19.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "CVUtils.h" +#include "image/ImageExceptions.h" + +QImage CVUtils::matrixToImage(const cv::Mat& matrix) +{ + cv::Mat imageMatrix = matrix; + + // Check if the matrix has an unsupported type; if so, convert it first + if (matrix.type() != CV_8UC4 && matrix.type() != CV_8UC3 && matrix.type() != CV_8UC1) + { + matrix.convertTo(imageMatrix, CV_8U); + cv::normalize(imageMatrix, imageMatrix, std::numeric_limits<unsigned char>::max(), std::numeric_limits<unsigned char>::min(), cv::NORM_MINMAX); + } + + switch (imageMatrix.type()) + { + case CV_8UC4: + return QImage(imageMatrix.data, imageMatrix.cols, imageMatrix.rows, static_cast<int>(imageMatrix.step), QImage::Format_ARGB32); + + case CV_8UC3: + return QImage(imageMatrix.data, imageMatrix.cols, imageMatrix.rows, static_cast<int>(imageMatrix.step), QImage::Format_RGB888).rgbSwapped(); + + case CV_8UC1: + return QImage(imageMatrix.data, imageMatrix.cols, imageMatrix.rows, static_cast<int>(imageMatrix.step), QImage::Format_Grayscale8); + + default: + throw ImageException{_EXCPT("Unsupported matrix format for conversion to an image")}; + } +} + +QPixmap CVUtils::matrixToPixmap(const cv::Mat& matrix) +{ + return QPixmap::fromImage(matrixToImage(matrix)); +} diff --git a/Grinder/util/CVUtils.h b/Grinder/util/CVUtils.h new file mode 100644 index 0000000000000000000000000000000000000000..575fc9b850858c83e4b3ce30e56be7cdb4491802 --- /dev/null +++ b/Grinder/util/CVUtils.h @@ -0,0 +1,34 @@ +/****************************************************************************** + * File: CVUtils.h + * Date: 19.2.2018 + *****************************************************************************/ + +#ifndef CVUTILS_H +#define CVUTILS_H + +#include <QString> +#include <QImage> +#include <functional> +#include <opencv2/core.hpp> + +namespace grndr +{ + class CVUtils final + { + public: + static QImage matrixToImage(const cv::Mat& matrix); + static QPixmap matrixToPixmap(const cv::Mat& matrix); + + template<typename DataType> + static QString matrixToString(const cv::Mat& matrix); + template<typename DataType> + static QString matrixToString(const cv::Mat& matrix, std::function<QString(const DataType&)> formatter); + + private: + CVUtils() { } + }; +} + +#include "CVUtils.impl.h" + +#endif diff --git a/Grinder/util/CVUtils.impl.h b/Grinder/util/CVUtils.impl.h new file mode 100644 index 0000000000000000000000000000000000000000..5b409e341fdc4135865998a754c3749f8e7b2f07 --- /dev/null +++ b/Grinder/util/CVUtils.impl.h @@ -0,0 +1,36 @@ +/****************************************************************************** + * File: CVUtils.impl.h + * Date: 19.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "CVUtils.h" + +template<typename DataType> +QString CVUtils::matrixToString(const cv::Mat& matrix) +{ + // Default formatter: Use QString's arg function + return matrixToString(matrix, [](const DataType& value) { return QString{"%1"}.arg(value); }); +} + +template<typename DataType> +QString CVUtils::matrixToString(const cv::Mat& matrix, std::function<QString(const DataType&)> formatter) +{ + QStringList entries; + QString stringRep; + + for (int r = 0; r < matrix.rows; ++r) + { + entries.clear(); + + for (int c = 0; c < matrix.cols; ++c) + entries << formatter(matrix.at<DataType>(r, c)); + + if (r != 0) + stringRep += "\n"; + + stringRep += entries.join(", "); + } + + return stringRep; +} diff --git a/Grinder/util/DataUtils.cpp b/Grinder/util/DataUtils.cpp new file mode 100644 index 0000000000000000000000000000000000000000..f2d53d3a8cc5473afa14186dc44f3632b0e106c0 --- /dev/null +++ b/Grinder/util/DataUtils.cpp @@ -0,0 +1,97 @@ +/****************************************************************************** + * File: DataUtils.cpp + * Date: 16.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "DataUtils.h" + +QString DataUtils::getDataDescriptorTypeName(DataDescriptor::StructureType type) +{ + switch (type) + { + case DataDescriptor::StructureType::Any: + return "Arbitrary"; + + case DataDescriptor::StructureType::Adaptive: + return "Adaptive"; + + case DataDescriptor::StructureType::Scalar: + return "Scalar"; + + case DataDescriptor::StructureType::Vector: + return "Vector"; + + case DataDescriptor::StructureType::Matrix: + return "Matrix"; + + default: + return "Unknown"; + } +} + +QString DataUtils::getDataDescriptorTypeName(DataDescriptor::FieldType type) +{ + switch (type) + { + case DataDescriptor::FieldType::Any: + return "Arbitrary"; + + case DataDescriptor::FieldType::Adaptive: + return "Adaptive"; + + case DataDescriptor::FieldType::Basic: + return "Basic"; + + case DataDescriptor::FieldType::Color: + return "Colors"; + + case DataDescriptor::FieldType::Point2D: + return "2D points"; + + case DataDescriptor::FieldType::Point3D: + return "3D points"; + + default: + return "Unknown"; + } +} + +QString DataUtils::getDataDescriptorTypeName(DataDescriptor::ValueType type) +{ + switch (type) + { + case DataDescriptor::ValueType::Any: + return "Arbitrary"; + + case DataDescriptor::ValueType::Adaptive: + return "Adaptive"; + + case DataDescriptor::ValueType::Int8: + return "Integer (signed, 8 bit)"; + + case DataDescriptor::ValueType::UInt8: + return "Integer (unsigned, 8 bit)"; + + case DataDescriptor::ValueType::Int16: + return "Integer (signed, 16 bit)"; + + case DataDescriptor::ValueType::UInt16: + return "Integer (unsigned, 16 bit)"; + + case DataDescriptor::ValueType::Int32: + return "Integer (signed, 32 bit)"; + + case DataDescriptor::ValueType::UInt32: + return "Integer (unsigned, 32 bit)"; + + case DataDescriptor::ValueType::Float: + return "Real (single precision)"; + + case DataDescriptor::ValueType::Double: + return "Real (double precision)"; + + default: + return "Unknown"; + } +} diff --git a/Grinder/util/DataUtils.h b/Grinder/util/DataUtils.h new file mode 100644 index 0000000000000000000000000000000000000000..0f78a81ea5b7a867019ac841f0b049417f9ff2db --- /dev/null +++ b/Grinder/util/DataUtils.h @@ -0,0 +1,25 @@ +/****************************************************************************** + * File: DataUtils.h + * Date: 16.2.2018 + *****************************************************************************/ + +#ifndef DATAUTILS_H +#define DATAUTILS_H + +#include "engine/data/DataDescriptor.h" + +namespace grndr +{ + class DataUtils final + { + public: + static QString getDataDescriptorTypeName(DataDescriptor::StructureType type); + static QString getDataDescriptorTypeName(DataDescriptor::FieldType type); + static QString getDataDescriptorTypeName(DataDescriptor::ValueType type); + + private: + DataUtils() { } + }; +} + +#endif diff --git a/Grinder/util/FileUtils.cpp b/Grinder/util/FileUtils.cpp new file mode 100644 index 0000000000000000000000000000000000000000..765cbc05dfb86c92b22a9f09f37a523ca143c8c7 --- /dev/null +++ b/Grinder/util/FileUtils.cpp @@ -0,0 +1,34 @@ +/****************************************************************************** + * File: FileUtils.cpp + * Date: 10.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "FileUtils.h" + +QStringList FileUtils::expandFileList(QStringList paths, QStringList filters) +{ + QStringList files; + + for (auto path : paths) + { + QFileInfo fileInfo{path}; + + if (fileInfo.isDir()) + { + QDirIterator dirIt{fileInfo.filePath(), filters, QDir::NoFilter, QDirIterator::Subdirectories}; + + while (dirIt.hasNext()) + { + auto file = dirIt.next(); + + if (dirIt.fileInfo().isFile()) + files << file; + } + } + else + files << path; + } + + return files; +} diff --git a/Grinder/util/FileUtils.h b/Grinder/util/FileUtils.h new file mode 100644 index 0000000000000000000000000000000000000000..e9de78229e2a52e8763e90cc279b2618e396d5ec --- /dev/null +++ b/Grinder/util/FileUtils.h @@ -0,0 +1,23 @@ +/****************************************************************************** + * File: FileUtils.h + * Date: 10.2.2018 + *****************************************************************************/ + +#ifndef FILEUTILS_H +#define FILEUTILS_H + +#include <QStringList> + +namespace grndr +{ + class FileUtils final + { + public: + static QStringList expandFileList(QStringList paths, QStringList filters = QStringList{}); + + private: + FileUtils(); + }; +} + +#endif diff --git a/Grinder/util/ImageUtils.cpp b/Grinder/util/ImageUtils.cpp new file mode 100644 index 0000000000000000000000000000000000000000..3366c39109b55abb4ad470ee9927c3b2cb7885f8 --- /dev/null +++ b/Grinder/util/ImageUtils.cpp @@ -0,0 +1,24 @@ +/****************************************************************************** + * File: ImageUtils.cpp + * Date: 10.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ImageUtils.h" + +QStringList ImageUtils::getSupportedFormats() +{ + // Return a list of formats supported by OpenCV + return {"bmp", "dib", "jpeg", "jpg", "jpe", "jp2", "png", "webp", "pbm", "pgm", "ppm", "pxm", "pnm", "sr", "ras", "tiff", "tif", "exr", "hdr", "pic"}; +} + +QStringList ImageUtils::getSupportedFormatFilters() +{ + auto formats = getSupportedFormats(); + QStringList filters; + + for (auto format : formats) + filters << "*." + format; + + return filters; +} diff --git a/Grinder/util/ImageUtils.h b/Grinder/util/ImageUtils.h new file mode 100644 index 0000000000000000000000000000000000000000..3268df5771d608a70412c0c981a15f2db48dbf0b --- /dev/null +++ b/Grinder/util/ImageUtils.h @@ -0,0 +1,24 @@ +/****************************************************************************** + * File: ImageUtils.h + * Date: 10.2.2018 + *****************************************************************************/ + +#ifndef IMAGEUTILS_H +#define IMAGEUTILS_H + +#include <QStringList> + +namespace grndr +{ + class ImageUtils final + { + public: + static QStringList getSupportedFormats(); + static QStringList getSupportedFormatFilters(); + + private: + ImageUtils(); + }; +} + +#endif diff --git a/Grinder/util/MathUtils.cpp b/Grinder/util/MathUtils.cpp new file mode 100644 index 0000000000000000000000000000000000000000..652c90f69d169710294ec48de985670e569d871e --- /dev/null +++ b/Grinder/util/MathUtils.cpp @@ -0,0 +1,18 @@ +/****************************************************************************** + * File: MathUtils.cpp + * Date: 27.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "MathUtils.h" + +double MathUtils::vectorLength(double x, double y) +{ + // Simple L2 norm + return std::sqrt(x * x + y * y); +} + +QPoint MathUtils::round(const QPointF& point) +{ + return QPoint{std::lround(point.x()), std::lround(point.y())}; +} diff --git a/Grinder/util/MathUtils.h b/Grinder/util/MathUtils.h new file mode 100644 index 0000000000000000000000000000000000000000..049fa58b1143584d49898d582dd4101e8adb2caf --- /dev/null +++ b/Grinder/util/MathUtils.h @@ -0,0 +1,24 @@ +/****************************************************************************** + * File: MathUtils.h + * Date: 27.3.2018 + *****************************************************************************/ + +#ifndef MATHUTILS_H +#define MATHUTILS_H + +#include <QPoint> + +namespace grndr +{ + class MathUtils final + { + public: + static double vectorLength(double x, double y); + static QPoint round(const QPointF& point); + + private: + MathUtils() { } + }; +} + +#endif diff --git a/Grinder/util/SerializationUtils.cpp b/Grinder/util/SerializationUtils.cpp new file mode 100644 index 0000000000000000000000000000000000000000..864050eb7225513387f5653543b4209a62b94adc --- /dev/null +++ b/Grinder/util/SerializationUtils.cpp @@ -0,0 +1,7 @@ +/****************************************************************************** + * File: SerializationUtils.cpp + * Date: 01.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "SerializationUtils.h" diff --git a/Grinder/util/SerializationUtils.h b/Grinder/util/SerializationUtils.h new file mode 100644 index 0000000000000000000000000000000000000000..8190a56f897c44848178e151d96928bbf6db1aee --- /dev/null +++ b/Grinder/util/SerializationUtils.h @@ -0,0 +1,29 @@ +/****************************************************************************** + * File: SerializationUtils.h + * Date: 01.3.2018 + *****************************************************************************/ + +#ifndef SERIALIZATIONUTILS_H +#define SERIALIZATIONUTILS_H + +#include "project/serialization/SerializationContext.h" +#include "project/serialization/DeserializationContext.h" + +namespace grndr +{ + class SerializationUtils final + { + public: + template<typename ContainerType> + static void serializeContainer(const ContainerType& container, QString elemName, SerializationContext& ctx, std::function<bool(const typename ContainerType::value_type&)> predicate = nullptr); + template<typename ContainerType> + static void deserializeContainer(QString elemName, DeserializationContext& ctx, std::function<typename ContainerType::value_type(const SettingsContainer&)> objCreator); + + private: + SerializationUtils() { } + }; +} + +#include "SerializationUtils.impl.h" + +#endif diff --git a/Grinder/util/SerializationUtils.impl.h b/Grinder/util/SerializationUtils.impl.h new file mode 100644 index 0000000000000000000000000000000000000000..83e9bd488e147441c99e11eaeac8be70924390f4 --- /dev/null +++ b/Grinder/util/SerializationUtils.impl.h @@ -0,0 +1,35 @@ +/****************************************************************************** + * File: SerializationUtils.impl.h + * Date: 01.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "SerializationUtils.h" + +template<typename ContainerType> +void SerializationUtils::serializeContainer(const ContainerType& container, QString elemName, SerializationContext& ctx, std::function<bool(const typename ContainerType::value_type&)> predicate) +{ + for (const auto& object : container) + { + if (!predicate || predicate(object)) + { + ctx.beginGroup(elemName); + object->serialize(ctx); + ctx.endGroup(); + } + } +} + +template<typename ContainerType> +void SerializationUtils::deserializeContainer(QString elemName, DeserializationContext& ctx, std::function<typename ContainerType::value_type(const SettingsContainer&)> objCreator) +{ + for (const auto elemSettings : ctx.settings().children(elemName)) + { + ctx.beginGroup(elemSettings); + + if (auto object = objCreator(*elemSettings)) + object->deserialize(ctx); + + ctx.endGroup(); + } +} diff --git a/Grinder/util/StringConv.cpp b/Grinder/util/StringConv.cpp new file mode 100644 index 0000000000000000000000000000000000000000..57e298dadce2d12632acc421b4cf08ef5ace279a --- /dev/null +++ b/Grinder/util/StringConv.cpp @@ -0,0 +1,172 @@ +/****************************************************************************** + * File: StringConv.cpp + * Date: 12.1.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "StringConv.h" + +template<> +QString StringConv::convertValue<bool>(const bool& val) +{ + return val ? "true" : "false"; +} + +template<> +QString StringConv::convertValue<QPoint>(const QPoint& val) +{ + return QString{"%1;%2"}.arg(val.x()).arg(val.y()); +} + +template<> +QString StringConv::convertValue<QPointF>(const QPointF& val) +{ + return QString{"%1;%2"}.arg(val.x()).arg(val.y()); +} + +template<> +QString StringConv::convertValue<QSize>(const QSize& val) +{ + QPoint pt{val.width(), val.height()}; + return convertValue(pt); +} + +template<> +QString StringConv::convertValue<QColor>(const QColor& val) +{ + return val.name(); +} + +template<> +QString StringConv::convertString<QString>(const QString& str, bool* ok) +{ + if (ok) + *ok = true; + + return str; +} + +template<> +short StringConv::convertString<short>(const QString& str, bool* ok) +{ + return str.toShort(ok); +} + +template<> +ushort StringConv::convertString<ushort>(const QString& str, bool* ok) +{ + return str.toUShort(ok); +} + +template<> +int StringConv::convertString<int>(const QString& str, bool* ok) +{ + return str.toInt(ok); +} + +template<> +uint StringConv::convertString<uint>(const QString& str, bool* ok) +{ + return str.toUInt(ok); +} + +template<> +long StringConv::convertString<long>(const QString& str, bool* ok) +{ + return str.toLong(ok); +} + +template<> +ulong StringConv::convertString<ulong>(const QString& str, bool* ok) +{ + return str.toULong(ok); +} + +template<> +qlonglong StringConv::convertString<qlonglong>(const QString& str, bool* ok) +{ + return str.toLongLong(ok); +} + +template<> +qulonglong StringConv::convertString<qulonglong>(const QString& str, bool* ok) +{ + return str.toULongLong(ok); +} + +template<> +float StringConv::convertString<float>(const QString& str, bool* ok) +{ + return str.toFloat(ok); +} + +template<> +double StringConv::convertString<double>(const QString& str, bool* ok) +{ + return str.toDouble(ok); +} + +template<> +bool StringConv::convertString<bool>(const QString& str, bool* ok) +{ + if (ok) + *ok = true; + + return (str.compare("true", Qt::CaseInsensitive) == 0 || str.compare("yes", Qt::CaseInsensitive) == 0 || str == "1") ? true : false; +} + +template<> +QPoint StringConv::convertString<QPoint>(const QString& str, bool* ok) +{ + auto tokens = str.split(";"); + + if (tokens.size() == 2) + { + bool ok1 = true, ok2 = true; + QPoint point{tokens[0].toInt(&ok1), tokens[1].toInt(&ok2)}; + + if (ok) + *ok = ok1 && ok2; + + return point; + } + else + return QPoint{}; +} + +template<> +QPointF StringConv::convertString<QPointF>(const QString& str, bool* ok) +{ + auto tokens = str.split(";"); + + if (tokens.size() == 2) + { + bool ok1 = true, ok2 = true; + QPointF point{tokens[0].toDouble(&ok1), tokens[1].toDouble(&ok2)}; + + if (ok) + *ok = ok1 && ok2; + + return point; + } + else + return QPointF{}; +} + +template<> +QSize StringConv::convertString<QSize>(const QString& str, bool* ok) +{ + QPoint pt = convertString<QPoint>(str, ok); + return QSize{pt.x(), pt.y()}; +} + +template<> +QColor StringConv::convertString<QColor>(const QString& str, bool* ok) +{ + QColor color{str}; + + if (ok) + *ok = color.isValid(); + + return color; +} diff --git a/Grinder/util/StringConv.h b/Grinder/util/StringConv.h new file mode 100644 index 0000000000000000000000000000000000000000..05ca47113d4bd4a21386b4d9b6beba57298b18af --- /dev/null +++ b/Grinder/util/StringConv.h @@ -0,0 +1,56 @@ +/****************************************************************************** + * File: StringConv.h + * Date: 12.1.2018 + *****************************************************************************/ + +#ifndef STRINGCONV_H +#define STRINGCONV_H + +#include <QString> +#include <QPoint> +#include <QSize> +#include <QColor> + +namespace grndr +{ + class StringConv final + { + public: + template<typename ValueType> + static QString convertValue(const ValueType& val) + { + return QString("%1").arg(val); + } + + template<typename ValueType> + static ValueType convertString(const QString&, bool* ok = nullptr); + + private: + StringConv() { } + }; + + template<> QString StringConv::convertValue<bool>(const bool& val); + template<> QString StringConv::convertValue<QPoint>(const QPoint& val); + template<> QString StringConv::convertValue<QPointF>(const QPointF& val); + template<> QString StringConv::convertValue<QSize>(const QSize& val); + template<> QString StringConv::convertValue<QColor>(const QColor& val); + + template<> QString StringConv::convertString<QString>(const QString& str, bool* ok); + template<> short StringConv::convertString<short>(const QString& str, bool* ok); + template<> ushort StringConv::convertString<ushort>(const QString& str, bool* ok); + template<> int StringConv::convertString<int>(const QString& str, bool* ok); + template<> uint StringConv::convertString<uint>(const QString& str, bool* ok); + template<> long StringConv::convertString<long>(const QString& str, bool* ok); + template<> ulong StringConv::convertString<ulong>(const QString& str, bool* ok); + template<> qlonglong StringConv::convertString<qlonglong>(const QString& str, bool* ok); + template<> qulonglong StringConv::convertString<qulonglong>(const QString& str, bool* ok); + template<> float StringConv::convertString<float>(const QString& str, bool* ok); + template<> double StringConv::convertString<double>(const QString& str, bool* ok); + template<> bool StringConv::convertString<bool>(const QString& str, bool* ok); + template<> QPoint StringConv::convertString<QPoint>(const QString& str, bool* ok); + template<> QPointF StringConv::convertString<QPointF>(const QString& str, bool* ok); + template<> QSize StringConv::convertString<QSize>(const QString& str, bool* ok); + template<> QColor StringConv::convertString<QColor>(const QString& str, bool* ok); +} + +#endif diff --git a/Grinder/util/StringUtils.cpp b/Grinder/util/StringUtils.cpp new file mode 100644 index 0000000000000000000000000000000000000000..f0bb5cae1a07ddf95ad5d5dfe80a6ac89750edea --- /dev/null +++ b/Grinder/util/StringUtils.cpp @@ -0,0 +1,70 @@ +/****************************************************************************** + * File: StringUtils.cpp + * Date: 12.1.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "StringUtils.h" + +QString StringUtils::encodeEscapeCharacters(QString str, bool includeQuotes) +{ + str.replace("\\", "\\\\"); + str.replace("\n", "\\n"); + str.replace("\r", "\\r"); + str.replace("\t", "\\t"); + + if (includeQuotes) + str.replace("\"", "\\\""); + + return str; +} + +QString StringUtils::decodeEscapeCharacters(const QString& str, bool includeQuotes) +{ + QString strOut; + bool isEscape = false; + + for (QChar c : str) + { + if (isEscape) + { + if (c == 'n') + c = '\n'; + else if (c == 'r') + c = '\r'; + else if (c == 't') + c = '\t'; + else if (c == '\\') + c = '\\'; + else if (c == '\"' && includeQuotes) + c = '\"'; + + strOut += c; + isEscape = false; + } + else + { + if (c == '\\') + isEscape = true; + else + strOut += c; + } + } + + return strOut; +} + +QString StringUtils::fileSizeToString(qint64 fileSize) +{ + static const QString units[] = {"B", "KB", "MB", "GB"}; + double size = fileSize; + int unitIndex = 0; + + while (size > 1024.0) + { + size /= 1024.0; + unitIndex++; + } + + return QString{"%1 %2"}.arg(size, 0, 'f', 2).arg(units[unitIndex]); +} diff --git a/Grinder/util/StringUtils.h b/Grinder/util/StringUtils.h new file mode 100644 index 0000000000000000000000000000000000000000..8e28ce4a48018bfc7ec16b1f7fc1bbb946b7e440 --- /dev/null +++ b/Grinder/util/StringUtils.h @@ -0,0 +1,32 @@ +/****************************************************************************** + * File: StringUtils.h + * Date: 12.1.2018 + *****************************************************************************/ + +#ifndef STRINGUTILS_H +#define STRINGUTILS_H + +#include <QString> +#include <functional> + +namespace grndr +{ + class StringUtils final + { + public: + static QString encodeEscapeCharacters(QString str, bool includeQuotes = true); + static QString decodeEscapeCharacters(const QString& str, bool includeQuotes = true); + + static QString fileSizeToString(qint64 fileSize); + + template<typename ContainerType> + static QString generateUniqueItemName(const ContainerType& container, QString name, std::function<QString(const typename ContainerType::value_type&)> nameGetter, bool caseSensitive = false, bool addSpaces = true); + + private: + StringUtils() { } + }; +} + +#include "StringUtils.impl.h" + +#endif diff --git a/Grinder/util/StringUtils.impl.h b/Grinder/util/StringUtils.impl.h new file mode 100644 index 0000000000000000000000000000000000000000..19c50973a54af398b1af8d5c21eef31fe8c2c2ff --- /dev/null +++ b/Grinder/util/StringUtils.impl.h @@ -0,0 +1,54 @@ +/****************************************************************************** + * File: StringUtils.impl.h + * Date: 02.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "StringUtils.h" + +template<typename ContainerType> +QString StringUtils::generateUniqueItemName(const ContainerType& container, QString name, std::function<QString(const typename ContainerType::value_type&)> nameGetter, bool caseSensitive, bool addSpaces) +{ + QString newName; + + // Convert names like ThisIsMyName to This Is My Name + if (addSpaces) + { + bool prevUpper = true; + + for (QChar c : name) + { + if (c.isUpper()) + { + if (!prevUpper) + newName += " "; + + prevUpper = true; + } + + newName += c; + prevUpper = c.isUpper(); + } + } + else + newName = name; + + unsigned int index = 1; + + // Add increasing numbers to the name until we've found a unique one + while (true) + { + QString indexedName = QString{"%1 #%2"}.arg(newName).arg(index++); + + if (std::none_of(std::cbegin(container), std::cend(container), [indexedName, caseSensitive, nameGetter](const typename ContainerType::value_type& value) { + QString existingName = nameGetter(value); + return existingName.compare(indexedName, caseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive) == 0; + })) + { + newName = indexedName; + break; + } + } + + return newName; +} diff --git a/Grinder/util/TemporaryStatusMessage.cpp b/Grinder/util/TemporaryStatusMessage.cpp new file mode 100644 index 0000000000000000000000000000000000000000..4c8af3429f6b0760cc5c286ea79ef24f4608ebf9 --- /dev/null +++ b/Grinder/util/TemporaryStatusMessage.cpp @@ -0,0 +1,31 @@ +/****************************************************************************** + * File: TemporaryStatusMessage.cpp + * Date: 10.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "TemporaryStatusMessage.h" +#include "core/GrinderApplication.h" + +TemporaryStatusMessage::TemporaryStatusMessage(QString message) +{ + setMessage(message); +} + +TemporaryStatusMessage::~TemporaryStatusMessage() +{ + if (grinder()->mainWindow()) + { + grinder()->mainWindow()->statusBar()->clearMessage(); + QApplication::processEvents(QEventLoop::ExcludeUserInputEvents); + } +} + +void TemporaryStatusMessage::setMessage(QString message) const +{ + if (!message.isEmpty() && grinder()->mainWindow()) + { + grinder()->mainWindow()->statusBar()->showMessage(message); + QApplication::processEvents(QEventLoop::ExcludeUserInputEvents); + } +} diff --git a/Grinder/util/TemporaryStatusMessage.h b/Grinder/util/TemporaryStatusMessage.h new file mode 100644 index 0000000000000000000000000000000000000000..c17bf07f5de7438fc4f43c523dd54f850944ec51 --- /dev/null +++ b/Grinder/util/TemporaryStatusMessage.h @@ -0,0 +1,24 @@ +/****************************************************************************** + * File: TemporaryStatusMessage.h + * Date: 10.2.2018 + *****************************************************************************/ + +#ifndef TEMPORARYSTATUSMESSAGE_H +#define TEMPORARYSTATUSMESSAGE_H + +#include <QString> + +namespace grndr +{ + class TemporaryStatusMessage + { + public: + TemporaryStatusMessage(QString message = ""); + ~TemporaryStatusMessage(); + + public: + void setMessage(QString message) const; + }; +} + +#endif diff --git a/Grinder/util/UIUtils.cpp b/Grinder/util/UIUtils.cpp new file mode 100644 index 0000000000000000000000000000000000000000..d6cc7433ab9678b6ff4e70d82dfaf3a003bceb77 --- /dev/null +++ b/Grinder/util/UIUtils.cpp @@ -0,0 +1,87 @@ +/****************************************************************************** + * File: UIUtils.cpp + * Date: 02.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "UIUtils.h" +#include "core/GrinderApplication.h" + +QAction* UIUtils::createAction(QObject* owner, QString name, QString icon, const char* callback, QString help, QString shortcut, Qt::ShortcutContext context, QObject* receiver) +{ + auto action = new QAction{name, owner}; + + if (auto widget = dynamic_cast<QWidget*>(owner)) + widget->addAction(action); + + if (!icon.isEmpty()) + action->setIcon(QIcon{icon}); + + if (!help.isEmpty()) + { + action->setStatusTip(help); + action->setToolTip(help); + } + + if (!shortcut.isEmpty()) + { + action->setShortcut(QKeySequence{shortcut}); + action->setShortcutContext(context); + action->setShortcutVisibleInContextMenu(true); + } + + if (callback) + owner->connect(action, SIGNAL(triggered(bool)), receiver ? receiver : owner, callback); + + return action; +} + +QString UIUtils::askFileName(bool saveFileName, QString dlgName, QWidget* parent, QString caption, QString filter, QString* selectedFilter, QFileDialog::Options options) +{ + QString dir = grinder()->settings().getFileDialogDir(dlgName); + QString fileName = saveFileName ? QFileDialog::getSaveFileName(parent, caption, dir, filter, selectedFilter, options) : QFileDialog::getOpenFileName(parent, caption, dir, filter, selectedFilter, options); + + if (!fileName.isEmpty()) + { + QFileInfo fileInfo{fileName}; + grinder()->settings().setFileDialogDir(dlgName, fileInfo.path()); + } + + return fileName; +} + +QStringList UIUtils::askFileNames(QString dlgName, QWidget* parent, QString caption, QString filter, QString* selectedFilter, QFileDialog::Options options) +{ + QString dir = grinder()->settings().getFileDialogDir(dlgName); + auto fileNames = QFileDialog::getOpenFileNames(parent, caption, dir, filter, selectedFilter, options); + + if (!fileNames.isEmpty()) + { + QFileInfo fileInfo{fileNames.front()}; + grinder()->settings().setFileDialogDir(dlgName, fileInfo.path()); + } + + return fileNames; +} + +void UIUtils::removeChildrenFromLayout(QLayout* layout) +{ + QLayoutItem* item; + QLayout* sublayout; + QWidget* widget; + + while ((item = layout->takeAt(0))) + { + if ((sublayout = item->layout())) + { + removeChildrenFromLayout(sublayout); + } + else if ((widget = item->widget())) + { + widget->hide(); + delete widget; + } + else + delete item; + } +} diff --git a/Grinder/util/UIUtils.h b/Grinder/util/UIUtils.h new file mode 100644 index 0000000000000000000000000000000000000000..2e6597b9bfbe6b3f7d5b23bea73b84aceb2f0e4d --- /dev/null +++ b/Grinder/util/UIUtils.h @@ -0,0 +1,29 @@ +/****************************************************************************** + * File: UIUtils.h + * Date: 02.2.2018 + *****************************************************************************/ + +#ifndef UIUTILS_H +#define UIUTILS_H + +#include <QAction> +#include <QFileDialog> + +namespace grndr +{ + class UIUtils final + { + public: + static QAction* createAction(QObject* owner, QString name, QString icon = "", const char* callback = nullptr, QString help = "", QString shortcut = "", Qt::ShortcutContext context = Qt::WidgetShortcut, QObject* receiver = nullptr); + + static QString askFileName(bool saveFileName, QString dlgName, QWidget* parent = nullptr, QString caption = "", QString filter = "", QString* selectedFilter = nullptr, QFileDialog::Options options = QFileDialog::Options{}); + static QStringList askFileNames(QString dlgName, QWidget* parent = nullptr, QString caption = "", QString filter = "", QString* selectedFilter = nullptr, QFileDialog::Options options = QFileDialog::Options{}); + + static void removeChildrenFromLayout(QLayout* layout); + + private: + UIUtils() { } + }; +} + +#endif