diff --git a/Examples/Plugins/PluginList.cmake b/Examples/Plugins/PluginList.cmake index 4a4d4ec08e..4f595b8cec 100644 --- a/Examples/Plugins/PluginList.cmake +++ b/Examples/Plugins/PluginList.cmake @@ -1,17 +1,18 @@ # Plug-ins must be ordered according to their dependencies set(MITK_EXAMPLE_PLUGINS org.mitk.example.gui.minimalapplication:ON org.mitk.example.gui.customviewer:ON org.mitk.example.gui.customviewer.views:ON org.mitk.example.gui.multipleperspectives:ON org.mitk.example.gui.selectionserviceqt:ON org.mitk.example.gui.selectionservicemitk:ON org.mitk.example.gui.selectionservicemitk.views:ON org.mitk.example.gui.extensionpointdefinition:ON org.mitk.example.gui.extensionpointcontribution:ON org.mitk.example.gui.regiongrowing:ON org.mitk.example.gui.pcaexample:ON org.mitk.example.gui.imaging:ON + org.mitk.example.gui.forms:ON ) diff --git a/Examples/Plugins/org.mitk.example.gui.forms/CMakeLists.txt b/Examples/Plugins/org.mitk.example.gui.forms/CMakeLists.txt new file mode 100644 index 0000000000..1f1cbb8764 --- /dev/null +++ b/Examples/Plugins/org.mitk.example.gui.forms/CMakeLists.txt @@ -0,0 +1,8 @@ +project(org_mitk_example_gui_forms) + +mitk_create_plugin( + EXPORT_DIRECTIVE FORMS_EXPORT + EXPORTED_INCLUDE_SUFFIXES src + MODULE_DEPENDS MitkFormsUI + NO_INSTALL +) diff --git a/Examples/Plugins/org.mitk.example.gui.forms/files.cmake b/Examples/Plugins/org.mitk.example.gui.forms/files.cmake new file mode 100644 index 0000000000..69d97f7b42 --- /dev/null +++ b/Examples/Plugins/org.mitk.example.gui.forms/files.cmake @@ -0,0 +1,18 @@ +set(CPP_FILES + src/internal/mitkPluginActivator.cpp + src/internal/QmitkFormsExampleView.cpp +) + +set(MOC_H_FILES + src/internal/mitkPluginActivator.h + src/internal/QmitkFormsExampleView.h +) + +set(CACHED_RESOURCE_FILES + resources/FormsIcon.svg + plugin.xml +) + +set(QRC_FILES + resources/FormsExample.qrc +) diff --git a/Examples/Plugins/org.mitk.example.gui.forms/manifest_headers.cmake b/Examples/Plugins/org.mitk.example.gui.forms/manifest_headers.cmake new file mode 100644 index 0000000000..59fbd0ee21 --- /dev/null +++ b/Examples/Plugins/org.mitk.example.gui.forms/manifest_headers.cmake @@ -0,0 +1,5 @@ +set(Plugin-Name "MITK Forms Example") +set(Plugin-Version "0.1") +set(Plugin-Vendor "German Cancer Research Center (DKFZ)") +set(Plugin-ContactAddress "https://www.mitk.org") +set(Require-Plugin org.mitk.gui.qt.common) diff --git a/Examples/Plugins/org.mitk.example.gui.forms/plugin.xml b/Examples/Plugins/org.mitk.example.gui.forms/plugin.xml new file mode 100644 index 0000000000..f5fc563d96 --- /dev/null +++ b/Examples/Plugins/org.mitk.example.gui.forms/plugin.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/Examples/Plugins/org.mitk.example.gui.forms/resources/ExampleForm.json b/Examples/Plugins/org.mitk.example.gui.forms/resources/ExampleForm.json new file mode 100644 index 0000000000..4850ad4ad5 --- /dev/null +++ b/Examples/Plugins/org.mitk.example.gui.forms/resources/ExampleForm.json @@ -0,0 +1,67 @@ +{ + "FileFormat": "MITK Form", + "Version": 1, + "Sections": [ + { + "Title": "Example Form", + "Description": "This form was deserialized from a JSON file. Feel free to complete and submit it, resp. save its responses to a new or existing comma-separated value file.", + "Questions": [ + { + "Text": "Is this is a multiple choice question?", + "Required": true, + "Type": "Multiple choice", + "Options": [ + "Yes, it is.", + "No, it isn't. Wait... it actually is." + ], + "Other": true + }, + { + "Text": "Is this a checkboxes question?", + "Type": "Checkboxes", + "Options": [ + "Yes, it is.", + "It is indeed.", + "Yep, I'm sure." + ], + "Other": true + }, + { + "Text": "Do you agree that this is a linear scale question?", + "Required": true, + "Type": "Linear scale", + "Range": [ + 1, + 4 + ], + "Labels": [ + "Agree", + "Agree even more" + ] + }, + { + "Text": "Is this a short-answer question?", + "Type": "Short answer" + } + ] + }, + { + "Title": "Second section", + "Description": "Welcome to the second and last section of this form.", + "Questions": [ + { + "Text": "Is this a dropdown question?", + "Type": "Drop-down", + "Options": [ + "Yes, it is.", + "I'm not sure (yes)." + ] + }, + { + "Text": "Is this a paragraph question?", + "Type": "Paragraph" + } + ] + } + ] +} diff --git a/Examples/Plugins/org.mitk.example.gui.forms/resources/FormsExample.qrc b/Examples/Plugins/org.mitk.example.gui.forms/resources/FormsExample.qrc new file mode 100644 index 0000000000..9b4072c7ea --- /dev/null +++ b/Examples/Plugins/org.mitk.example.gui.forms/resources/FormsExample.qrc @@ -0,0 +1,6 @@ + + + FormsIcon.svg + ExampleForm.json + + diff --git a/Examples/Plugins/org.mitk.example.gui.forms/resources/FormsIcon.svg b/Examples/Plugins/org.mitk.example.gui.forms/resources/FormsIcon.svg new file mode 100644 index 0000000000..2ba2d6773e --- /dev/null +++ b/Examples/Plugins/org.mitk.example.gui.forms/resources/FormsIcon.svg @@ -0,0 +1,68 @@ + + + + diff --git a/Examples/Plugins/org.mitk.example.gui.forms/src/internal/QmitkFormsExampleView.cpp b/Examples/Plugins/org.mitk.example.gui.forms/src/internal/QmitkFormsExampleView.cpp new file mode 100644 index 0000000000..4fc85f4f92 --- /dev/null +++ b/Examples/Plugins/org.mitk.example.gui.forms/src/internal/QmitkFormsExampleView.cpp @@ -0,0 +1,56 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#include "QmitkFormsExampleView.h" +#include + +#include + +#include +#include +#include + +const std::string QmitkFormsExampleView::VIEW_ID = "org.mitk.views.example.forms"; + +namespace +{ + QString ReadFileAsString(const QString& path) + { + QFile file(path); + + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) + mitkThrow() << "Could not open file \"" << path << "\"!"; + + QTextStream stream(&file); + return stream.readAll(); + } +} + +QmitkFormsExampleView::QmitkFormsExampleView() + : m_Form(nlohmann::json::parse(ReadFileAsString(":/FormsExample/ExampleForm.json").toStdString())) +{ +} + +QmitkFormsExampleView::~QmitkFormsExampleView() +{ +} + +void QmitkFormsExampleView::CreateQtPartControl(QWidget* parent) +{ + auto layout = new QVBoxLayout(parent); + layout->addWidget(new QmitkForm(m_Form)); + layout->addStretch(); +} + +void QmitkFormsExampleView::SetFocus() +{ +} diff --git a/Examples/Plugins/org.mitk.example.gui.forms/src/internal/QmitkFormsExampleView.h b/Examples/Plugins/org.mitk.example.gui.forms/src/internal/QmitkFormsExampleView.h new file mode 100644 index 0000000000..f541a1fb75 --- /dev/null +++ b/Examples/Plugins/org.mitk.example.gui.forms/src/internal/QmitkFormsExampleView.h @@ -0,0 +1,36 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#ifndef QmitkFormsExampleView_h +#define QmitkFormsExampleView_h + +#include +#include + +class QmitkFormsExampleView : public QmitkAbstractView +{ + Q_OBJECT + +public: + static const std::string VIEW_ID; + + QmitkFormsExampleView(); + ~QmitkFormsExampleView() override; + + void CreateQtPartControl(QWidget* parent) override; + void SetFocus() override; + +private: + mitk::Forms::Form m_Form; +}; + +#endif diff --git a/Examples/Plugins/org.mitk.example.gui.forms/src/internal/mitkPluginActivator.cpp b/Examples/Plugins/org.mitk.example.gui.forms/src/internal/mitkPluginActivator.cpp new file mode 100644 index 0000000000..428d756dbd --- /dev/null +++ b/Examples/Plugins/org.mitk.example.gui.forms/src/internal/mitkPluginActivator.cpp @@ -0,0 +1,23 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#include "mitkPluginActivator.h" +#include "QmitkFormsExampleView.h" + +void mitk::mitkPluginActivator::start(ctkPluginContext* context) +{ + BERRY_REGISTER_EXTENSION_CLASS(QmitkFormsExampleView, context) +} + +void mitk::mitkPluginActivator::stop(ctkPluginContext*) +{ +} diff --git a/Examples/Plugins/org.mitk.example.gui.forms/src/internal/mitkPluginActivator.h b/Examples/Plugins/org.mitk.example.gui.forms/src/internal/mitkPluginActivator.h new file mode 100644 index 0000000000..72d1f42520 --- /dev/null +++ b/Examples/Plugins/org.mitk.example.gui.forms/src/internal/mitkPluginActivator.h @@ -0,0 +1,32 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#ifndef mitkPluginActivator_h +#define mitkPluginActivator_h + +#include + +namespace mitk +{ + class mitkPluginActivator : public QObject, public ctkPluginActivator + { + Q_OBJECT + Q_PLUGIN_METADATA(IID "org_mitk_gui_example_forms") + Q_INTERFACES(ctkPluginActivator) + + public: + void start(ctkPluginContext* context) override; + void stop(ctkPluginContext* context) override; + }; +} + +#endif diff --git a/Modules/Forms/CMakeLists.txt b/Modules/Forms/CMakeLists.txt new file mode 100644 index 0000000000..83366a3827 --- /dev/null +++ b/Modules/Forms/CMakeLists.txt @@ -0,0 +1,7 @@ +mitk_create_module( + INCLUDE_DIRS + PUBLIC include + PRIVATE src + DEPENDS + PUBLIC MitkCore +) diff --git a/Modules/Forms/files.cmake b/Modules/Forms/files.cmake new file mode 100644 index 0000000000..ffa7c50a1e --- /dev/null +++ b/Modules/Forms/files.cmake @@ -0,0 +1,31 @@ +set(H_FILES + include/mitkCheckboxesQuestion.h + include/mitkDropdownQuestion.h + include/mitkForm.h + include/mitkIQuestionFactory.h + include/mitkLinearScaleQuestion.h + include/mitkMultipleChoiceQuestion.h + include/mitkParagraphQuestion.h + include/mitkQuestion.h + include/mitkQuestionWithOptions.h + include/mitkQuestionWithOtherOption.h + include/mitkShortAnswerQuestion.h + include/mitkTextQuestion.h +) + +set(CPP_FILES + mitkCheckboxesQuestion.cpp + mitkDropdownQuestion.cpp + mitkForm.cpp + mitkIQuestionFactory.cpp + mitkLinearScaleQuestion.cpp + mitkModuleActivator.cpp + mitkMultipleChoiceQuestion.cpp + mitkParagraphQuestion.cpp + mitkQuestion.cpp + mitkQuestionFactory.cpp + mitkQuestionWithOptions.cpp + mitkQuestionWithOtherOption.cpp + mitkShortAnswerQuestion.cpp + mitkTextQuestion.cpp +) diff --git a/Modules/Forms/include/mitkCheckboxesQuestion.h b/Modules/Forms/include/mitkCheckboxesQuestion.h new file mode 100644 index 0000000000..2dac18b3a1 --- /dev/null +++ b/Modules/Forms/include/mitkCheckboxesQuestion.h @@ -0,0 +1,49 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#ifndef mitkCheckboxesQuestion_h +#define mitkCheckboxesQuestion_h + +#include + +namespace mitk::Forms +{ + /** \brief A Question whose possible responses are represented by checkboxes. + * + * The question can have multiple responses at once, including an optional + * free text response. + * + * \sa MultipleChoiceQuestion, DropdownQuestion + */ + class MITKFORMS_EXPORT CheckboxesQuestion : public QuestionWithOtherOption + { + public: + ~CheckboxesQuestion() override; + + std::string GetType() const override; + Question* CreateAnother() const override; + + void FromJSON(const nlohmann::ordered_json& j) override; + void ToJSON(nlohmann::ordered_json& j) const override; + + using QuestionWithOptions::AddResponse; + using QuestionWithOptions::RemoveResponse; + + using QuestionWithOtherOption::AddOtherResponse; + using QuestionWithOtherOption::RemoveOtherResponse; + }; + + MITKFORMS_EXPORT void from_json(const nlohmann::ordered_json& j, CheckboxesQuestion& q); + MITKFORMS_EXPORT void to_json(nlohmann::ordered_json& j, const CheckboxesQuestion& q); +} + +#endif diff --git a/Modules/Forms/include/mitkDropdownQuestion.h b/Modules/Forms/include/mitkDropdownQuestion.h new file mode 100644 index 0000000000..ac93d62b3b --- /dev/null +++ b/Modules/Forms/include/mitkDropdownQuestion.h @@ -0,0 +1,44 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#ifndef mitkDropdownQuestion_h +#define mitkDropdownQuestion_h + +#include + +namespace mitk::Forms +{ + /** \brief A Question whose possible responses are represented by a combo box. + * + * The question can have a single response. + * + * \sa MultipleChoiceQuestion, CheckboxesQuestion + */ + class MITKFORMS_EXPORT DropdownQuestion : public QuestionWithOptions + { + public: + ~DropdownQuestion() override; + + std::string GetType() const override; + Question* CreateAnother() const override; + + void FromJSON(const nlohmann::ordered_json& j) override; + void ToJSON(nlohmann::ordered_json& j) const override; + + using QuestionWithOptions::SetResponse; + }; + + MITKFORMS_EXPORT void from_json(const nlohmann::ordered_json& j, DropdownQuestion& q); + MITKFORMS_EXPORT void to_json(nlohmann::ordered_json& j, const DropdownQuestion& q); +} + +#endif diff --git a/Modules/Forms/include/mitkForm.h b/Modules/Forms/include/mitkForm.h new file mode 100644 index 0000000000..fde5796820 --- /dev/null +++ b/Modules/Forms/include/mitkForm.h @@ -0,0 +1,99 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#ifndef mitkForm_h +#define mitkForm_h + +#include +#include + +#include +#include +#include + +#include + +namespace mitk::Forms +{ + class Question; + + /** A form consisting of questions possibly divided into multiple sections/pages. + * + * A form always has at least a single section, which is also used to define the form's general + * title and description. Helper methods like SetTitle() or SetDescription() can be used to + * conveniently set these properties of the first section. + */ + class MITKFORMS_EXPORT Form + { + public: + class MITKFORMS_EXPORT Section + { + public: + explicit Section(const std::string& title = "", const std::string& description = ""); + ~Section(); + + Section(Section&& other) noexcept; + Section& operator=(Section&& other) noexcept; + + std::string GetTitle() const; + void SetTitle(const std::string& title); + + std::string GetDescription() const; + void SetDescription(const std::string& description); + + std::vector GetQuestions() const; + void AddQuestion(Question* question); + + private: + std::string m_Title; + std::string m_Description; + std::vector> m_Questions; + }; + + explicit Form(const std::string& title = "", const std::string& description = ""); + ~Form(); + + Form(Form&& other) noexcept; + Form& operator=(Form&& other) noexcept; + + Section& AddSection(const std::string& title = "", const std::string& description = ""); + int GetNumberOfSections() const; + + Section& GetSection(int index); + const Section& GetSection(int index) const; + + std::string GetTitle() const; + void SetTitle(const std::string& title); + + std::string GetDescription() const; + void SetDescription(const std::string& description); + + std::vector GetQuestions() const; + void AddQuestion(Question* question); + + std::vector
::const_iterator begin() const; + std::vector
::const_iterator end() const; + + std::vector
::iterator begin(); + std::vector
::iterator end(); + + private: + std::vector
m_Sections; + }; + + MITKFORMS_EXPORT void SubmitToCSV(const Form& form, const fs::path& csvPath); + + MITKFORMS_EXPORT void from_json(const nlohmann::ordered_json& j, Form& f); + MITKFORMS_EXPORT void to_json(nlohmann::ordered_json& j, const Form& f); +} + +#endif diff --git a/Modules/Forms/include/mitkIQuestionFactory.h b/Modules/Forms/include/mitkIQuestionFactory.h new file mode 100644 index 0000000000..6dcd8fffa6 --- /dev/null +++ b/Modules/Forms/include/mitkIQuestionFactory.h @@ -0,0 +1,61 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#ifndef mitkIQuestionFactory_h +#define mitkIQuestionFactory_h + +#include +#include + +#include + +namespace mitk::Forms +{ + class Question; + + /** \brief Register subclasses of Question to create instances of them based on their type string. + * + * This is a service interface. Obtain a pointer to its single instance via GetInstance(). + * + * Each Question subclass must be registered by calling Register(), which is typically done + * in the module activator class. After Question subclasses are registered, instances of them + * can be created with Create() based on their type string. This mechanism is primarily used + * for the deserialization of questions. + */ + class MITKFORMS_EXPORT IQuestionFactory + { + public: + /** \brief Obtain a pointer to the single instance of this service. + */ + static IQuestionFactory* GetInstance(); + + virtual ~IQuestionFactory(); + + /** \brief Register a Question subclass for the instance creation based on its type string. + * + * The service takes over ownership of the passed Question pointer. + * + * \sa Question::GetType() + */ + virtual void Register(Question* question) = 0; + + /** \brief Create an instance of a Question subclass based on its type string. + * + * \sa Question::GetType(), Question::CreateAnother() + */ + virtual Question* Create(const std::string& type) const = 0; + }; +} + +MITK_DECLARE_SERVICE_INTERFACE(mitk::Forms::IQuestionFactory, "org.mitk.Forms.IQuestionFactory") + +#endif diff --git a/Modules/Forms/include/mitkLinearScaleQuestion.h b/Modules/Forms/include/mitkLinearScaleQuestion.h new file mode 100644 index 0000000000..c5b01cdce3 --- /dev/null +++ b/Modules/Forms/include/mitkLinearScaleQuestion.h @@ -0,0 +1,72 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#ifndef mitkLinearScaleQuestion_h +#define mitkLinearScaleQuestion_h + +#include +#include +#include + +namespace mitk::Forms +{ + /** \brief A Question whose possible responses are represented by a discrete linear scale. + * + * The question can have a single response, which is one of the numbers within the allowed + * range. The range must start with 0 or 1 and end with any number between 2 and 10 (1 to 5 + * by default). + */ + class MITKFORMS_EXPORT LinearScaleQuestion : public Question + { + public: + LinearScaleQuestion(); + ~LinearScaleQuestion() override; + + std::string GetType() const override; + Question* CreateAnother() const override; + + void FromJSON(const nlohmann::ordered_json& j) override; + void ToJSON(nlohmann::ordered_json& j) const override; + + std::vector GetResponsesAsStrings() const override; + void ClearResponses() override; + + bool IsComplete() const override; + + std::optional GetResponse() const; + void SetResponse(int response); + + std::pair GetRange() const; + + /** \brief Set the allowed range of numbers for a valid response. + * + * A valid range must start with 0 or 1 and end with any number between 2 and 10 (1 to 5 + * by default). + * + * \exception Exception Invalid range. + */ + void SetRange(const std::pair& range); + + std::pair GetRangeLabels() const; + void SetRangeLabels(const std::pair& labels); + + private: + std::optional m_Response; + std::pair m_Range; + std::pair m_Labels; + }; + + MITKFORMS_EXPORT void from_json(const nlohmann::ordered_json& j, LinearScaleQuestion& q); + MITKFORMS_EXPORT void to_json(nlohmann::ordered_json& j, const LinearScaleQuestion& q); +} + +#endif diff --git a/Modules/Forms/include/mitkMultipleChoiceQuestion.h b/Modules/Forms/include/mitkMultipleChoiceQuestion.h new file mode 100644 index 0000000000..9ff48b749f --- /dev/null +++ b/Modules/Forms/include/mitkMultipleChoiceQuestion.h @@ -0,0 +1,45 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#ifndef mitkMultipleChoiceQuestion_h +#define mitkMultipleChoiceQuestion_h + +#include + +namespace mitk::Forms +{ + /** \brief A Question whose possible responses are represented by radio buttons. + * + * The question can have a single response, which can optionally be free text. + * + * \sa CheckboxesQuestion, DropdownQuestion + */ + class MITKFORMS_EXPORT MultipleChoiceQuestion : public QuestionWithOtherOption + { + public: + ~MultipleChoiceQuestion() override; + + std::string GetType() const override; + Question* CreateAnother() const override; + + void FromJSON(const nlohmann::ordered_json& j) override; + void ToJSON(nlohmann::ordered_json& j) const override; + + using QuestionWithOptions::SetResponse; + using QuestionWithOtherOption::SetOtherResponse; + }; + + MITKFORMS_EXPORT void from_json(const nlohmann::ordered_json& j, MultipleChoiceQuestion& q); + MITKFORMS_EXPORT void to_json(nlohmann::ordered_json& j, const MultipleChoiceQuestion& q); +} + +#endif diff --git a/Modules/Forms/include/mitkParagraphQuestion.h b/Modules/Forms/include/mitkParagraphQuestion.h new file mode 100644 index 0000000000..68574c8a0d --- /dev/null +++ b/Modules/Forms/include/mitkParagraphQuestion.h @@ -0,0 +1,40 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#ifndef mitkParagraphQuestion_h +#define mitkParagraphQuestion_h + +#include + +namespace mitk::Forms +{ + /** \brief A Question whose possible response is free text. + * + * \sa ShortAnswerQuestion + */ + class MITKFORMS_EXPORT ParagraphQuestion : public TextQuestion + { + public: + ~ParagraphQuestion() override; + + std::string GetType() const override; + Question* CreateAnother() const override; + + void FromJSON(const nlohmann::ordered_json& j) override; + void ToJSON(nlohmann::ordered_json& j) const override; + }; + + MITKFORMS_EXPORT void from_json(const nlohmann::ordered_json& j, ParagraphQuestion& q); + MITKFORMS_EXPORT void to_json(nlohmann::ordered_json& j, const ParagraphQuestion& q); +} + +#endif diff --git a/Modules/Forms/include/mitkQuestion.h b/Modules/Forms/include/mitkQuestion.h new file mode 100644 index 0000000000..56829ecb5a --- /dev/null +++ b/Modules/Forms/include/mitkQuestion.h @@ -0,0 +1,238 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#ifndef mitkQuestion_h +#define mitkQuestion_h + +#include + +#include +#include + +#include + +namespace mitk::Forms +{ + /** \brief Abstract base class for all types of questions used in a Form. + * + * Please make sure to read the full documentation of the pure virtual functions in particular to + * fully understand implications and requirements. + */ + class MITKFORMS_EXPORT Question + { + public: + Question(); + virtual ~Question(); + + /** \brief Get the literal question. + */ + std::string GetQuestionText() const; + + /** \brief Set the literal question. + */ + void SetQuestionText(const std::string& question); + + /** \brief Check whether a response to this question is required to complete a form. + */ + bool IsRequired() const; + + /** \brief Set whether a resonse to this question is required to complete a form. + * + * A question is not required by default. + */ + void SetRequired(bool required = true); + + /** \brief Get the text that should be displayed to clearly mark a question as required. + * + * This text is typically displayed as soon as a response to a required question is missing. + * The method can be overridden to customize the default text, e.g. for more complex types + * of questions, where specific guidance is beneficial. + * + * \sa IsComplete() + */ + virtual std::string GetRequiredText() const; + + /** \name Pure virtual functions + * + * Question is an abstract base class. Derive from this class to add a new type of question + * and override the following pure virtual functions. Please read the full documentation for + * all of these functions to fully understand implications and requirements. + * + * Do not forget to register any new type of question by calling IQuestionFactory::Register() + * like it is done in this module's activator class. + * + * \sa IQuestionFactory + * \{ + */ + + /** \brief Return the type of a question as string, e.g. "Multiple choice" or "Drop-down". + * + * This method is essential for the deserialization of questions into their correct type, resp. + * derived class. The type string is used by IQuestionFactory to look up a registered prototype + * instance of a certain type to create another instance of it. + * + * The type string does not have to match the class name or follow any other convention except + * for it must be unique amongst all question types. Prefer natural language like in the + * examples above in case it is used in a user interface to display a question's type. + * + * \code{.cpp} + * std::string RhetoricalQuestion::GetType() const + * { + * return "Rhetorical"; + * } + * \endcode + * + * \sa CreateAnother() + */ + virtual std::string GetType() const = 0; + + /** \brief Create a new instance of the derived question class type. + * + * This method is mainly used by IQuestionFactory to create new instances from registered + * prototype instances based on a type string. + * + * \code{.cpp} + * Question* RhetoricalQuestion::CreateAnother() const + * { + * return new RhetoricalQuestion; + * } + * \endcode + * + * \sa GetType() + */ + virtual Question* CreateAnother() const = 0; + + /** \brief Return the question's response(s) as strings. + * + * This is the single common generic interface for retrieving responses from all types + * of questions. It is typically used to store responses in text-based formats like CSV. + * + * It is implemented as vector since certain types of questions like a checkboxes question + * may support multiple responses. Otherwise, return a vector with a single element. + * + * \code{.cpp} + * std::vector RhetoricalQuestion::GetResponsesAsStrings() const + * { + * return { m_Response }; + * } + * \endcode + */ + virtual std::vector GetResponsesAsStrings() const = 0; + + /** \brief Clear the/all response(s). + */ + virtual void ClearResponses() = 0; + + /** \brief Check if a question is considered to be answered completely. + * + * This method is typically called when IsRequired() returns \c true to determine + * whether the requirements are fulfilled. + */ + virtual bool IsComplete() const = 0; + + /** \brief Deserialize from JSON. + * + * Polymorphism and the use of base class pointers make it necessary to implement serialization + * through member functions. Using the pure native approach of the "JSON for Modern C++" library + * via free functions would lead to partially serialized instances of derived classes. + * + * The actual implementation can and should still be located in a corresponding %from_json() + * free function but we also need the indirection through this member function. + * + * \code{.cpp} + * void RhetoricalQuestion::FromJSON(const nlohmann::ordered_json& j) + * { + * from_json(j, *this); + * } + * \endcode + * + * \sa from_json(const nlohmann::ordered_json& j, Question& q) + */ + virtual void FromJSON(const nlohmann::ordered_json& j) = 0; + + /** \brief Serialize to JSON. + * + * Polymorphism and the use of base class pointers make it necessary to implement serialization + * through member functions. Using the pure native approach of the "JSON for Modern C++" library + * via free functions would lead to partially serialized instances of derived classes. + * + * The actual implementation can and should still be located in a corresponding %to_json() free + * function but we also need the indirection through this member function. + * + * \code{.cpp} + * void RhetoricalQuestion::ToJSON(nlohmann::ordered_json& j) const + * { + * to_json(j, *this); + * } + * \endcode + * + * \sa to_json(nlohmann::ordered_json& j, const Question& q) + */ + virtual void ToJSON(nlohmann::ordered_json& j) const = 0; + + /**\}*/ + + private: + std::string m_QuestionText; + bool m_IsRequired; + }; + + /** \brief Deserialize from JSON. + * + * Polymorphism and the use of base class pointers make it necessary to implement serialization + * through member functions. Using the pure native approach of the "JSON for Modern C++" library + * via free functions would lead to partially serialized instances of derived classes. + * + * The actual implementation can and should still be located in a %from_json() free function but we + * also need the indirection through a corresponding member function. + * + * Overloaded functions for derived Question classes should reuse their base class versions of + * the function. + * + * \code{.cpp} + * void mitk::Forms::from_json(const ordered_json& j, RhetoricalQuestion& q) + * { + * from_json(j, static_cast(q)); + * // TODO: Deserialization of members exclusive to the derived class... + * } + * \endcode + * + * \sa Question::FromJSON() + */ + MITKFORMS_EXPORT void from_json(const nlohmann::ordered_json& j, Question& q); + + /** \brief Serialize to JSON. + * + * Polymorphism and the use of base class pointers make it necessary to implement serialization + * through member functions. Using the pure native approach of the "JSON for Modern C++" library + * via free functions would lead to partially serialized instances of derived classes. + * + * The actual implementation can and should still be located in a %to_json() free function but we + * also need the indirection through a corresponding member function. + * + * Overloaded functions for derived Question classes should reuse their base class versions of + * the function. + * + * \code{.cpp} + * void mitk::Forms::to_json(nlohmann::ordered_json& j, const RhetoricalQuestion& q) + * { + * to_json(j, static_cast(q)); + * // TODO: Serialization of members exclusive to the derived class... + * } + * \endcode + * + * \sa Question::ToJSON() + */ + MITKFORMS_EXPORT void to_json(nlohmann::ordered_json& j, const Question& q); +} + +#endif diff --git a/Modules/Forms/include/mitkQuestionWithOptions.h b/Modules/Forms/include/mitkQuestionWithOptions.h new file mode 100644 index 0000000000..4003ded473 --- /dev/null +++ b/Modules/Forms/include/mitkQuestionWithOptions.h @@ -0,0 +1,79 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#ifndef mitkQuestionWithOptions_h +#define mitkQuestionWithOptions_h + +#include +#include +#include + +namespace mitk::Forms +{ + /** \brief Base class for questions with options to choose from as response. + * + * This base class represents both, questions with mutually exclusive options as well as + * questions with multiple answer options. Derived classes should use using-declarations + * to change the visibility of the respective protected member functions to public, i.e., + * SetResponse() vs. AddResponse() and RemoveResponse(). + */ + class MITKFORMS_EXPORT QuestionWithOptions : public Question + { + public: + ~QuestionWithOptions() override; + + std::vector GetResponsesAsStrings() const override; + void ClearResponses() override; + + bool IsComplete() const override; + + /** \brief Add a non-exclusive option as possible answer to the question. + * + * The option is appended to the list of already existing options. + * + * \return The index of the option + */ + size_t AddOption(const std::string& option); + + std::vector GetOptions() const; + + protected: + /** \brief Add one of the possible answer options to the responses. + * + * Indexes to responses are inserted in ascending order. + * + * \sa AddOption() + */ + void AddResponse(size_t i); + + /** \brief Remove one of the already given responses. + * + * \sa AddOption(), AddResponse() + */ + void RemoveResponse(size_t i); + + /** \brief Set one of the possible answer options as the single response. + * + * \note This will remove any responses added by AddResponse(). + */ + virtual void SetResponse(size_t i); + + private: + std::vector m_Options; + std::set m_Responses; + }; + + MITKFORMS_EXPORT void from_json(const nlohmann::ordered_json& j, QuestionWithOptions& q); + MITKFORMS_EXPORT void to_json(nlohmann::ordered_json& j, const QuestionWithOptions& q); +} + +#endif diff --git a/Modules/Forms/include/mitkQuestionWithOtherOption.h b/Modules/Forms/include/mitkQuestionWithOtherOption.h new file mode 100644 index 0000000000..5850dcfb9d --- /dev/null +++ b/Modules/Forms/include/mitkQuestionWithOtherOption.h @@ -0,0 +1,79 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#ifndef mitkQuestionWithOtherOption_h +#define mitkQuestionWithOtherOption_h + +#include +#include + +namespace mitk::Forms +{ + /** \brief Base class for questions with options that may also have a free text option. + * + * This base class is an extension of QuestionWithOptions representing questions that + * may also have a free text option, typically labeled "Other" in forms/surveys. + * + * Just like with QuestionWithOptions, depending on the exclusivity of the answer + * options, use using-declarations in derived classes to change the visibility of + * the respective protected member functions to public, i.e., SetOtherResponse() + * vs. AddOtherResponse() and RemoveOtherResponse(). + */ + class MITKFORMS_EXPORT QuestionWithOtherOption : public QuestionWithOptions + { + public: + QuestionWithOtherOption(); + ~QuestionWithOtherOption() override; + + std::vector GetResponsesAsStrings() const override; + void ClearResponses() override; + + bool IsComplete() const override; + + /** \brief Query whether this question actually has an "Other" option. + */ + bool HasOtherOption() const; + + /** \brief Switch on the "Other" option. + * + * By default, a question does not have an "Other" option. + */ + void EnableOtherOption(); + + protected: + void SetResponse(size_t i) override; + + /** \brief Add the free text given as "Other" option to the list of responses. + * + * \note A question can only have a single "Other" response. Consequtive calls + * to this method will override the previously set "Other" response. + */ + void AddOtherResponse(const std::string& response); + + /** \brief Remove the "Other" response from the list of already given responses. + */ + void RemoveOtherResponse(); + + /** \brief Set the "Other" response as single exclusive response to this question. + */ + void SetOtherResponse(const std::string& response); + + private: + bool m_HasOtherOption; + std::optional m_OtherResponse; + }; + + MITKFORMS_EXPORT void from_json(const nlohmann::ordered_json& j, QuestionWithOtherOption& q); + MITKFORMS_EXPORT void to_json(nlohmann::ordered_json& j, const QuestionWithOtherOption& q); +} + +#endif diff --git a/Modules/Forms/include/mitkShortAnswerQuestion.h b/Modules/Forms/include/mitkShortAnswerQuestion.h new file mode 100644 index 0000000000..4d90af707e --- /dev/null +++ b/Modules/Forms/include/mitkShortAnswerQuestion.h @@ -0,0 +1,40 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#ifndef mitkShortAnswerQuestion_h +#define mitkShortAnswerQuestion_h + +#include + +namespace mitk::Forms +{ + /** \brief A Question whose possible response is a short single-line free text. + * + * \sa ParagraphQuestion + */ + class MITKFORMS_EXPORT ShortAnswerQuestion : public TextQuestion + { + public: + ~ShortAnswerQuestion() override; + + std::string GetType() const override; + Question* CreateAnother() const override; + + void FromJSON(const nlohmann::ordered_json& j) override; + void ToJSON(nlohmann::ordered_json& j) const override; + }; + + MITKFORMS_EXPORT void from_json(const nlohmann::ordered_json& j, ShortAnswerQuestion& q); + MITKFORMS_EXPORT void to_json(nlohmann::ordered_json& j, const ShortAnswerQuestion& q); +} + +#endif diff --git a/Modules/Forms/include/mitkTextQuestion.h b/Modules/Forms/include/mitkTextQuestion.h new file mode 100644 index 0000000000..ac88c1254d --- /dev/null +++ b/Modules/Forms/include/mitkTextQuestion.h @@ -0,0 +1,38 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#ifndef mitkTextQuestion_h +#define mitkTextQuestion_h + +#include + +namespace mitk::Forms +{ + class MITKFORMS_EXPORT TextQuestion : public Question + { + public: + ~TextQuestion() override; + + std::vector GetResponsesAsStrings() const override; + void ClearResponses() override; + + bool IsComplete() const override; + + std::string GetResponse() const; + void SetResponse(const std::string& response); + + private: + std::string m_Response; + }; +} + +#endif diff --git a/Modules/Forms/src/mitkCheckboxesQuestion.cpp b/Modules/Forms/src/mitkCheckboxesQuestion.cpp new file mode 100644 index 0000000000..4945d756c9 --- /dev/null +++ b/Modules/Forms/src/mitkCheckboxesQuestion.cpp @@ -0,0 +1,48 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#include + +using namespace mitk::Forms; +using namespace nlohmann; + +CheckboxesQuestion::~CheckboxesQuestion() = default; + +std::string CheckboxesQuestion::GetType() const +{ + return "Checkboxes"; +} + +Question* CheckboxesQuestion::CreateAnother() const +{ + return new CheckboxesQuestion; +} + +void CheckboxesQuestion::FromJSON(const nlohmann::ordered_json& j) +{ + from_json(j, *this); +} + +void CheckboxesQuestion::ToJSON(nlohmann::ordered_json& j) const +{ + to_json(j, *this); +} + +void mitk::Forms::from_json(const ordered_json& j, CheckboxesQuestion& q) +{ + from_json(j, static_cast(q)); +} + +void mitk::Forms::to_json(ordered_json& j, const CheckboxesQuestion& q) +{ + to_json(j, static_cast(q)); +} diff --git a/Modules/Forms/src/mitkDropdownQuestion.cpp b/Modules/Forms/src/mitkDropdownQuestion.cpp new file mode 100644 index 0000000000..4733262450 --- /dev/null +++ b/Modules/Forms/src/mitkDropdownQuestion.cpp @@ -0,0 +1,48 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#include + +using namespace mitk::Forms; +using namespace nlohmann; + +DropdownQuestion::~DropdownQuestion() = default; + +std::string DropdownQuestion::GetType() const +{ + return "Drop-down"; +} + +Question* DropdownQuestion::CreateAnother() const +{ + return new DropdownQuestion; +} + +void DropdownQuestion::FromJSON(const nlohmann::ordered_json& j) +{ + from_json(j, *this); +} + +void DropdownQuestion::ToJSON(nlohmann::ordered_json& j) const +{ + to_json(j, *this); +} + +void mitk::Forms::from_json(const ordered_json& j, DropdownQuestion& q) +{ + from_json(j, static_cast(q)); +} + +void mitk::Forms::to_json(ordered_json& j, const DropdownQuestion& q) +{ + to_json(j, static_cast(q)); +} diff --git a/Modules/Forms/src/mitkForm.cpp b/Modules/Forms/src/mitkForm.cpp new file mode 100644 index 0000000000..7b846c90c3 --- /dev/null +++ b/Modules/Forms/src/mitkForm.cpp @@ -0,0 +1,306 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#include + +#include +#include +#include + +#include + +#include +#include + +using namespace mitk::Forms; +using namespace nlohmann; + +namespace +{ + std::string GetCurrentISO8601DateTime() + { + std::timespec ts; + std::timespec_get(&ts, TIME_UTC); + std::array buffer{}; // YYYY-MM-DDThh:mm:ssZ + + std::strftime(buffer.data(), buffer.size(), "%FT%TZ", std::gmtime(&ts.tv_sec)); + + return buffer.data(); + } +} + +Form::Section::Section(const std::string& title, const std::string& description) + : m_Title(title), + m_Description(description) +{ +} + +Form::Section::~Section() = default; + +Form::Section::Section(Section&& other) noexcept = default; + +Form::Section& Form::Section::operator=(Section&& other) noexcept = default; + +std::string Form::Section::GetTitle() const +{ + return m_Title; +} + +void Form::Section::SetTitle(const std::string& title) +{ + m_Title = title; +} + +std::string Form::Section::GetDescription() const +{ + return m_Description; +} + +void Form::Section::SetDescription(const std::string& description) +{ + m_Description = description; +} + +std::vector Form::Section::GetQuestions() const +{ + std::vector questions; + questions.reserve(m_Questions.size()); + + for (const auto& question : m_Questions) + questions.push_back(question.get()); + + return questions; +} + +void Form::Section::AddQuestion(Question* question) +{ + m_Questions.emplace_back(question); +} + +Form::Form(const std::string& title, const std::string& description) +{ + m_Sections.emplace_back(title, description); +} + +Form::~Form() = default; + +Form::Form(Form&& other) noexcept = default; + +Form& Form::operator=(Form&& other) noexcept = default; + +Form::Section& Form::AddSection(const std::string& title, const std::string& description) +{ + return m_Sections.emplace_back(title, description); +} + +int Form::GetNumberOfSections() const +{ + return static_cast(m_Sections.size()); +} + +Form::Section& Form::GetSection(int index) +{ + return m_Sections.at(index); +} + +const Form::Section& Form::GetSection(int index) const +{ + return m_Sections.at(index); +} + +std::string Form::GetTitle() const +{ + return m_Sections[0].GetTitle(); +} + +void Form::SetTitle(const std::string& title) +{ + m_Sections[0].SetTitle(title); +} + +std::string Form::GetDescription() const +{ + return m_Sections[0].GetDescription(); +} + +void Form::SetDescription(const std::string& description) +{ + m_Sections[0].SetDescription(description); +} + +std::vector Form::GetQuestions() const +{ + return m_Sections[0].GetQuestions(); +} + +void Form::AddQuestion(Question* question) +{ + m_Sections[0].AddQuestion(question); +} + +std::vector::const_iterator Form::begin() const +{ + return m_Sections.begin(); +} + +std::vector::const_iterator Form::end() const +{ + return m_Sections.end(); +} + +std::vector::iterator Form::begin() +{ + return m_Sections.begin(); +} + +std::vector::iterator Form::end() +{ + return m_Sections.end(); +} + +void mitk::Forms::SubmitToCSV(const Form& form, const fs::path& csvPath) +{ + std::ofstream csvFile; + + if (fs::exists(csvPath)) + { + csvFile.open(csvPath, std::ofstream::app); + + if (!csvFile.is_open()) + mitkThrow() << "Could not open file \"" << csvPath << "\"!"; + } + else + { + csvFile.open(csvPath); + + if (!csvFile.is_open()) + mitkThrow() << "Could not create file \"" << csvPath << "\"!"; + + csvFile << "\"Timestamp\""; + + for (const auto& section : form) + { + for (const auto* question : section.GetQuestions()) + { + csvFile << ",\"" << question->GetQuestionText() << '"'; + } + } + + csvFile << '\n'; + } + + csvFile << '"' << GetCurrentISO8601DateTime() << '"'; + + for (const auto& section : form) + { + for (const auto* question : section.GetQuestions()) + { + csvFile << ",\""; + bool isFirstResponse = true; + + for (const auto& response : question->GetResponsesAsStrings()) + { + if (!isFirstResponse) + csvFile << ';'; + + csvFile << response; + + isFirstResponse = false; + } + + csvFile << '"'; + } + } + + csvFile << std::endl; +} + +void mitk::Forms::from_json(const nlohmann::ordered_json& j, Form& f) +{ + if (!j.contains("FileFormat") || j["FileFormat"] != "MITK Form") + mitkThrow() << "Expected \"FileFormat\" field to be \"MITK Form\"!"; + + if (!j.contains("Version") || j["Version"] != 1) + mitkThrow() << "Expected \"Version\" field to be 1!";; + + const auto* questionFactory = IQuestionFactory::GetInstance(); + + if (j.contains("Sections")) + { + bool isFirstSection = true; + + for (const auto& jSection : j["Sections"]) + { + std::string title; + std::string description; + + if (jSection.contains("Title")) + title = jSection["Title"]; + + if (jSection.contains("Description")) + description = jSection["Description"]; + + auto& section = isFirstSection + ? f.GetSection(0) + : f.AddSection(title, description); + + if (isFirstSection) + { + section.SetTitle(title); + section.SetDescription(description); + + isFirstSection = false; + } + + if (jSection.contains("Questions")) + { + for (const auto& jQuestion : jSection["Questions"]) + { + auto question = questionFactory->Create(jQuestion["Type"]); + question->FromJSON(jQuestion); + + section.AddQuestion(question); + } + } + } + } +} + +void mitk::Forms::to_json(nlohmann::ordered_json& j, const Form& f) +{ + j["FileFormat"] = "MITK Form"; + j["Version"] = 1; + + const int numberOfSections = f.GetNumberOfSections(); + + for (int index = 0; index < numberOfSections; ++index) + { + const auto& section = f.GetSection(index); + ordered_json jSection; + + if (auto title = section.GetTitle(); !title.empty()) + jSection["Title"] = title; + + if (auto description = section.GetDescription(); !description.empty()) + jSection["Description"] = description; + + for (const auto* question : section.GetQuestions()) + { + ordered_json jQuestion; + question->ToJSON(jQuestion); + + jSection["Questions"].push_back(jQuestion); + } + + j["Sections"].push_back(jSection); + } +} diff --git a/Modules/Forms/src/mitkIQuestionFactory.cpp b/Modules/Forms/src/mitkIQuestionFactory.cpp new file mode 100644 index 0000000000..e3a6ab5a26 --- /dev/null +++ b/Modules/Forms/src/mitkIQuestionFactory.cpp @@ -0,0 +1,34 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#include + +#include +#include +#include + +using namespace mitk::Forms; + +IQuestionFactory* IQuestionFactory::GetInstance() +{ + static auto context = us::GetModuleContext(); + static us::ServiceReference serviceReference; + + if (!serviceReference) + serviceReference = context->GetServiceReference(); + + return serviceReference + ? context->GetService(serviceReference) + : nullptr; +} + +IQuestionFactory::~IQuestionFactory() = default; diff --git a/Modules/Forms/src/mitkLinearScaleQuestion.cpp b/Modules/Forms/src/mitkLinearScaleQuestion.cpp new file mode 100644 index 0000000000..b2c55bc310 --- /dev/null +++ b/Modules/Forms/src/mitkLinearScaleQuestion.cpp @@ -0,0 +1,121 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#include +#include + +#include + +using namespace mitk::Forms; +using namespace nlohmann; + +LinearScaleQuestion::LinearScaleQuestion() + : m_Range({1, 5}) +{ +} + +LinearScaleQuestion::~LinearScaleQuestion() = default; + +std::string LinearScaleQuestion::GetType() const +{ + return "Linear scale"; +} + +Question* LinearScaleQuestion::CreateAnother() const +{ + return new LinearScaleQuestion; +} + +void LinearScaleQuestion::FromJSON(const nlohmann::ordered_json& j) +{ + from_json(j, *this); +} + +void LinearScaleQuestion::ToJSON(nlohmann::ordered_json& j) const +{ + to_json(j, *this); +} + +std::vector LinearScaleQuestion::GetResponsesAsStrings() const +{ + std::vector responses; + + if (m_Response.has_value()) + responses.push_back(std::to_string(m_Response.value())); + + return responses; +} + +void LinearScaleQuestion::ClearResponses() +{ + m_Response.reset(); +} + +bool LinearScaleQuestion::IsComplete() const +{ + return m_Response.has_value(); +} + +std::optional LinearScaleQuestion::GetResponse() const +{ + return m_Response; +} + +void LinearScaleQuestion::SetResponse(int response) +{ + m_Response = response; +} + +std::pair LinearScaleQuestion::GetRange() const +{ + return m_Range; +} + +void LinearScaleQuestion::SetRange(const std::pair& range) +{ + if (range.first < 0 || range.first > 1) + mitkThrow() << "Invalid lower bound " << range.first << " in range: must be 0 or 1!"; + + if (range.second < 2 || range.second > 10) + mitkThrow() << "Invalid upper bound " << range.second << " in range: must lie between 2 and 10!"; + + m_Range = range; +} + +std::pair LinearScaleQuestion::GetRangeLabels() const +{ + return m_Labels; +} + +void LinearScaleQuestion::SetRangeLabels(const std::pair& labels) +{ + m_Labels = labels; +} + +void mitk::Forms::from_json(const ordered_json& j, LinearScaleQuestion& q) +{ + from_json(j, static_cast(q)); + + if (j.contains("Range")) + q.SetRange(j["Range"]); + + if (j.contains("Labels")) + q.SetRangeLabels(j["Labels"]); +} + +void mitk::Forms::to_json(ordered_json& j, const LinearScaleQuestion& q) +{ + to_json(j, static_cast(q)); + + j["Range"] = q.GetRange(); + j["Labels"] = q.GetRangeLabels(); +} diff --git a/Modules/Forms/src/mitkModuleActivator.cpp b/Modules/Forms/src/mitkModuleActivator.cpp new file mode 100644 index 0000000000..362454bb27 --- /dev/null +++ b/Modules/Forms/src/mitkModuleActivator.cpp @@ -0,0 +1,50 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#include "mitkModuleActivator.h" +#include "mitkQuestionFactory.h" + +#include +#include +#include +#include +#include +#include + +#include + +using namespace mitk::Forms; + +ModuleActivator::ModuleActivator() + : m_QuestionFactory(std::make_unique()) +{ +} + +ModuleActivator::~ModuleActivator() = default; + +void ModuleActivator::Load(us::ModuleContext* context) +{ + context->RegisterService(m_QuestionFactory.get()); + + this->RegisterQuestion(); + this->RegisterQuestion(); + this->RegisterQuestion(); + this->RegisterQuestion(); + this->RegisterQuestion(); + this->RegisterQuestion(); +} + +void ModuleActivator::Unload(us::ModuleContext*) +{ +} + +US_EXPORT_MODULE_ACTIVATOR(ModuleActivator) diff --git a/Modules/Forms/src/mitkModuleActivator.h b/Modules/Forms/src/mitkModuleActivator.h new file mode 100644 index 0000000000..44f22607e2 --- /dev/null +++ b/Modules/Forms/src/mitkModuleActivator.h @@ -0,0 +1,42 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#ifndef mitkModuleActivator_h +#define mitkModuleActivator_h + +#include +#include +#include + +namespace mitk::Forms +{ + class ModuleActivator : public us::ModuleActivator + { + public: + ModuleActivator(); + ~ModuleActivator() override; + + void Load(us::ModuleContext* context) override; + void Unload(us::ModuleContext* context) override; + + private: + template + void RegisterQuestion() + { + m_QuestionFactory->Register(new TQuestion); + } + + std::unique_ptr m_QuestionFactory; + }; +} + +#endif diff --git a/Modules/Forms/src/mitkMultipleChoiceQuestion.cpp b/Modules/Forms/src/mitkMultipleChoiceQuestion.cpp new file mode 100644 index 0000000000..78ef357c4f --- /dev/null +++ b/Modules/Forms/src/mitkMultipleChoiceQuestion.cpp @@ -0,0 +1,48 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#include + +using namespace mitk::Forms; +using namespace nlohmann; + +MultipleChoiceQuestion::~MultipleChoiceQuestion() = default; + +std::string MultipleChoiceQuestion::GetType() const +{ + return "Multiple choice"; +} + +Question* MultipleChoiceQuestion::CreateAnother() const +{ + return new MultipleChoiceQuestion; +} + +void MultipleChoiceQuestion::FromJSON(const nlohmann::ordered_json& j) +{ + from_json(j, *this); +} + +void MultipleChoiceQuestion::ToJSON(nlohmann::ordered_json& j) const +{ + to_json(j, *this); +} + +void mitk::Forms::from_json(const ordered_json& j, MultipleChoiceQuestion& q) +{ + from_json(j, static_cast(q)); +} + +void mitk::Forms::to_json(ordered_json& j, const MultipleChoiceQuestion& q) +{ + to_json(j, static_cast(q)); +} diff --git a/Modules/Forms/src/mitkParagraphQuestion.cpp b/Modules/Forms/src/mitkParagraphQuestion.cpp new file mode 100644 index 0000000000..6348322d28 --- /dev/null +++ b/Modules/Forms/src/mitkParagraphQuestion.cpp @@ -0,0 +1,48 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#include + +using namespace mitk::Forms; +using namespace nlohmann; + +ParagraphQuestion::~ParagraphQuestion() = default; + +std::string ParagraphQuestion::GetType() const +{ + return "Paragraph"; +} + +Question* ParagraphQuestion::CreateAnother() const +{ + return new ParagraphQuestion; +} + +void ParagraphQuestion::FromJSON(const nlohmann::ordered_json& j) +{ + from_json(j, *this); +} + +void ParagraphQuestion::ToJSON(nlohmann::ordered_json& j) const +{ + to_json(j, *this); +} + +void mitk::Forms::from_json(const ordered_json& j, ParagraphQuestion& q) +{ + from_json(j, static_cast(q)); +} + +void mitk::Forms::to_json(ordered_json& j, const ParagraphQuestion& q) +{ + to_json(j, static_cast(q)); +} diff --git a/Modules/Forms/src/mitkQuestion.cpp b/Modules/Forms/src/mitkQuestion.cpp new file mode 100644 index 0000000000..ffc9fba686 --- /dev/null +++ b/Modules/Forms/src/mitkQuestion.cpp @@ -0,0 +1,68 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#include +#include + +using namespace mitk::Forms; +using namespace nlohmann; + +Question::Question() + : m_IsRequired(false) +{ +} + +Question::~Question() = default; + +std::string Question::GetQuestionText() const +{ + return m_QuestionText; +} + +void Question::SetQuestionText(const std::string& question) +{ + m_QuestionText = question; +} + +void Question::SetRequired(bool required) +{ + m_IsRequired = required; +} + +bool Question::IsRequired() const +{ + return m_IsRequired; +} + +std::string Question::GetRequiredText() const +{ + return "This is a required question"; +} + +void mitk::Forms::from_json(const ordered_json& j, Question& q) +{ + if (j.contains("Text")) + q.SetQuestionText(j["Text"]); + + if (j.contains("Required")) + q.SetRequired(j["Required"]); +} + +void mitk::Forms::to_json(ordered_json& j, const Question& q) +{ + j["Text"] = q.GetQuestionText(); + + if (q.IsRequired()) + j["Required"] = true; + + j["Type"] = q.GetType(); +} diff --git a/Modules/Forms/src/mitkQuestionFactory.cpp b/Modules/Forms/src/mitkQuestionFactory.cpp new file mode 100644 index 0000000000..26e77969c7 --- /dev/null +++ b/Modules/Forms/src/mitkQuestionFactory.cpp @@ -0,0 +1,47 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#include "mitkQuestionFactory.h" + +#include +#include + +using namespace mitk::Forms; + +QuestionFactory::QuestionFactory() = default; + +QuestionFactory::~QuestionFactory() +{ +} + +void QuestionFactory::Register(Question* question) +{ + auto type = question->GetType(); + + if (m_Prototypes.find(type) != m_Prototypes.end()) + mitkThrow() << "A prototype is already registered for questions of type \"" << type << "\"!"; + + m_Prototypes[type].reset(question); +} + +Question* QuestionFactory::Create(const std::string& type) const +{ + try + { + return m_Prototypes.at(type)->CreateAnother(); + } + catch (const std::out_of_range&) + { + } + + return nullptr; +} diff --git a/Modules/Forms/src/mitkQuestionFactory.h b/Modules/Forms/src/mitkQuestionFactory.h new file mode 100644 index 0000000000..30bbd418db --- /dev/null +++ b/Modules/Forms/src/mitkQuestionFactory.h @@ -0,0 +1,38 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#ifndef mitkQuestionFactory_h +#define mitkQuestionFactory_h + +#include + +#include +#include +#include + +namespace mitk::Forms +{ + class QuestionFactory : public IQuestionFactory + { + public: + QuestionFactory(); + ~QuestionFactory() override; + + void Register(Question* question) override; + Question* Create(const std::string& type) const override; + + private: + std::unordered_map> m_Prototypes; + }; +} + +#endif diff --git a/Modules/Forms/src/mitkQuestionWithOptions.cpp b/Modules/Forms/src/mitkQuestionWithOptions.cpp new file mode 100644 index 0000000000..101f44df8c --- /dev/null +++ b/Modules/Forms/src/mitkQuestionWithOptions.cpp @@ -0,0 +1,84 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#include +#include + +using namespace mitk::Forms; +using namespace nlohmann; + +QuestionWithOptions::~QuestionWithOptions() = default; + +size_t QuestionWithOptions::AddOption(const std::string& option) +{ + auto i = m_Options.size(); + m_Options.push_back(option); + return i; +} + +std::vector QuestionWithOptions::GetOptions() const +{ + return m_Options; +} + +void QuestionWithOptions::AddResponse(size_t i) +{ + m_Responses.insert(i); +} + +void QuestionWithOptions::RemoveResponse(size_t i) +{ + m_Responses.erase(i); +} + +void QuestionWithOptions::SetResponse(size_t i) +{ + m_Responses = { i }; +} + +std::vector QuestionWithOptions::GetResponsesAsStrings() const +{ + std::vector responses; + + for (const auto i : m_Responses) + responses.push_back(m_Options[i]); + + return responses; +} + +void QuestionWithOptions::ClearResponses() +{ + m_Responses.clear(); +} + +bool QuestionWithOptions::IsComplete() const +{ + return !m_Responses.empty(); +} + +void mitk::Forms::from_json(const ordered_json& j, QuestionWithOptions& q) +{ + from_json(j, static_cast(q)); + + if (j.contains("Options")) + { + for (const auto& option : j["Options"]) + q.AddOption(option); + } +} + +void mitk::Forms::to_json(ordered_json& j, const QuestionWithOptions& q) +{ + to_json(j, static_cast(q)); + + j["Options"] = q.GetOptions(); +} diff --git a/Modules/Forms/src/mitkQuestionWithOtherOption.cpp b/Modules/Forms/src/mitkQuestionWithOtherOption.cpp new file mode 100644 index 0000000000..eaa9f5ad97 --- /dev/null +++ b/Modules/Forms/src/mitkQuestionWithOtherOption.cpp @@ -0,0 +1,101 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#include +#include + +#include + +using namespace mitk::Forms; +using namespace nlohmann; + +QuestionWithOtherOption::QuestionWithOtherOption() + : m_HasOtherOption(false) +{ +} + +QuestionWithOtherOption::~QuestionWithOtherOption() = default; + +void QuestionWithOtherOption::EnableOtherOption() +{ + m_HasOtherOption = true; +} + +bool QuestionWithOtherOption::HasOtherOption() const +{ + return m_HasOtherOption; +} + +void QuestionWithOtherOption::SetResponse(size_t i) +{ + QuestionWithOptions::SetResponse(i); + m_OtherResponse.reset(); +} + +void QuestionWithOtherOption::AddOtherResponse(const std::string& response) +{ + if (!m_HasOtherOption) + mitkThrow() << "Adding \"Other\" response is disallowed!"; + + m_OtherResponse = response; +} + +void QuestionWithOtherOption::RemoveOtherResponse() +{ + m_OtherResponse.reset(); +} + +void QuestionWithOtherOption::SetOtherResponse(const std::string& response) +{ + if (!m_HasOtherOption) + mitkThrow() << "Setting \"Other\" response is disallowed!"; + + QuestionWithOptions::ClearResponses(); + m_OtherResponse = response; +} + +std::vector QuestionWithOtherOption::GetResponsesAsStrings() const +{ + auto responses = QuestionWithOptions::GetResponsesAsStrings(); + + if (m_OtherResponse.has_value()) + responses.push_back(m_OtherResponse.value()); + + return responses; +} + +void QuestionWithOtherOption::ClearResponses() +{ + QuestionWithOptions::ClearResponses(); + m_OtherResponse.reset(); +} + +bool QuestionWithOtherOption::IsComplete() const +{ + return QuestionWithOptions::IsComplete() || m_OtherResponse.has_value(); +} + +void mitk::Forms::from_json(const ordered_json& j, QuestionWithOtherOption& q) +{ + from_json(j, static_cast(q)); + + if (j.contains("Other") && j["Other"] == true) + q.EnableOtherOption(); +} + +void mitk::Forms::to_json(ordered_json& j, const QuestionWithOtherOption& q) +{ + to_json(j, static_cast(q)); + + if (q.HasOtherOption()) + j["Other"] = true; +} diff --git a/Modules/Forms/src/mitkShortAnswerQuestion.cpp b/Modules/Forms/src/mitkShortAnswerQuestion.cpp new file mode 100644 index 0000000000..9931e99c1e --- /dev/null +++ b/Modules/Forms/src/mitkShortAnswerQuestion.cpp @@ -0,0 +1,48 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#include + +using namespace mitk::Forms; +using namespace nlohmann; + +ShortAnswerQuestion::~ShortAnswerQuestion() = default; + +std::string ShortAnswerQuestion::GetType() const +{ + return "Short answer"; +} + +Question* ShortAnswerQuestion::CreateAnother() const +{ + return new ShortAnswerQuestion; +} + +void ShortAnswerQuestion::FromJSON(const nlohmann::ordered_json& j) +{ + from_json(j, *this); +} + +void ShortAnswerQuestion::ToJSON(nlohmann::ordered_json& j) const +{ + to_json(j, *this); +} + +void mitk::Forms::from_json(const ordered_json& j, ShortAnswerQuestion& q) +{ + from_json(j, static_cast(q)); +} + +void mitk::Forms::to_json(ordered_json& j, const ShortAnswerQuestion& q) +{ + to_json(j, static_cast(q)); +} diff --git a/Modules/Forms/src/mitkTextQuestion.cpp b/Modules/Forms/src/mitkTextQuestion.cpp new file mode 100644 index 0000000000..4b538eb262 --- /dev/null +++ b/Modules/Forms/src/mitkTextQuestion.cpp @@ -0,0 +1,42 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#include + +using namespace mitk::Forms; + +TextQuestion::~TextQuestion() = default; + +std::vector TextQuestion::GetResponsesAsStrings() const +{ + return { m_Response }; +} + +void TextQuestion::ClearResponses() +{ + m_Response.clear(); +} + +bool TextQuestion::IsComplete() const +{ + return !m_Response.empty(); +} + +std::string TextQuestion::GetResponse() const +{ + return m_Response; +} + +void TextQuestion::SetResponse(const std::string& response) +{ + m_Response = response; +} diff --git a/Modules/FormsUI/CMakeLists.txt b/Modules/FormsUI/CMakeLists.txt new file mode 100644 index 0000000000..2020d63fae --- /dev/null +++ b/Modules/FormsUI/CMakeLists.txt @@ -0,0 +1,9 @@ +mitk_create_module( + INCLUDE_DIRS + PUBLIC include + PRIVATE src + DEPENDS + PUBLIC MitkForms + PACKAGE_DEPENDS + PUBLIC Qt6|Widgets +) diff --git a/Modules/FormsUI/files.cmake b/Modules/FormsUI/files.cmake new file mode 100644 index 0000000000..3e2b067d18 --- /dev/null +++ b/Modules/FormsUI/files.cmake @@ -0,0 +1,40 @@ +set(H_FILES + include/mitkIQuestionWidgetFactory.h + include/QmitkCheckboxesQuestionWidget.h + include/QmitkDropdownQuestionWidget.h + include/QmitkForm.h + include/QmitkLinearScaleQuestionWidget.h + include/QmitkMultipleChoiceQuestionWidget.h + include/QmitkParagraphQuestionWidget.h + include/QmitkQuestionWidget.h + include/QmitkShortAnswerQuestionWidget.h +) + +set(MOC_H_FILES + include/QmitkCheckboxesQuestionWidget.h + include/QmitkDropdownQuestionWidget.h + include/QmitkLinearScaleQuestionWidget.h + include/QmitkForm.h + include/QmitkMultipleChoiceQuestionWidget.h + include/QmitkParagraphQuestionWidget.h + include/QmitkQuestionWidget.h + include/QmitkShortAnswerQuestionWidget.h +) + +set(UI_FILES + src/QmitkForm.ui +) + +set(CPP_FILES + mitkIQuestionWidgetFactory.cpp + mitkModuleActivator.cpp + mitkQuestionWidgetFactory.cpp + QmitkCheckboxesQuestionWidget.cpp + QmitkDropdownQuestionWidget.cpp + QmitkForm.cpp + QmitkLinearScaleQuestionWidget.cpp + QmitkMultipleChoiceQuestionWidget.cpp + QmitkParagraphQuestionWidget.cpp + QmitkQuestionWidget.cpp + QmitkShortAnswerQuestionWidget.cpp +) diff --git a/Modules/FormsUI/include/QmitkCheckboxesQuestionWidget.h b/Modules/FormsUI/include/QmitkCheckboxesQuestionWidget.h new file mode 100644 index 0000000000..65a3d214a7 --- /dev/null +++ b/Modules/FormsUI/include/QmitkCheckboxesQuestionWidget.h @@ -0,0 +1,52 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#ifndef QmitkCheckboxQuestionWidget_h +#define QmitkCheckboxQuestionWidget_h + +#include +#include + +class QButtonGroup; +class QGridLayout; +class QLineEdit; + +class MITKFORMSUI_EXPORT QmitkCheckboxesQuestionWidget : public QmitkQuestionWidget +{ + Q_OBJECT + +public: + explicit QmitkCheckboxesQuestionWidget(QWidget* parent = nullptr); + ~QmitkCheckboxesQuestionWidget() override; + + QmitkQuestionWidget* CreateAnother(QWidget* parent = nullptr) const override; + mitk::Forms::Question* GetQuestion() const override; + void SetQuestion(mitk::Forms::Question* question) override; + void Reset() override; + +private: + void CreateWidgets(); + void RemoveWidgets(); + + void OnIdToggled(int id, bool checked); + void OnTextEdited(const QString& text); + void OnEditingFinished(); + + mitk::Forms::CheckboxesQuestion* m_Question; + + QGridLayout* m_Layout; + QButtonGroup* m_ButtonGroup; + QLineEdit* m_OtherLineEdit; + int m_OtherId; +}; + +#endif diff --git a/Modules/FormsUI/include/QmitkDropdownQuestionWidget.h b/Modules/FormsUI/include/QmitkDropdownQuestionWidget.h new file mode 100644 index 0000000000..0814ac44df --- /dev/null +++ b/Modules/FormsUI/include/QmitkDropdownQuestionWidget.h @@ -0,0 +1,46 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#ifndef QmitkDropdownQuestionWidget_h +#define QmitkDropdownQuestionWidget_h + +#include +#include + +class QComboBox; + +class MITKFORMSUI_EXPORT QmitkDropdownQuestionWidget : public QmitkQuestionWidget +{ + Q_OBJECT + +public: + explicit QmitkDropdownQuestionWidget(QWidget* parent = nullptr); + ~QmitkDropdownQuestionWidget() override; + + QmitkQuestionWidget* CreateAnother(QWidget* parent = nullptr) const override; + mitk::Forms::Question* GetQuestion() const override; + void SetQuestion(mitk::Forms::Question* question) override; + void Reset() override; + +private: + void CreateWidgets(); + void RemoveWidgets(); + + void OnCurrentIndexChanged(int index); + + mitk::Forms::DropdownQuestion* m_Question; + + QVBoxLayout* m_Layout; + QComboBox* m_ComboBox; +}; + +#endif diff --git a/Modules/FormsUI/include/QmitkForm.h b/Modules/FormsUI/include/QmitkForm.h new file mode 100644 index 0000000000..52c777e3aa --- /dev/null +++ b/Modules/FormsUI/include/QmitkForm.h @@ -0,0 +1,62 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#ifndef QmitkForm_h +#define QmitkForm_h + +#include + +#include + +#include + +namespace Ui +{ + class QmitkForm; +} + +class MITKFORMSUI_EXPORT QmitkForm : public QWidget +{ + Q_OBJECT + +public: + explicit QmitkForm(mitk::Forms::Form& form, QWidget* parent = nullptr); + ~QmitkForm() override; + + fs::path GetResponsesPath() const; + void SetResponsesPath(const fs::path& csvPath); + +private: + void CreateQuestionWidgets(); + bool ValidateCurrentSection(); + void Reset(); + + void Update(); + void UpdateFormHeader(); + void UpdateSubmittedHeader(); + void UpdateSectionHeader(); + void UpdateQuestionWidgets(); + void UpdateFormButtons(); + + void OnBackButtonClicked(); + void OnNextButtonClicked(); + void OnSubmitButtonClicked(); + void OnClearButtonClicked(); + void OnSubmitAnotherButtonClicked(); + + Ui::QmitkForm* m_Ui; + mitk::Forms::Form& m_Form; + fs::path m_ResponsesPath; + bool m_HasBeenSubmitted; +}; + +#endif diff --git a/Modules/FormsUI/include/QmitkLinearScaleQuestionWidget.h b/Modules/FormsUI/include/QmitkLinearScaleQuestionWidget.h new file mode 100644 index 0000000000..5a14ff4d34 --- /dev/null +++ b/Modules/FormsUI/include/QmitkLinearScaleQuestionWidget.h @@ -0,0 +1,50 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#ifndef QmitkLinearScaleQuestionWidget_h +#define QmitkLinearScaleQuestionWidget_h + +#include +#include + +class QButtonGroup; +class QGridLayout; +class QPushButton; + +class MITKFORMSUI_EXPORT QmitkLinearScaleQuestionWidget : public QmitkQuestionWidget +{ + Q_OBJECT + +public: + explicit QmitkLinearScaleQuestionWidget(QWidget* parent = nullptr); + ~QmitkLinearScaleQuestionWidget() override; + + QmitkQuestionWidget* CreateAnother(QWidget* parent = nullptr) const override; + mitk::Forms::Question* GetQuestion() const override; + void SetQuestion(mitk::Forms::Question* question) override; + void Reset() override; + +private: + void CreateWidgets(); + void RemoveWidgets(); + + void OnIdClicked(int id); + void OnClearButtonClicked(); + + mitk::Forms::LinearScaleQuestion* m_Question; + + QGridLayout* m_Layout; + QButtonGroup* m_ButtonGroup; + QPushButton* m_ClearButton; +}; + +#endif diff --git a/Modules/FormsUI/include/QmitkMultipleChoiceQuestionWidget.h b/Modules/FormsUI/include/QmitkMultipleChoiceQuestionWidget.h new file mode 100644 index 0000000000..db0d58a697 --- /dev/null +++ b/Modules/FormsUI/include/QmitkMultipleChoiceQuestionWidget.h @@ -0,0 +1,55 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#ifndef QmitkMultipleChoiceQuestionWidget_h +#define QmitkMultipleChoiceQuestionWidget_h + +#include +#include + +class QButtonGroup; +class QGridLayout; +class QLineEdit; +class QPushButton; + +class MITKFORMSUI_EXPORT QmitkMultipleChoiceQuestionWidget : public QmitkQuestionWidget +{ + Q_OBJECT + +public: + explicit QmitkMultipleChoiceQuestionWidget(QWidget* parent = nullptr); + ~QmitkMultipleChoiceQuestionWidget() override; + + QmitkQuestionWidget* CreateAnother(QWidget* parent = nullptr) const override; + mitk::Forms::Question* GetQuestion() const override; + void SetQuestion(mitk::Forms::Question* question) override; + void Reset() override; + +private: + void CreateWidgets(); + void RemoveWidgets(); + + void OnIdClicked(int id); + void OnTextEdited(const QString& text); + void OnEditingFinished(); + void OnClearButtonClicked(); + + mitk::Forms::MultipleChoiceQuestion* m_Question; + + QGridLayout* m_Layout; + QButtonGroup* m_ButtonGroup; + QLineEdit* m_OtherLineEdit; + int m_OtherId; + QPushButton* m_ClearButton; +}; + +#endif diff --git a/Modules/FormsUI/include/QmitkParagraphQuestionWidget.h b/Modules/FormsUI/include/QmitkParagraphQuestionWidget.h new file mode 100644 index 0000000000..23590d541f --- /dev/null +++ b/Modules/FormsUI/include/QmitkParagraphQuestionWidget.h @@ -0,0 +1,44 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#ifndef QmitkParagraphQuestionWidget_h +#define QmitkParagraphQuestionWidget_h + +#include +#include + +class QTextEdit; + +class MITKFORMSUI_EXPORT QmitkParagraphQuestionWidget : public QmitkQuestionWidget +{ + Q_OBJECT + +public: + explicit QmitkParagraphQuestionWidget(QWidget* parent = nullptr); + ~QmitkParagraphQuestionWidget() override; + + QmitkQuestionWidget* CreateAnother(QWidget* parent = nullptr) const override; + mitk::Forms::Question* GetQuestion() const override; + void SetQuestion(mitk::Forms::Question* question) override; + void Reset() override; + +private: + void AdjustHeight(); + void OnTextChanged(); + + mitk::Forms::ParagraphQuestion* m_Question; + + QVBoxLayout* m_Layout; + QTextEdit* m_TextEdit; +}; + +#endif diff --git a/Modules/FormsUI/include/QmitkQuestionWidget.h b/Modules/FormsUI/include/QmitkQuestionWidget.h new file mode 100644 index 0000000000..28b61f1500 --- /dev/null +++ b/Modules/FormsUI/include/QmitkQuestionWidget.h @@ -0,0 +1,160 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#ifndef QmitkQuestionWidget_h +#define QmitkQuestionWidget_h + +#include +#include + +class QVBoxLayout; +class QLabel; + +namespace mitk::Forms +{ + class Question; +} + +/** \brief Abstract base class for all types of question widgets used in a QmitkForm. + * + * This class manages GUI elements common to all questions like labels for the question text and + * a reminder that a response might be required. All specific GUI elements of derived classes + * must be put into a layout and inserted by calling InsertLayout(). + * + * Please make sure to read the full documentation of the pure virtual functions in particular to + * fully understand implications and requirements. + */ +class MITKFORMSUI_EXPORT QmitkQuestionWidget : public QFrame +{ + Q_OBJECT + +public: + explicit QmitkQuestionWidget(QWidget* parent = nullptr); + ~QmitkQuestionWidget() override; + + /** \name Pure virtual functions + * + * QmitkQuestionWidget is an abstract base class. Derive from this class to add a widget for a + * certain type of Question and override the following pure virtual functions. Please read the + * full documentation for all of these functions to fully understand implications and requirements. + * + * Do not forget to register any new question widget by calling mitk::Forms::UI::IQuestionWidgetFactory::Register() + * like it is done in this module's activator class. + * + * \sa mitk::Forms::UI::IQuestionWidgetFactory + * \{ + */ + + /** \brief Create a new instance of the derived question widget class type. + * + * This method is mainly used by mitk::Forms::UI::IQuestionWidgetFactory to create + * new instances from registered prototype instances. + * + * \code{.cpp} + * QmitkQuestionWidget* QmitkRhetoricalQuestionWidget::CreateAnother(QWidget* parent) const + * { + * return new RhetoricalQuestionWidget(parent); + * } + * \endcode + */ + virtual QmitkQuestionWidget* CreateAnother(QWidget* parent = nullptr) const = 0; + + /** \brief Get the question associated with this widget. + * + * \code{.cpp} + * Question* QmitkRhetoricalQuestionWidget::GetQuestion() const + * { + * // In class declaration: mitk::Forms::RhetoricalQuestion* m_Question; + * return m_Question; + * } + * \endcode + * + * \sa SetQuestion() + */ + virtual mitk::Forms::Question* GetQuestion() const = 0; + + /** \brief Initialize the widget based on the given question. + * + * This method is rarely used explicitly since it is automatically called by + * mitk::Forms::UI::IQuestionWidgetFactory::Create(). + * + * You are excepted to throw an mitk::Exception if the Question type does not match + * the expectations of the widget. + * + * \note It is required to call this base class method at the beginning of the derived + * method. + * + * \code{.cpp} + * void QmitkRhetoricalQuestionWidget::SetQuestion(Question* question) + * { + * QmitkQuestionWidget::SetQuestion(question); + * + * auto rhetoricalQuestion = dynamic_cast(question); + * + * if (rhetoricalQuestion == nullptr) + * mitkThrow() << "QmitkRhetoricalQuestionWidget only accepts RhetoricalQuestion as question type!"; + * + * m_Question = rhetoricalQuestion; + * } + * \endcode + */ + virtual void SetQuestion(mitk::Forms::Question* question) = 0; + + /** \brief Reset the state of the GUI as if no interaction would have been happened yet. + * + * \note It is required to call this base class method at the end of the derived method. + * + * \code{.cpp} + * void QmitkRhetoricalQuestionWidget::Reset() + * { + * // It's a rhetorical question... nothing specific to be reset. + * QmitkQuestionWidget::Reset(); // Nevertheless, this is required at the end! + * } + * \endcode + */ + virtual void Reset() = 0; + + /**\}*/ + + void SetRequirementVisible(bool visible); + void ShowRequirement(); + void HideRequirement(); + +protected: + /** \brief Insert a layout containing all GUI elements specific to the derived question widget type. + * + * This method is typically called from the constructor of a derived class after all GUI elements + * have been set up and organized in a layout. + * + * \code{.cpp} + * QmitkRhetoricalQuestionWidget::QmitkRhetoricalQuestionWidget(QWidget* parent) + * : QmitkQuestionWidget(parent), + * m_Question(nullptr), + * m_Layout(new QVBoxLayout), + * m_Label(new QLabel) + * { + * m_Label->setText("You know the answer... we all do."); + * m_Layout->addWidget(m_Label); + * + * this->InsertLayout(m_Layout); + * } + * \endcode + */ + void InsertLayout(QLayout* layout); + +private: + QVBoxLayout* m_Layout; + QLabel* m_QuestionLabel; + QLabel* m_RequiredLabel; +}; + +#endif diff --git a/Modules/FormsUI/include/QmitkShortAnswerQuestionWidget.h b/Modules/FormsUI/include/QmitkShortAnswerQuestionWidget.h new file mode 100644 index 0000000000..a62359d6ee --- /dev/null +++ b/Modules/FormsUI/include/QmitkShortAnswerQuestionWidget.h @@ -0,0 +1,43 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#ifndef QmitkShortAnswerQuestionWidget_h +#define QmitkShortAnswerQuestionWidget_h + +#include +#include + +class QLineEdit; + +class MITKFORMSUI_EXPORT QmitkShortAnswerQuestionWidget : public QmitkQuestionWidget +{ + Q_OBJECT + +public: + explicit QmitkShortAnswerQuestionWidget(QWidget* parent = nullptr); + ~QmitkShortAnswerQuestionWidget() override; + + QmitkQuestionWidget* CreateAnother(QWidget* parent = nullptr) const override; + mitk::Forms::Question* GetQuestion() const override; + void SetQuestion(mitk::Forms::Question* question) override; + void Reset() override; + +private: + void OnTextEdited(const QString& text); + + mitk::Forms::ShortAnswerQuestion* m_Question; + + QVBoxLayout* m_Layout; + QLineEdit* m_LineEdit; +}; + +#endif diff --git a/Modules/FormsUI/include/mitkIQuestionWidgetFactory.h b/Modules/FormsUI/include/mitkIQuestionWidgetFactory.h new file mode 100644 index 0000000000..284946de2d --- /dev/null +++ b/Modules/FormsUI/include/mitkIQuestionWidgetFactory.h @@ -0,0 +1,65 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#ifndef mitkIQuestionWidgetFactory_h +#define mitkIQuestionWidgetFactory_h + +#include +#include + +#include + +namespace mitk::Forms +{ + class Question; + + namespace UI + { + /** \brief Register widgets for questions. + * + * This is a service interface. Obtain a pointer to its single instance via GetInstance(). + * + * Each QmitkQuestionWidget subclass must be registered by calling Register(), which is typically done + * in the module activator class. After QmitkQuestionWidget subclasses are registered, instances of them + * can be created with Create() based on their matching type of Question. + */ + class MITKFORMSUI_EXPORT IQuestionWidgetFactory + { + public: + /** \brief Obtain a pointer to the single instance of this service. + */ + static IQuestionWidgetFactory* GetInstance(); + + virtual ~IQuestionWidgetFactory(); + + /** \brief Register a QmitkQuestionWidget subclass for a certain Question type string. + * + * The service takes over ownership of the passed QmitkQuestionWidget pointer. + * + * \sa Question::GetType() + */ + virtual void Register(const std::string& questionType, QmitkQuestionWidget* widgetPrototype) = 0; + + /** \brief Create an instance of a matching QmitkQuestionWidget subclass for a certain question. + * + * The given question is passed to QmitkQuestionWidget::SetQuestion(). + * + * \sa QmitkQuestionWidget::CreateAnother() + */ + virtual QmitkQuestionWidget* Create(Question* question, QWidget* parent = nullptr) const = 0; + }; + } +} + +MITK_DECLARE_SERVICE_INTERFACE(mitk::Forms::UI::IQuestionWidgetFactory, "org.mitk.Forms.UI.IQuestionWidgetFactory") + +#endif diff --git a/Modules/FormsUI/src/QmitkCheckboxesQuestionWidget.cpp b/Modules/FormsUI/src/QmitkCheckboxesQuestionWidget.cpp new file mode 100644 index 0000000000..81a80f3a37 --- /dev/null +++ b/Modules/FormsUI/src/QmitkCheckboxesQuestionWidget.cpp @@ -0,0 +1,172 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#include +#include + +#include +#include +#include +#include + +using namespace mitk::Forms; +using Self = QmitkCheckboxesQuestionWidget; + +QmitkCheckboxesQuestionWidget::QmitkCheckboxesQuestionWidget(QWidget* parent) + : QmitkQuestionWidget(parent), + m_Question(nullptr), + m_Layout(new QGridLayout), + m_ButtonGroup(nullptr), + m_OtherLineEdit(nullptr), + m_OtherId(-1) +{ + this->InsertLayout(m_Layout); +} + +QmitkCheckboxesQuestionWidget::~QmitkCheckboxesQuestionWidget() +{ +} + +QmitkQuestionWidget* QmitkCheckboxesQuestionWidget::CreateAnother(QWidget* parent) const +{ + return new QmitkCheckboxesQuestionWidget(parent); +} + +Question* QmitkCheckboxesQuestionWidget::GetQuestion() const +{ + return m_Question; +} + +void QmitkCheckboxesQuestionWidget::SetQuestion(Question* question) +{ + QmitkQuestionWidget::SetQuestion(question); + + auto checkboxesQuestion = dynamic_cast(question); + + if (checkboxesQuestion == nullptr) + mitkThrow() << "QmitkCheckboxesQuestionWidget only accepts CheckboxesQuestion as question type!"; + + m_Question = checkboxesQuestion; + + this->CreateWidgets(); +} + +void QmitkCheckboxesQuestionWidget::Reset() +{ + if (m_ButtonGroup != nullptr) + { + for (auto checkBox : m_ButtonGroup->buttons()) + checkBox->setChecked(false); + } + + if (m_OtherLineEdit != nullptr) + m_OtherLineEdit->clear(); + + if (m_Question != nullptr) + m_Question->ClearResponses(); + + QmitkQuestionWidget::Reset(); +} + +void QmitkCheckboxesQuestionWidget::CreateWidgets() +{ + this->RemoveWidgets(); + + int row = 0; + + for (const auto& option : m_Question->GetOptions()) + { + auto checkBox = new QCheckBox(QString::fromStdString(option)); + m_Layout->addWidget(checkBox, row, 0, 1, 2); + m_ButtonGroup->addButton(checkBox, row++); + } + + if (m_Question->HasOtherOption()) + { + auto checkBox = new QCheckBox("Other:"); + m_Layout->addWidget(checkBox, row, 0); + m_ButtonGroup->addButton(checkBox, row); + m_OtherId = row; + + m_OtherLineEdit = new QLineEdit; + m_Layout->addWidget(m_OtherLineEdit, row++, 1); + + connect(m_OtherLineEdit, &QLineEdit::textEdited, this, &Self::OnTextEdited); + connect(m_OtherLineEdit, &QLineEdit::editingFinished, this, &Self::OnEditingFinished); + } + else + { + m_OtherId = -1; + } +} + +void QmitkCheckboxesQuestionWidget::RemoveWidgets() +{ + if (m_ButtonGroup != nullptr) + delete m_ButtonGroup; + + m_ButtonGroup = new QButtonGroup(this); + m_ButtonGroup->setExclusive(false); + + connect(m_ButtonGroup, &QButtonGroup::idToggled, this, &Self::OnIdToggled); + + QLayoutItem* child; + while ((child = m_Layout->takeAt(0)) != nullptr) + { + delete child->widget(); + delete child; + } + + m_OtherLineEdit = nullptr; +} + +void QmitkCheckboxesQuestionWidget::OnIdToggled(int id, bool checked) +{ + if (checked) + { + if (id == m_OtherId) + { + m_Question->AddOtherResponse(m_OtherLineEdit->text().toStdString()); + m_OtherLineEdit->setFocus(); + } + else + { + m_Question->AddResponse(static_cast(id)); + } + } + else + { + if (id == m_OtherId) + { + m_Question->RemoveOtherResponse(); + } + else + { + m_Question->RemoveResponse(static_cast(id)); + } + } + + if (m_Question->IsRequired()) + this->SetRequirementVisible(!m_Question->IsComplete()); +} + +void QmitkCheckboxesQuestionWidget::OnTextEdited(const QString&) +{ + if (!m_ButtonGroup->button(m_OtherId)->isChecked()) + m_ButtonGroup->button(m_OtherId)->click(); +} + +void QmitkCheckboxesQuestionWidget::OnEditingFinished() +{ + if (m_ButtonGroup->button(m_OtherId)->isChecked()) + m_Question->AddOtherResponse(m_OtherLineEdit->text().toStdString()); +} diff --git a/Modules/FormsUI/src/QmitkDropdownQuestionWidget.cpp b/Modules/FormsUI/src/QmitkDropdownQuestionWidget.cpp new file mode 100644 index 0000000000..91f6041b95 --- /dev/null +++ b/Modules/FormsUI/src/QmitkDropdownQuestionWidget.cpp @@ -0,0 +1,104 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#include +#include + +#include +#include + +using namespace mitk::Forms; +using Self = QmitkDropdownQuestionWidget; + +QmitkDropdownQuestionWidget::QmitkDropdownQuestionWidget(QWidget* parent) + : QmitkQuestionWidget(parent), + m_Question(nullptr), + m_Layout(new QVBoxLayout), + m_ComboBox(new QComboBox) +{ + m_Layout->addWidget(m_ComboBox); + + this->InsertLayout(m_Layout); + + connect(m_ComboBox, &QComboBox::currentIndexChanged, this, &Self::OnCurrentIndexChanged); +} + +QmitkDropdownQuestionWidget::~QmitkDropdownQuestionWidget() +{ +} + +QmitkQuestionWidget* QmitkDropdownQuestionWidget::CreateAnother(QWidget* parent) const +{ + return new QmitkDropdownQuestionWidget(parent); +} + +Question* QmitkDropdownQuestionWidget::GetQuestion() const +{ + return m_Question; +} + +void QmitkDropdownQuestionWidget::SetQuestion(Question* question) +{ + QmitkQuestionWidget::SetQuestion(question); + + auto dropdownQuestion = dynamic_cast(question); + + if (dropdownQuestion == nullptr) + mitkThrow() << "QmitkDropdownQuestionWidget only accepts DropdownQuestion as question type!"; + + m_Question = dropdownQuestion; + + this->CreateWidgets(); +} + +void QmitkDropdownQuestionWidget::Reset() +{ + m_ComboBox->setCurrentIndex(0); + + if (m_Question) + m_Question->ClearResponses(); + + QmitkQuestionWidget::Reset(); +} + +void QmitkDropdownQuestionWidget::CreateWidgets() +{ + this->RemoveWidgets(); + + for (const auto& option : m_Question->GetOptions()) + m_ComboBox->addItem(QString::fromStdString(option)); +} + +void QmitkDropdownQuestionWidget::RemoveWidgets() +{ + m_ComboBox->clear(); + m_ComboBox->addItem("Choose"); + m_ComboBox->insertSeparator(1); +} + +void QmitkDropdownQuestionWidget::OnCurrentIndexChanged(int index) +{ + if (m_Question == nullptr) + return; + + if (index > 1) + { + m_Question->SetResponse(static_cast(index - 2)); + } + else + { + m_Question->ClearResponses(); + } + + if (m_Question->IsRequired()) + this->SetRequirementVisible(!m_Question->IsComplete()); +} diff --git a/Modules/FormsUI/src/QmitkForm.cpp b/Modules/FormsUI/src/QmitkForm.cpp new file mode 100644 index 0000000000..8d9c7c9e37 --- /dev/null +++ b/Modules/FormsUI/src/QmitkForm.cpp @@ -0,0 +1,339 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#include +#include + +#include +#include +#include + +#include +#include +#include + +using namespace mitk::Forms; +using Self = QmitkForm; + +QmitkForm::QmitkForm(Form& form, QWidget* parent) + : QWidget(parent), + m_Ui(new Ui::QmitkForm), + m_Form(form), + m_HasBeenSubmitted(false) +{ + this->setStyleSheet( + "QFrame[frameShape=\"1\"] { border-radius: 6px; }" + "QComboBox, QComboBox::drop-down, QLineEdit, QPushButton { border-radius: 4px; }" + ); + + m_Ui->setupUi(this); + + this->CreateQuestionWidgets(); + this->Update(); + + connect(m_Ui->sectionWidget, &QStackedWidget::currentChanged, this, [this](int) { this->Update(); }); + + connect(m_Ui->backButton, &QPushButton::clicked, this, &Self::OnBackButtonClicked); + connect(m_Ui->nextButton, &QPushButton::clicked, this, &Self::OnNextButtonClicked); + connect(m_Ui->submitButton, &QPushButton::clicked, this, &Self::OnSubmitButtonClicked); + connect(m_Ui->clearButton, &QPushButton::clicked, this, &Self::OnClearButtonClicked); + connect(m_Ui->submitAnotherButton, &QPushButton::clicked, this, &Self::OnSubmitAnotherButtonClicked); +} + +QmitkForm::~QmitkForm() +{ +} + +fs::path QmitkForm::GetResponsesPath() const +{ + return m_ResponsesPath; +} + +void QmitkForm::SetResponsesPath(const fs::path& csvPath) +{ + m_ResponsesPath = csvPath; +} + +void QmitkForm::CreateQuestionWidgets() +{ + const int numberOfSections = m_Form.GetNumberOfSections(); + + for (int sectionIndex = 0; sectionIndex < numberOfSections; ++sectionIndex) + { + const auto& section = m_Form.GetSection(sectionIndex); + auto questions = section.GetQuestions(); + + auto sectionWidget = new QWidget; + m_Ui->sectionWidget->addWidget(sectionWidget); + + auto sectionLayout = new QVBoxLayout(sectionWidget); + sectionLayout->setContentsMargins(0, 0, 0, 0); + sectionLayout->setSpacing(8); + + for (auto question : questions) + { + auto questionWidget = UI::IQuestionWidgetFactory::GetInstance()->Create(question); + sectionLayout->addWidget(questionWidget); + } + } +} + +bool QmitkForm::ValidateCurrentSection() +{ + auto sectionWidget = m_Ui->sectionWidget->currentWidget(); + auto questionWidgets = sectionWidget->findChildren(); + + bool isComplete = true; + + for (auto questionWidget : questionWidgets) + { + auto question = questionWidget->GetQuestion(); + + if (question->IsRequired() && !question->IsComplete()) + { + questionWidget->ShowRequirement(); + isComplete = false; + } + } + + return isComplete; +} + +void QmitkForm::Reset() +{ + int numberOfSections = m_Ui->sectionWidget->count(); + + for (int sectionIndex = 0; sectionIndex < numberOfSections; ++sectionIndex) + { + auto sectionWidget = m_Ui->sectionWidget->widget(sectionIndex); + auto questionWidgets = sectionWidget->findChildren(); + + for (auto questionWidget : questionWidgets) + questionWidget->Reset(); + } + + m_HasBeenSubmitted = false; + m_Ui->sectionWidget->setCurrentIndex(0); +} + +void QmitkForm::Update() +{ + this->UpdateFormHeader(); + this->UpdateSubmittedHeader(); + this->UpdateSectionHeader(); + this->UpdateQuestionWidgets(); + this->UpdateFormButtons(); +} + +void QmitkForm::UpdateFormHeader() +{ + if (m_HasBeenSubmitted) + { + m_Ui->formHeaderFrame->hide(); + return; + } + + bool showTitle = !m_Form.GetTitle().empty(); + + m_Ui->formTitleLabel->setVisible(showTitle); + + if (showTitle) + m_Ui->formTitleLabel->setText(QString::fromStdString(m_Form.GetTitle())); + + int sectionIndex = m_Ui->sectionWidget->currentIndex(); + bool showDescription = sectionIndex == 0 && !m_Form.GetDescription().empty(); + + m_Ui->formDescriptionLabel->setVisible(showDescription); + + if (showDescription) + m_Ui->formDescriptionLabel->setText(QString::fromStdString(m_Form.GetDescription())); + + bool hasRequiredQuestion = false; + + for (auto question : m_Form.GetSection(sectionIndex).GetQuestions()) + { + if (question->IsRequired()) + { + hasRequiredQuestion = true; + break; + } + } + + m_Ui->requiredLabel->setVisible(hasRequiredQuestion); + + m_Ui->formHeaderFrame->setVisible(showTitle || showDescription || hasRequiredQuestion); +} + +void QmitkForm::UpdateSubmittedHeader() +{ + if (m_HasBeenSubmitted) + { + bool showTitle = !m_Form.GetTitle().empty(); + + m_Ui->submittedTitleLabel->setVisible(showTitle); + + if (showTitle) + m_Ui->submittedTitleLabel->setText(QString::fromStdString(m_Form.GetTitle())); + + m_Ui->submittedFrame->show(); + } + else + { + m_Ui->submittedFrame->hide(); + } +} + +void QmitkForm::UpdateSectionHeader() +{ + if (m_HasBeenSubmitted) + { + m_Ui->sectionHeaderFrame->hide(); + return; + } + + int sectionIndex = m_Ui->sectionWidget->currentIndex(); + + if (sectionIndex == 0) + { + m_Ui->sectionHeaderFrame->hide(); + return; + } + + const auto& section = m_Form.GetSection(sectionIndex); + bool showTitle = !section.GetTitle().empty(); + + m_Ui->sectionTitleLabel->setVisible(showTitle); + + if (showTitle) + m_Ui->sectionTitleLabel->setText(QString::fromStdString(section.GetTitle())); + + bool showDescription = !section.GetDescription().empty(); + + m_Ui->sectionDescriptionLabel->setVisible(showDescription); + + if (showDescription) + m_Ui->sectionDescriptionLabel->setText(QString::fromStdString(section.GetDescription())); + + m_Ui->sectionHeaderFrame->setVisible(showTitle || showDescription); +} + +void QmitkForm::UpdateQuestionWidgets() +{ + if (m_HasBeenSubmitted) + { + m_Ui->sectionWidget->hide(); + return; + } + else + { + m_Ui->sectionWidget->show(); + } + + int currentSectionIndex = m_Ui->sectionWidget->currentIndex(); + int numberOfSections = m_Ui->sectionWidget->count(); + + for (int sectionIndex = 0; sectionIndex < numberOfSections; ++sectionIndex) + { + auto sectionWidget = m_Ui->sectionWidget->widget(sectionIndex); + auto questionWidgets = sectionWidget->findChildren(); + + for (auto questionWidget : questionWidgets) + questionWidget->setVisible(sectionIndex == currentSectionIndex); + } +} + +void QmitkForm::UpdateFormButtons() +{ + int sectionIndex = m_Ui->sectionWidget->currentIndex(); + + m_Ui->backButton->setVisible(!m_HasBeenSubmitted && sectionIndex != 0); + m_Ui->nextButton->setVisible(!m_HasBeenSubmitted && sectionIndex < m_Ui->sectionWidget->count() - 1); + m_Ui->submitButton->setVisible(!m_HasBeenSubmitted && sectionIndex == m_Ui->sectionWidget->count() - 1); + m_Ui->clearButton->setVisible(!m_HasBeenSubmitted); +} + +void QmitkForm::OnBackButtonClicked() +{ + m_Ui->sectionWidget->setCurrentIndex(m_Ui->sectionWidget->currentIndex() - 1); +} + +void QmitkForm::OnNextButtonClicked() +{ + if (this->ValidateCurrentSection()) + m_Ui->sectionWidget->setCurrentIndex(m_Ui->sectionWidget->currentIndex() + 1); +} + +void QmitkForm::OnSubmitButtonClicked() +{ + if (this->ValidateCurrentSection()) + { + if (m_ResponsesPath.empty()) + { + m_ResponsesPath = QFileDialog::getSaveFileName(this, + "Submit Form", + QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation).append("/form.csv"), + "Comma-separated values (*.csv)").toStdString(); + } + + if (!m_ResponsesPath.empty()) + { + bool retry; + + do + { + retry = false; + + try + { + mitk::Forms::SubmitToCSV(m_Form, m_ResponsesPath); + } + catch (const mitk::Exception& e) + { + QMessageBox messageBox; + messageBox.setWindowTitle("Submit form"); + messageBox.setIcon(QMessageBox::Warning); + messageBox.setText(e.GetDescription()); + auto retryButton = messageBox.addButton("Retry", QMessageBox::NoRole); + messageBox.addButton("Ignore", QMessageBox::YesRole); + + messageBox.exec(); + + if (messageBox.clickedButton() == retryButton) + retry = true; + } + } while (retry); + + m_HasBeenSubmitted = true; + this->Update(); + } + } +} + +void QmitkForm::OnClearButtonClicked() +{ + QMessageBox messageBox; + messageBox.setWindowTitle("Clear form?"); + messageBox.setIcon(QMessageBox::Question); + messageBox.setText("This will remove your answers from all\nquestions and cannot be undone."); + messageBox.addButton("Cancel", QMessageBox::NoRole); + auto clearFormButton = messageBox.addButton("Clear form", QMessageBox::YesRole); + + messageBox.exec(); + + if (messageBox.clickedButton() == clearFormButton) + this->Reset(); +} + +void QmitkForm::OnSubmitAnotherButtonClicked() +{ + this->Reset(); +} diff --git a/Modules/FormsUI/src/QmitkForm.ui b/Modules/FormsUI/src/QmitkForm.ui new file mode 100644 index 0000000000..8b38ff5419 --- /dev/null +++ b/Modules/FormsUI/src/QmitkForm.ui @@ -0,0 +1,247 @@ + + + QmitkForm + + + + 0 + 0 + 600 + 313 + + + + + 0 + 0 + + + + + 8 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + QFrame::Box + + + + + + + 16 + + + + Form Title + + + true + + + + + + + Form description. + + + true + + + + + + + <span style="color: red;">* Indicates required question</span> + + + true + + + + + + + + + + QFrame::Box + + + QFrame::Plain + + + + + + + 16 + + + + Form Title + + + true + + + + + + + Your response has been recorded. + + + true + + + + + + + Submit another response + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + 0 + 0 + + + + QFrame::Box + + + + + + + 12 + + + + Section Title + + + true + + + + + + + Section description. + + + true + + + + + + + + + + + 0 + 0 + + + + + + + + QLayout::SetMaximumSize + + + + + Back + + + + + + + Next + + + + + + + Submit + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Clear form + + + true + + + + + + + + + + diff --git a/Modules/FormsUI/src/QmitkLinearScaleQuestionWidget.cpp b/Modules/FormsUI/src/QmitkLinearScaleQuestionWidget.cpp new file mode 100644 index 0000000000..2a557fc2f7 --- /dev/null +++ b/Modules/FormsUI/src/QmitkLinearScaleQuestionWidget.cpp @@ -0,0 +1,161 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#include +#include + +#include +#include +#include +#include +#include + +using namespace mitk::Forms; +using Self = QmitkLinearScaleQuestionWidget; + +QmitkLinearScaleQuestionWidget::QmitkLinearScaleQuestionWidget(QWidget* parent) + : QmitkQuestionWidget(parent), + m_Question(nullptr), + m_Layout(new QGridLayout), + m_ButtonGroup(nullptr), + m_ClearButton(nullptr) +{ + this->InsertLayout(m_Layout); +} + +QmitkLinearScaleQuestionWidget::~QmitkLinearScaleQuestionWidget() +{ +} + +QmitkQuestionWidget* QmitkLinearScaleQuestionWidget::CreateAnother(QWidget* parent) const +{ + return new QmitkLinearScaleQuestionWidget(parent); +} + +Question* QmitkLinearScaleQuestionWidget::GetQuestion() const +{ + return m_Question; +} + +void QmitkLinearScaleQuestionWidget::SetQuestion(Question* question) +{ + QmitkQuestionWidget::SetQuestion(question); + + auto linearScaleQuestion = dynamic_cast(question); + + if (linearScaleQuestion == nullptr) + mitkThrow() << "QmitkLinearScaleQuestionWidget only accepts LinearScaleQuestion as question type!"; + + m_Question = linearScaleQuestion; + + this->CreateWidgets(); +} + +void QmitkLinearScaleQuestionWidget::Reset() +{ + this->OnClearButtonClicked(); + + if (m_Question) + m_Question->ClearResponses(); + + QmitkQuestionWidget::Reset(); +} + +void QmitkLinearScaleQuestionWidget::CreateWidgets() +{ + this->RemoveWidgets(); + + auto [from, to] = m_Question->GetRange(); + auto [fromText, toText] = m_Question->GetRangeLabels(); + int column = 0; + + if (!fromText.empty()) + { + auto fromLabel = new QLabel(QString::fromStdString(fromText)); + m_Layout->addWidget(fromLabel, 1, column++, Qt::AlignRight); + } + + for (int i = from; i <= to; ++i) + { + auto label = new QLabel(QString("%1").arg(i)); + + auto radioButton = new QRadioButton; + radioButton->setFixedWidth(radioButton->sizeHint().height()); + + m_Layout->addWidget(label, 0, column, Qt::AlignHCenter); + m_Layout->addWidget(radioButton, 1, column++, Qt::AlignHCenter); + + m_ButtonGroup->addButton(radioButton, i); + } + + if (!toText.empty()) + { + auto toLabel = new QLabel(QString::fromStdString(toText)); + m_Layout->addWidget(toLabel, 1, column++, Qt::AlignLeft); + } + + if (m_Question->IsRequired()) + return; + + m_ClearButton = new QPushButton("Clear selection"); + m_ClearButton->setFlat(true); + m_ClearButton->hide(); + + m_Layout->addWidget(m_ClearButton, 2, 0, 1, column, Qt::AlignRight); + + connect(m_ClearButton, &QPushButton::clicked, this, &Self::OnClearButtonClicked); +} + +void QmitkLinearScaleQuestionWidget::RemoveWidgets() +{ + if (m_ButtonGroup != nullptr) + delete m_ButtonGroup; + + m_ButtonGroup = new QButtonGroup(this); + + connect(m_ButtonGroup, &QButtonGroup::idClicked, this, &Self::OnIdClicked); + + QLayoutItem* child; + while ((child = m_Layout->takeAt(0)) != nullptr) + { + delete child->widget(); + delete child; + } + + m_ClearButton = nullptr; +} + +void QmitkLinearScaleQuestionWidget::OnIdClicked(int id) +{ + m_Question->SetResponse(id); + + if (m_ClearButton != nullptr) + m_ClearButton->show(); + + if (m_Question->IsRequired()) + this->SetRequirementVisible(!m_Question->IsComplete()); +} + +void QmitkLinearScaleQuestionWidget::OnClearButtonClicked() +{ + auto checkedButton = m_ButtonGroup->checkedButton(); + + if (checkedButton != nullptr) + { + m_ButtonGroup->setExclusive(false); + checkedButton->setChecked(false); + m_ButtonGroup->setExclusive(true); + } + + if (m_ClearButton != nullptr) + m_ClearButton->hide(); +} diff --git a/Modules/FormsUI/src/QmitkMultipleChoiceQuestionWidget.cpp b/Modules/FormsUI/src/QmitkMultipleChoiceQuestionWidget.cpp new file mode 100644 index 0000000000..0c74854685 --- /dev/null +++ b/Modules/FormsUI/src/QmitkMultipleChoiceQuestionWidget.cpp @@ -0,0 +1,185 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#include +#include + +#include +#include +#include +#include +#include + +using namespace mitk::Forms; +using Self = QmitkMultipleChoiceQuestionWidget; + +QmitkMultipleChoiceQuestionWidget::QmitkMultipleChoiceQuestionWidget(QWidget* parent) + : QmitkQuestionWidget(parent), + m_Question(nullptr), + m_Layout(new QGridLayout), + m_ButtonGroup(nullptr), + m_OtherLineEdit(nullptr), + m_OtherId(-1), + m_ClearButton(nullptr) +{ + this->InsertLayout(m_Layout); +} + +QmitkMultipleChoiceQuestionWidget::~QmitkMultipleChoiceQuestionWidget() +{ +} + +QmitkQuestionWidget* QmitkMultipleChoiceQuestionWidget::CreateAnother(QWidget* parent) const +{ + return new QmitkMultipleChoiceQuestionWidget(parent); +} + +Question* QmitkMultipleChoiceQuestionWidget::GetQuestion() const +{ + return m_Question; +} + +void QmitkMultipleChoiceQuestionWidget::SetQuestion(Question* question) +{ + QmitkQuestionWidget::SetQuestion(question); + + auto multipleChoiceQuestion = dynamic_cast(question); + + if (multipleChoiceQuestion == nullptr) + mitkThrow() << "QmitkMultipleChoiceQuestionWidget only accepts MultipleChoiceQuestion as question type!"; + + m_Question = multipleChoiceQuestion; + + this->CreateWidgets(); +} + +void QmitkMultipleChoiceQuestionWidget::Reset() +{ + this->OnClearButtonClicked(); + + if (m_OtherLineEdit != nullptr) + m_OtherLineEdit->clear(); + + if (m_Question != nullptr) + m_Question->ClearResponses(); + + QmitkQuestionWidget::Reset(); +} + +void QmitkMultipleChoiceQuestionWidget::CreateWidgets() +{ + this->RemoveWidgets(); + + int row = 0; + + for (const auto& option : m_Question->GetOptions()) + { + auto optionRadioButton = new QRadioButton(QString::fromStdString(option)); + m_Layout->addWidget(optionRadioButton, row, 0, 1, 2); + m_ButtonGroup->addButton(optionRadioButton, row++); + } + + if (m_Question->HasOtherOption()) + { + auto otherRadioButton = new QRadioButton("Other:"); + m_Layout->addWidget(otherRadioButton, row, 0); + m_ButtonGroup->addButton(otherRadioButton, row); + m_OtherId = row; + + m_OtherLineEdit = new QLineEdit; + m_Layout->addWidget(m_OtherLineEdit, row++, 1); + + connect(m_OtherLineEdit, &QLineEdit::textEdited, this, &Self::OnTextEdited); + connect(m_OtherLineEdit, &QLineEdit::editingFinished, this, &Self::OnEditingFinished); + } + else + { + m_OtherId = -1; + } + + if (m_Question->IsRequired()) + return; + + m_ClearButton = new QPushButton("Clear selection"); + m_ClearButton->setFlat(true); + m_ClearButton->hide(); + + m_Layout->addWidget(m_ClearButton, row, 0, 1, 2, Qt::AlignRight); + + connect(m_ClearButton, &QPushButton::clicked, this, &Self::OnClearButtonClicked); +} + +void QmitkMultipleChoiceQuestionWidget::RemoveWidgets() +{ + if (m_ButtonGroup != nullptr) + delete m_ButtonGroup; + + m_ButtonGroup = new QButtonGroup(this); + + connect(m_ButtonGroup, &QButtonGroup::idClicked, this, &Self::OnIdClicked); + + QLayoutItem* child; + while ((child = m_Layout->takeAt(0)) != nullptr) + { + delete child->widget(); + delete child; + } + + m_OtherLineEdit = nullptr; + m_ClearButton = nullptr; +} + +void QmitkMultipleChoiceQuestionWidget::OnIdClicked(int id) +{ + if (id == m_OtherId) + { + m_Question->SetOtherResponse(m_OtherLineEdit->text().toStdString()); + m_OtherLineEdit->setFocus(); + } + else + { + m_Question->SetResponse(static_cast(id)); + } + + if (m_ClearButton != nullptr) + m_ClearButton->show(); + + if (m_Question->IsRequired()) + this->SetRequirementVisible(!m_Question->IsComplete()); +} + +void QmitkMultipleChoiceQuestionWidget::OnTextEdited(const QString&) +{ + if (m_ButtonGroup->checkedId() != m_OtherId) + m_ButtonGroup->button(m_OtherId)->click(); +} + +void QmitkMultipleChoiceQuestionWidget::OnEditingFinished() +{ + if (m_ButtonGroup->checkedId() == m_OtherId) + m_Question->SetOtherResponse(m_OtherLineEdit->text().toStdString()); +} + +void QmitkMultipleChoiceQuestionWidget::OnClearButtonClicked() +{ + auto checkedButton = m_ButtonGroup->checkedButton(); + + if (checkedButton != nullptr) + { + m_ButtonGroup->setExclusive(false); + checkedButton->setChecked(false); + m_ButtonGroup->setExclusive(true); + } + + if (m_ClearButton != nullptr) + m_ClearButton->hide(); +} diff --git a/Modules/FormsUI/src/QmitkParagraphQuestionWidget.cpp b/Modules/FormsUI/src/QmitkParagraphQuestionWidget.cpp new file mode 100644 index 0000000000..5a2e1778f3 --- /dev/null +++ b/Modules/FormsUI/src/QmitkParagraphQuestionWidget.cpp @@ -0,0 +1,91 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#include +#include + +#include +#include +#include + +using namespace mitk::Forms; +using Self = QmitkParagraphQuestionWidget; + +QmitkParagraphQuestionWidget::QmitkParagraphQuestionWidget(QWidget* parent) + : QmitkQuestionWidget(parent), + m_Question(nullptr), + m_Layout(new QVBoxLayout), + m_TextEdit(new QTextEdit) +{ + m_TextEdit->setPlaceholderText("Your answer"); + m_Layout->addWidget(m_TextEdit); + + this->InsertLayout(m_Layout); + this->AdjustHeight(); + + connect(m_TextEdit, &QTextEdit::textChanged, this, &Self::OnTextChanged); +} + +QmitkParagraphQuestionWidget::~QmitkParagraphQuestionWidget() +{ +} + +QmitkQuestionWidget* QmitkParagraphQuestionWidget::CreateAnother(QWidget* parent) const +{ + return new QmitkParagraphQuestionWidget(parent); +} + +Question* QmitkParagraphQuestionWidget::GetQuestion() const +{ + return m_Question; +} + +void QmitkParagraphQuestionWidget::SetQuestion(Question* question) +{ + QmitkQuestionWidget::SetQuestion(question); + + auto paragraphQuestion = dynamic_cast(question); + + if (paragraphQuestion == nullptr) + mitkThrow() << "QmitkParagraphQuestionWidget only accepts ParagraphQuestion as question type!"; + + m_Question = paragraphQuestion; +} + +void QmitkParagraphQuestionWidget::Reset() +{ + m_TextEdit->clear(); + + if (m_Question) + m_Question->ClearResponses(); + + QmitkQuestionWidget::Reset(); +} + +void QmitkParagraphQuestionWidget::AdjustHeight() +{ + QMargins contentsMargins = m_TextEdit->contentsMargins(); + int documentHeight = static_cast(m_TextEdit->document()->documentLayout()->documentSize().height()); + int totalHeight = contentsMargins.top() + documentHeight + contentsMargins.bottom(); + + m_TextEdit->setFixedHeight(totalHeight); +} + +void QmitkParagraphQuestionWidget::OnTextChanged() +{ + m_Question->SetResponse(m_TextEdit->toPlainText().toStdString()); + + if (m_Question->IsRequired()) + this->SetRequirementVisible(!m_Question->IsComplete()); + + this->AdjustHeight(); +} diff --git a/Modules/FormsUI/src/QmitkQuestionWidget.cpp b/Modules/FormsUI/src/QmitkQuestionWidget.cpp new file mode 100644 index 0000000000..40f0e30ead --- /dev/null +++ b/Modules/FormsUI/src/QmitkQuestionWidget.cpp @@ -0,0 +1,94 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#include + +#include +#include + +#include +#include + +QmitkQuestionWidget::QmitkQuestionWidget(QWidget* parent) + : QFrame(parent), + m_Layout(new QVBoxLayout(this)), + m_QuestionLabel(new QLabel), + m_RequiredLabel(new QLabel) +{ + this->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); + this->setFrameShape(QFrame::Box); + + m_Layout->addWidget(m_QuestionLabel); + m_Layout->addWidget(m_RequiredLabel); + + m_QuestionLabel->setWordWrap(true); + + m_RequiredLabel->setWordWrap(true); + m_RequiredLabel->hide(); +} + +QmitkQuestionWidget::~QmitkQuestionWidget() +{ +} + +void QmitkQuestionWidget::SetQuestion(mitk::Forms::Question* question) +{ + if (question == nullptr) + mitkThrow() << "Invalid question: cannot be nullptr!"; + + auto text = QString::fromStdString(question->GetQuestionText()); + + if (question->IsRequired()) + { + if (!text.isEmpty()) + text.append(' '); + + text.append("*"); + } + + m_QuestionLabel->setText(text); + m_RequiredLabel->setText(QString("%1").arg(QString::fromStdString(question->GetRequiredText()))); +} + +void QmitkQuestionWidget::Reset() +{ + this->HideRequirement(); +} + +void QmitkQuestionWidget::SetRequirementVisible(bool visible) +{ + if (visible) + { + this->ShowRequirement(); + } + else + { + this->HideRequirement(); + } +} + +void QmitkQuestionWidget::ShowRequirement() +{ + this->setStyleSheet("QmitkQuestionWidget { border-color: red; }"); + m_RequiredLabel->show(); +} + +void QmitkQuestionWidget::HideRequirement() +{ + this->setStyleSheet(""); + m_RequiredLabel->hide(); +} + +void QmitkQuestionWidget::InsertLayout(QLayout* layout) +{ + m_Layout->insertLayout(1, layout); +} diff --git a/Modules/FormsUI/src/QmitkShortAnswerQuestionWidget.cpp b/Modules/FormsUI/src/QmitkShortAnswerQuestionWidget.cpp new file mode 100644 index 0000000000..4f45c01d85 --- /dev/null +++ b/Modules/FormsUI/src/QmitkShortAnswerQuestionWidget.cpp @@ -0,0 +1,78 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#include +#include + +#include +#include + +using namespace mitk::Forms; +using Self = QmitkShortAnswerQuestionWidget; + +QmitkShortAnswerQuestionWidget::QmitkShortAnswerQuestionWidget(QWidget* parent) + : QmitkQuestionWidget(parent), + m_Question(nullptr), + m_Layout(new QVBoxLayout), + m_LineEdit(new QLineEdit) +{ + m_LineEdit->setPlaceholderText("Your answer"); + m_Layout->addWidget(m_LineEdit); + + this->InsertLayout(m_Layout); + + connect(m_LineEdit, &QLineEdit::textEdited, this, &Self::OnTextEdited); +} + +QmitkShortAnswerQuestionWidget::~QmitkShortAnswerQuestionWidget() +{ +} + +QmitkQuestionWidget* QmitkShortAnswerQuestionWidget::CreateAnother(QWidget* parent) const +{ + return new QmitkShortAnswerQuestionWidget(parent); +} + +Question* QmitkShortAnswerQuestionWidget::GetQuestion() const +{ + return m_Question; +} + +void QmitkShortAnswerQuestionWidget::SetQuestion(Question* question) +{ + QmitkQuestionWidget::SetQuestion(question); + + auto shortAnswerQuestion = dynamic_cast(question); + + if (shortAnswerQuestion == nullptr) + mitkThrow() << "QmitkShortAnswerQuestionWidget only accepts ShortAnswerQuestion as question type!"; + + m_Question = shortAnswerQuestion; +} + +void QmitkShortAnswerQuestionWidget::Reset() +{ + m_LineEdit->clear(); + + if (m_Question) + m_Question->ClearResponses(); + + QmitkQuestionWidget::Reset(); +} + +void QmitkShortAnswerQuestionWidget::OnTextEdited(const QString& text) +{ + m_Question->SetResponse(text.toStdString()); + + if (m_Question->IsRequired()) + this->SetRequirementVisible(!m_Question->IsComplete()); +} diff --git a/Modules/FormsUI/src/mitkIQuestionWidgetFactory.cpp b/Modules/FormsUI/src/mitkIQuestionWidgetFactory.cpp new file mode 100644 index 0000000000..8895e58394 --- /dev/null +++ b/Modules/FormsUI/src/mitkIQuestionWidgetFactory.cpp @@ -0,0 +1,34 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#include + +#include +#include +#include + +using namespace mitk::Forms::UI; + +IQuestionWidgetFactory* IQuestionWidgetFactory::GetInstance() +{ + static auto context = us::GetModuleContext(); + static us::ServiceReference serviceReference; + + if (!serviceReference) + serviceReference = context->GetServiceReference(); + + return serviceReference + ? context->GetService(serviceReference) + : nullptr; +} + +IQuestionWidgetFactory::~IQuestionWidgetFactory() = default; diff --git a/Modules/FormsUI/src/mitkModuleActivator.cpp b/Modules/FormsUI/src/mitkModuleActivator.cpp new file mode 100644 index 0000000000..2c4eb80675 --- /dev/null +++ b/Modules/FormsUI/src/mitkModuleActivator.cpp @@ -0,0 +1,50 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#include "mitkModuleActivator.h" +#include "mitkQuestionWidgetFactory.h" + +#include +#include +#include +#include +#include +#include + +#include + +using namespace mitk::Forms::UI; + +ModuleActivator::ModuleActivator() + : m_QuestionWidgetFactory(std::make_unique()) +{ +} + +ModuleActivator::~ModuleActivator() = default; + +void ModuleActivator::Load(us::ModuleContext* context) +{ + context->RegisterService(m_QuestionWidgetFactory.get()); + + this->RegisterQuestionWidget(); + this->RegisterQuestionWidget(); + this->RegisterQuestionWidget(); + this->RegisterQuestionWidget(); + this->RegisterQuestionWidget(); + this->RegisterQuestionWidget(); +} + +void ModuleActivator::Unload(us::ModuleContext*) +{ +} + +US_EXPORT_MODULE_ACTIVATOR(ModuleActivator) diff --git a/Modules/FormsUI/src/mitkModuleActivator.h b/Modules/FormsUI/src/mitkModuleActivator.h new file mode 100644 index 0000000000..3832600cd4 --- /dev/null +++ b/Modules/FormsUI/src/mitkModuleActivator.h @@ -0,0 +1,43 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#ifndef mitkModuleActivator_h +#define mitkModuleActivator_h + +#include +#include +#include + +namespace mitk::Forms::UI +{ + class ModuleActivator : public us::ModuleActivator + { + public: + ModuleActivator(); + ~ModuleActivator() override; + + void Load(us::ModuleContext* context) override; + void Unload(us::ModuleContext* context) override; + + private: + template + void RegisterQuestionWidget() + { + auto question = std::make_unique(); + m_QuestionWidgetFactory->Register(question->GetType(), new TWidget); + } + + std::unique_ptr m_QuestionWidgetFactory; + }; +} + +#endif diff --git a/Modules/FormsUI/src/mitkQuestionWidgetFactory.cpp b/Modules/FormsUI/src/mitkQuestionWidgetFactory.cpp new file mode 100644 index 0000000000..5df045b9f4 --- /dev/null +++ b/Modules/FormsUI/src/mitkQuestionWidgetFactory.cpp @@ -0,0 +1,53 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#include "mitkQuestionWidgetFactory.h" + +#include +#include + +#include + +using namespace mitk::Forms::UI; + +QuestionWidgetFactory::QuestionWidgetFactory() = default; + +QuestionWidgetFactory::~QuestionWidgetFactory() +{ +} + +void QuestionWidgetFactory::Register(const std::string& questionType, QmitkQuestionWidget* widgetPrototype) +{ + if (widgetPrototype == nullptr) + mitkThrow() << "Cannot register nullptr as widget prototype!"; + + if (m_WidgetPrototypes.find(questionType) != m_WidgetPrototypes.end()) + mitkThrow() << "A widget prototype is already registered for questions of type \"" << questionType << "\"!"; + + m_WidgetPrototypes[questionType].reset(widgetPrototype); +} + +QmitkQuestionWidget* QuestionWidgetFactory::Create(Question* question, QWidget* parent) const +{ + try + { + auto widget = m_WidgetPrototypes.at(question->GetType())->CreateAnother(parent); + widget->SetQuestion(question); + + return widget; + } + catch (const std::out_of_range&) + { + } + + return nullptr; +} diff --git a/Modules/FormsUI/src/mitkQuestionWidgetFactory.h b/Modules/FormsUI/src/mitkQuestionWidgetFactory.h new file mode 100644 index 0000000000..2104c7e51c --- /dev/null +++ b/Modules/FormsUI/src/mitkQuestionWidgetFactory.h @@ -0,0 +1,37 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#ifndef mitkQuestionWidgetFactory_h +#define mitkQuestionWidgetFactory_h + +#include + +#include +#include + +namespace mitk::Forms::UI +{ + class QuestionWidgetFactory : public IQuestionWidgetFactory + { + public: + QuestionWidgetFactory(); + ~QuestionWidgetFactory() override; + + void Register(const std::string& questionType, QmitkQuestionWidget* widgetPrototype) override; + QmitkQuestionWidget* Create(Question* question, QWidget* parent = nullptr) const override; + + private: + std::unordered_map> m_WidgetPrototypes; + }; +} + +#endif diff --git a/Modules/ModuleList.cmake b/Modules/ModuleList.cmake index 54ad857bcd..cd0dc4feb9 100644 --- a/Modules/ModuleList.cmake +++ b/Modules/ModuleList.cmake @@ -1,59 +1,61 @@ # The entries in the mitk_modules list must be # ordered according to their dependencies. set(MITK_MODULES Log Core + Forms + FormsUI CommandLine CoreCmdApps AppUtil LegacyIO DataTypesExt Annotation LegacyGL AlgorithmsExt MapperExt DICOM DICOMQI DICOMTesting SceneSerializationBase PlanarFigure ImageDenoising ImageExtraction SceneSerialization Gizmo GraphAlgorithms Multilabel Chart ImageStatistics ContourModel SurfaceInterpolation BoundingShape Segmentation QtWidgets QtWidgetsExt ImageStatisticsUI SegmentationUI MatchPointRegistration MatchPointRegistrationUI Classification QtOverlays DICOMUI Remeshing Python QtPython Persistence RT RTUI IOExt XNAT RenderWindowManagerUI CEST BasicImageProcessing ModelFit ModelFitUI Pharmacokinetics PharmacokineticsUI DICOMPM ROI ) diff --git a/Modules/Multilabel/autoload/IO/mitkSegmentationTaskListIO.cpp b/Modules/Multilabel/autoload/IO/mitkSegmentationTaskListIO.cpp index e883c20426..66f5e9130c 100644 --- a/Modules/Multilabel/autoload/IO/mitkSegmentationTaskListIO.cpp +++ b/Modules/Multilabel/autoload/IO/mitkSegmentationTaskListIO.cpp @@ -1,267 +1,273 @@ /*============================================================================ The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center (DKFZ) All rights reserved. Use of this source code is governed by a 3-clause BSD license that can be found in the LICENSE file. ============================================================================*/ #include "mitkSegmentationTaskListIO.h" #include "mitkMultilabelIOMimeTypes.h" #include #include #include #include namespace mitk { void to_json(nlohmann::json& json, const SegmentationTaskList::Task& task) { if (task.HasName()) json["Name"] = task.GetName(); if (task.HasDescription()) json["Description"] = task.GetDescription(); if (task.HasImage()) json["Image"] = task.GetImage().string(); if (task.HasSegmentation()) json["Segmentation"] = task.GetSegmentation().string(); if (task.HasLabelName()) json["LabelName"] = task.GetLabelName(); if (task.HasLabelNameSuggestions()) json["LabelNameSuggestions"] = task.GetLabelNameSuggestions().string(); if (task.HasPreset()) json["Preset"] = task.GetPreset().string(); if (task.HasResult()) json["Result"] = task.GetResult().string(); if (task.HasDynamic()) json["Dynamic"] = task.GetDynamic(); } void from_json(const nlohmann::json& json, SegmentationTaskList::Task& task) { auto iter = json.find("Name"); if (iter != json.end()) task.SetName(json["Name"].get()); iter = json.find("Description"); if (iter != json.end()) task.SetDescription(json["Description"].get()); iter = json.find("Image"); if (iter != json.end()) task.SetImage(json["Image"].get()); iter = json.find("Segmentation"); if (iter != json.end()) task.SetSegmentation(json["Segmentation"].get()); iter = json.find("LabelName"); if (iter != json.end()) task.SetLabelName(json["LabelName"].get()); iter = json.find("LabelNameSuggestions"); if (iter != json.end()) task.SetLabelNameSuggestions(json["LabelNameSuggestions"].get()); iter = json.find("Preset"); if (iter != json.end()) task.SetPreset(json["Preset"].get()); iter = json.find("Result"); if (iter != json.end()) task.SetResult(json["Result"].get()); iter = json.find("Dynamic"); if (iter != json.end()) task.SetDynamic(json["Dynamic"].get()); } } mitk::SegmentationTaskListIO::SegmentationTaskListIO() : AbstractFileIO(SegmentationTaskList::GetStaticNameOfClass(), MitkMultilabelIOMimeTypes::SEGMENTATIONTASKLIST_MIMETYPE(), "MITK Segmentation Task List") { this->RegisterService(); } std::vector mitk::SegmentationTaskListIO::DoRead() { auto* stream = this->GetInputStream(); std::ifstream fileStream; if (nullptr == stream) { auto filename = this->GetInputLocation(); if (filename.empty() || !fs::exists(filename)) mitkThrow() << "Invalid or nonexistent filename: \"" << filename << "\"!"; fileStream.open(filename); if (!fileStream.is_open()) mitkThrow() << "Could not open file \"" << filename << "\" for reading!"; stream = &fileStream; } nlohmann::json json; try { json = nlohmann::json::parse(*stream); } catch (const nlohmann::json::exception& e) { mitkThrow() << e.what(); } if (!json.is_object()) mitkThrow() << "Unknown file format (expected JSON object as root)!"; if ("MITK Segmentation Task List" != json.value("FileFormat", "")) mitkThrow() << "Unknown file format (expected \"MITK Segmentation Task List\")!"; if (1 != json.value("Version", 0)) mitkThrow() << "Unknown file format version (expected \"1\")!"; if (!json.contains("Tasks") || !json["Tasks"].is_array()) mitkThrow() << "Tasks array not found!"; auto segmentationTaskList = SegmentationTaskList::New(); - if (json.contains("Name")) - segmentationTaskList->SetProperty("name", StringProperty::New(json["Name"].get())); - try { + if (json.contains("Name")) + segmentationTaskList->SetProperty("name", StringProperty::New(json["Name"].get())); + + if (json.contains("Form")) + segmentationTaskList->SetForm(json["Form"].get()); + if (json.contains("Defaults")) { segmentationTaskList->SetDefaults(json["Defaults"].get()); if (segmentationTaskList->GetDefaults().HasResult()) mitkThrow() << "Defaults must not contain \"Result\"!"; } for (const auto& task : json["Tasks"]) { auto i = segmentationTaskList->AddTask(task.get()); if (!segmentationTaskList->HasImage(i)) mitkThrow() << "Task " << i << " must contain \"Image\"!"; fs::path imagePath(segmentationTaskList->GetImage(i)); if (imagePath.is_relative()) { auto inputLocation = this->GetInputLocation(); /* If we have access to properties, we are reading from an MITK scene * file. In this case, paths are still relative to the original input * location, which is preserved in the properties. */ const auto* properties = this->GetProperties(); if (properties != nullptr) properties->GetStringProperty("MITK.IO.reader.inputlocation", inputLocation); imagePath = fs::path(inputLocation).remove_filename() / imagePath; } if (!fs::exists(imagePath)) mitkThrow() << "Referenced image \"" << imagePath << "\" in task " << i << " does not exist!"; if (!segmentationTaskList->HasResult(i)) mitkThrow() << "Task " << i << " must contain \"Result\"!"; } } catch (const nlohmann::json::type_error& e) { mitkThrow() << e.what(); } std::vector result; result.push_back(segmentationTaskList.GetPointer()); return result; } void mitk::SegmentationTaskListIO::Write() { auto segmentationTaskList = dynamic_cast(this->GetInput()); if (nullptr == segmentationTaskList) mitkThrow() << "Invalid input for writing!"; if (segmentationTaskList->GetNumberOfTasks() == 0) mitkThrow() << "No tasks found!"; auto* stream = this->GetOutputStream(); std::ofstream fileStream; if (nullptr == stream) { auto filename = this->GetOutputLocation(); if (filename.empty()) mitkThrow() << "Neither an output stream nor an output filename was specified!"; fileStream.open(filename); if (!fileStream.is_open()) mitkThrow() << "Could not open file \"" << filename << "\" for writing!"; stream = &fileStream; } if (!stream->good()) mitkThrow() << "Stream for writing is not good!"; nlohmann::ordered_json json = { { "FileFormat", "MITK Segmentation Task List" }, { "Version", 1 }, { "Name", segmentationTaskList->GetProperty("name")->GetValueAsString() } }; + if (segmentationTaskList->HasForm()) + json["Form"] = segmentationTaskList->GetForm().string(); + nlohmann::json defaults = segmentationTaskList->GetDefaults(); if (!defaults.is_null()) json["Defaults"] = defaults; nlohmann::json tasks; for (const auto& task : *segmentationTaskList) tasks.push_back(task); json["Tasks"] = tasks; *stream << std::setw(2) << json << std::endl; } mitk::SegmentationTaskListIO* mitk::SegmentationTaskListIO::IOClone() const { return new SegmentationTaskListIO(*this); } diff --git a/Modules/Multilabel/mitkSegmentationTaskList.cpp b/Modules/Multilabel/mitkSegmentationTaskList.cpp index 97523730e6..8badfb906a 100644 --- a/Modules/Multilabel/mitkSegmentationTaskList.cpp +++ b/Modules/Multilabel/mitkSegmentationTaskList.cpp @@ -1,204 +1,223 @@ /*============================================================================ The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center (DKFZ) All rights reserved. Use of this source code is governed by a 3-clause BSD license that can be found in the LICENSE file. ============================================================================*/ #include "mitkSegmentationTaskList.h" #include #include mitk::SegmentationTaskList::Task::Task() : m_Defaults(nullptr) { } mitk::SegmentationTaskList::Task::~Task() { } void mitk::SegmentationTaskList::Task::SetDefaults(const Task* defaults) { m_Defaults = defaults; } mitk::SegmentationTaskList::SegmentationTaskList() { // A base data cannot be serialized if empty. To be not considered empty its // geometry must consist of at least one time step. However, a segmentation // task would then appear as invisible spatial object in a scene. This can // be prevented by excluding it from the scene's bounding box calculations. this->GetTimeGeometry()->Expand(1); this->SetProperty("includeInBoundingBox", BoolProperty::New(false)); } mitk::SegmentationTaskList::SegmentationTaskList(const Self& other) : BaseData(other) { } mitk::SegmentationTaskList::~SegmentationTaskList() { } size_t mitk::SegmentationTaskList::GetNumberOfTasks() const { return m_Tasks.size(); } size_t mitk::SegmentationTaskList::AddTask(const Task& subtask) { m_Tasks.push_back(subtask); m_Tasks.back().SetDefaults(&m_Defaults); return m_Tasks.size() - 1; } const mitk::SegmentationTaskList::Task* mitk::SegmentationTaskList::GetTask(size_t index) const { return &m_Tasks.at(index); } mitk::SegmentationTaskList::Task* mitk::SegmentationTaskList::GetTask(size_t index) { return &m_Tasks.at(index); } const mitk::SegmentationTaskList::Task& mitk::SegmentationTaskList::GetDefaults() const { return m_Defaults; } void mitk::SegmentationTaskList::SetDefaults(const Task& defaults) { m_Defaults = defaults; for (auto& subtask : m_Tasks) subtask.SetDefaults(&m_Defaults); } +bool mitk::SegmentationTaskList::HasForm() const +{ + return m_Form.has_value(); +} + +std::filesystem::path mitk::SegmentationTaskList::GetForm() const +{ + return m_Form.value_or(std::filesystem::path()); +} + +void mitk::SegmentationTaskList::SetForm(const std::filesystem::path& form) +{ + if (this->GetForm() != form) + { + m_Form = form; + this->Modified(); + } +} + bool mitk::SegmentationTaskList::IsDone() const { for (size_t i = 0; i < m_Tasks.size(); ++i) { if (!this->IsDone(i)) return false; } return true; } bool mitk::SegmentationTaskList::IsDone(size_t index) const { return fs::exists(this->GetAbsolutePath(m_Tasks.at(index).GetResult())); } fs::path mitk::SegmentationTaskList::GetInputLocation() const { std::string inputLocation; this->GetPropertyList()->GetStringProperty("MITK.IO.reader.inputlocation", inputLocation); return !inputLocation.empty() #ifdef MITK_HAS_FILESYSTEM ? fs::path(inputLocation).lexically_normal() #else ? fs::path(inputLocation) #endif : fs::path(); } fs::path mitk::SegmentationTaskList::GetBasePath() const { return this->GetInputLocation().remove_filename(); } fs::path mitk::SegmentationTaskList::GetAbsolutePath(const fs::path& path) const { if (path.empty()) return path; #ifdef MITK_HAS_FILESYSTEM auto normalizedPath = path.lexically_normal(); #else auto normalizedPath = path; #endif return !normalizedPath.is_absolute() ? this->GetBasePath() / normalizedPath : normalizedPath; } fs::path mitk::SegmentationTaskList::GetInterimPath(const fs::path& path) const { if (path.empty() || !path.has_filename()) return path; auto interimPath = path; return interimPath.replace_extension(".interim" + path.extension().string()); } void mitk::SegmentationTaskList::SaveTask(size_t index, const BaseData* segmentation, bool saveAsInterimResult) { if (segmentation == nullptr) return; auto path = this->GetAbsolutePath(this->GetResult(index)); auto interimPath = this->GetInterimPath(path); if (fs::exists(path)) saveAsInterimResult = false; IOUtil::Save(segmentation, saveAsInterimResult ? interimPath.string() : path.string()); if (!saveAsInterimResult && fs::exists(interimPath)) { std::error_code ec; fs::remove(interimPath, ec); } } std::vector::const_iterator mitk::SegmentationTaskList::begin() const { return m_Tasks.begin(); } std::vector::const_iterator mitk::SegmentationTaskList::end() const { return m_Tasks.end(); } std::vector::iterator mitk::SegmentationTaskList::begin() { return m_Tasks.begin(); } std::vector::iterator mitk::SegmentationTaskList::end() { return m_Tasks.end(); } void mitk::SegmentationTaskList::SetRequestedRegionToLargestPossibleRegion() { } bool mitk::SegmentationTaskList::RequestedRegionIsOutsideOfTheBufferedRegion() { return false; } bool mitk::SegmentationTaskList::VerifyRequestedRegion() { return true; } void mitk::SegmentationTaskList::SetRequestedRegion(const itk::DataObject*) { } diff --git a/Modules/Multilabel/mitkSegmentationTaskList.h b/Modules/Multilabel/mitkSegmentationTaskList.h index 9a90f1210d..13355776ba 100644 --- a/Modules/Multilabel/mitkSegmentationTaskList.h +++ b/Modules/Multilabel/mitkSegmentationTaskList.h @@ -1,111 +1,116 @@ /*============================================================================ The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center (DKFZ) All rights reserved. Use of this source code is governed by a 3-clause BSD license that can be found in the LICENSE file. ============================================================================*/ #ifndef mitkSegmentationTaskList_h #define mitkSegmentationTaskList_h #include #include #include #include #include namespace mitk { /** \brief A list of segmentation tasks. * * See \ref MITKSegmentationTaskListsPage for more information. */ class MITKMULTILABEL_EXPORT SegmentationTaskList : public BaseData { public: class MITKMULTILABEL_EXPORT Task { public: Task(); ~Task(); void SetDefaults(const Task* defaults); mitkSegmentationTaskValueMacro(std::string, Name) mitkSegmentationTaskValueMacro(std::string, Description) mitkSegmentationTaskValueMacro(fs::path, Image) mitkSegmentationTaskValueMacro(fs::path, Segmentation) mitkSegmentationTaskValueMacro(std::string, LabelName) mitkSegmentationTaskValueMacro(fs::path, LabelNameSuggestions) mitkSegmentationTaskValueMacro(fs::path, Preset) mitkSegmentationTaskValueMacro(fs::path, Result) mitkSegmentationTaskValueMacro(bool, Dynamic) private: const Task* m_Defaults; }; mitkClassMacro(SegmentationTaskList, BaseData) itkFactorylessNewMacro(Self) itkCloneMacro(Self) mitkSegmentationTaskListValueMacro(std::string, Name) mitkSegmentationTaskListValueMacro(std::string, Description) mitkSegmentationTaskListValueMacro(fs::path, Image) mitkSegmentationTaskListValueMacro(fs::path, Segmentation) mitkSegmentationTaskListValueMacro(std::string, LabelName) mitkSegmentationTaskListValueMacro(fs::path, LabelNameSuggestions) mitkSegmentationTaskListValueMacro(fs::path, Preset) mitkSegmentationTaskListValueMacro(fs::path, Result) mitkSegmentationTaskListValueMacro(bool, Dynamic) size_t GetNumberOfTasks() const; size_t AddTask(const Task& subtask); const Task* GetTask(size_t index) const; Task* GetTask(size_t index); const Task& GetDefaults() const; void SetDefaults(const Task& defaults); + bool HasForm() const; + std::filesystem::path GetForm() const; + void SetForm(const std::filesystem::path& form); + bool IsDone() const; bool IsDone(size_t index) const; fs::path GetInputLocation() const; fs::path GetBasePath() const; fs::path GetAbsolutePath(const fs::path& path) const; fs::path GetInterimPath(const fs::path& path) const; void SaveTask(size_t index, const BaseData* segmentation, bool saveAsInterimResult = false); std::vector::const_iterator begin() const; std::vector::const_iterator end() const; std::vector::iterator begin(); std::vector::iterator end(); void SetRequestedRegionToLargestPossibleRegion() override; bool RequestedRegionIsOutsideOfTheBufferedRegion() override; bool VerifyRequestedRegion() override; void SetRequestedRegion(const itk::DataObject*) override; protected: mitkCloneMacro(Self) SegmentationTaskList(); SegmentationTaskList(const Self& other); ~SegmentationTaskList() override; private: Task m_Defaults; std::vector m_Tasks; + std::optional m_Form; }; } #endif diff --git a/Plugins/org.blueberry.ui.qt/resources/darkstyle.qss b/Plugins/org.blueberry.ui.qt/resources/darkstyle.qss index c064d49846..86d6fe4d5a 100644 --- a/Plugins/org.blueberry.ui.qt/resources/darkstyle.qss +++ b/Plugins/org.blueberry.ui.qt/resources/darkstyle.qss @@ -1,622 +1,629 @@ /*============================================================================ The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center (DKFZ) All rights reserved. Use of this source code is governed by a 3-clause BSD license that can be found in the LICENSE file. ============================================================================*/ /* iconColor = #c7c7c7 <- This line is parsed by MITK iconAccentColor = #d4821e <- This line is parsed by MITK */ font.warning { color: #ff5c33; font-weight: bold; } font.normal { color: #f1f1f1; } font.highlight { color: #d4821e; } font.disabled { color: #656565; } QWidget { background-color: #2d2d30; border: none; color: #f1f1f1; } QWidget:disabled { background-color: #2d2d30; border-color: #434346; color: #656565; } +QFrame[frameShape="1"] { + border: 1px solid #434346; +} + QStackedWidget { background-color: transparent; } QAbstractButton:hover, QComboBox:hover, QLineEdit:hover, QAbstractSpinBox:hover { background-color: #3f3f46; } QAbstractButton:pressed, QAbstractButton:checked { background-color: #434346; } QPushButton { - border: 1px solid #3f3f46; padding: 4px 8px; } +QPushButton[flat="false"] { + border: 1px solid #3f3f46; +} + QPushButton:pressed { border: 1px solid #434346; } QPushButton:checked, QToolButton:checked { border: 1px solid #007acc; } QToolButton { padding: 2px; } QToolBar QToolButton { padding: 4px; } QToolButton#qt_toolbar_ext_button { background-color: transparent; border: none; min-width: 12px; max-width: 12px; padding: 0; qproperty-icon: url(:/org.blueberry.ui.qt/dark/tight-right-arrow-textcolor.svg); } QToolBox::tab { border: 1px solid #3f3f46; } QToolBox::tab:hover { background-color: #434346; } QToolBox::tab:selected { background-color: #1c97ea; border: 1px solid #1c97ea; } QAbstractItemView { alternate-background-color: #1b1b1c; background-color: #252526; } QAbstractItemView:disabled { background-color: #1b1b1c; } QAbstractItemView::item { color: #f1f1f1; } QAbstractItemView::item:selected { background-color: #1c97ea; } QAbstractItemView::item:disabled { color: #656565; } QHeaderView::section { background-color: #2d2d30; border: 1px solid transparent; } QHeaderView::section:horizontal { border-right: 1px solid #3f3f46; } QHeaderView::section:vertical { border-bottom: 1px solid #3f3f46; } QHeaderView::section:vertical:checked { background-color: #1c97ea; } QHeaderView::section:vertical:pressed { background-color: #1c97ea; font-weight: bold; } QHeaderView::down-arrow { image: url(:/org.blueberry.ui.qt/dark/down-arrow.svg); height: 16px; width: 16px; } QHeaderView::down-arrow:hover { image: url(:/org.blueberry.ui.qt/dark/down-arrow-hover.svg); } QHeaderView::down-arrow:pressed { image: url(:/org.blueberry.ui.qt/dark/down-arrow-pressed.svg); } QHeaderView::up-arrow { image: url(:/org.blueberry.ui.qt/dark/up-arrow.svg); height: 16px; width: 16px; } QHeaderView::up-arrow:hover { image: url(:/org.blueberry.ui.qt/dark/up-arrow-hover.svg); } QHeaderView::up-arrow:pressed { image: url(:/org.blueberry.ui.qt/dark/up-arrow-pressed.svg); } QGroupBox { border: 1px solid #434346; margin-top: 8px; padding-top: 8px; } QGroupBox, QGroupBox:disabled { background-color: #252526; } QGroupBox::title { padding: 0 4px; subcontrol-origin: margin; subcontrol-position: top center; } QComboBox, QLineEdit, QAbstractSpinBox { background-color: #333337; border: 1px solid #434346; } QComboBox QAbstractItemView { border: 1px solid #333337; selection-background-color: #3f3f46; } QComboBox::drop-down { image: url(:/org.blueberry.ui.qt/dark/down-arrow.svg); subcontrol-origin: margin; subcontrol-position: right; width: 12px; } QComboBox::drop-down:hover { background-color: #1f1f20; border-left: 1px solid #007acc; image: url(:/org.blueberry.ui.qt/dark/down-arrow-pressed.svg); } QAbstractSpinBox::up-button, QAbstractSpinBox::down-button { background-color: transparent; border: none; height: 9px; } QAbstractSpinBox::up-button:hover, QAbstractSpinBox::down-button:hover { background-color: #1f1f20; } QAbstractSpinBox::up-arrow { image: url(:/org.blueberry.ui.qt/dark/tight-up-arrow.svg); } QAbstractSpinBox::up-arrow:disabled, QAbstractSpinBox::up-arrow:off { image: url(:/org.blueberry.ui.qt/dark/tight-up-arrow-disabled.svg); } QAbstractSpinBox::up-arrow:hover { image: url(:/org.blueberry.ui.qt/dark/tight-up-arrow-hover.svg); } QAbstractSpinBox::up-arrow:pressed { image: url(:/org.blueberry.ui.qt/dark/tight-up-arrow-pressed.svg); } QAbstractSpinBox::down-arrow { image: url(:/org.blueberry.ui.qt/dark/tight-down-arrow.svg); } QAbstractSpinBox::down-arrow:disabled, QAbstractSpinBox::down-arrow:off { image: url(:/org.blueberry.ui.qt/dark/tight-down-arrow-disabled.svg); } QAbstractSpinBox::down-arrow:hover { image: url(:/org.blueberry.ui.qt/dark/tight-down-arrow-hover.svg); } QAbstractSpinBox::down-arrow:pressed { image: url(:/org.blueberry.ui.qt/dark/tight-down-arrow-pressed.svg); } QCheckBox, QCheckBox:hover, QCheckBox:disabled, QCheckBox:checked, QRadioButton, QRadioButton:hover, QRadioButton:disabled, QRadioButton:checked { background-color: none; } QCheckBox::indicator, QRadioButton::indicator { height: 13px; width: 13px; } QCheckBox::indicator:unchecked { image: url(:/org.blueberry.ui.qt/dark/checkbox-unchecked.svg); } QCheckBox::indicator:unchecked:hover { image: url(:/org.blueberry.ui.qt/dark/checkbox-unchecked-hover.svg); } QCheckBox::indicator:unchecked:disabled { image: url(:/org.blueberry.ui.qt/dark/checkbox-unchecked-disabled.svg); } QCheckBox::indicator:checked { image: url(:/org.blueberry.ui.qt/dark/checkbox-checked.svg); } QCheckBox::indicator:checked:hover { image: url(:/org.blueberry.ui.qt/dark/checkbox-checked-hover.svg); } QCheckBox::indicator:checked:disabled { image: url(:/org.blueberry.ui.qt/dark/checkbox-checked-disabled.svg); } QRadioButton::indicator:unchecked { image: url(:/org.blueberry.ui.qt/dark/radiobutton-unchecked.svg); } QRadioButton::indicator:unchecked:hover { image: url(:/org.blueberry.ui.qt/dark/radiobutton-unchecked-hover.svg); } QRadioButton::indicator:unchecked:disabled { image: url(:/org.blueberry.ui.qt/dark/radiobutton-unchecked-disabled.svg); } QRadioButton::indicator:checked { image: url(:/org.blueberry.ui.qt/dark/radiobutton-checked.svg); } QRadioButton::indicator:checked:hover { image: url(:/org.blueberry.ui.qt/dark/radiobutton-checked-hover.svg); } QRadioButton::indicator:checked:disabled { image: url(:/org.blueberry.ui.qt/dark/radiobutton-checked-disabled.svg); } QSlider::groove { background-color: #686868; } QSlider::groove:hover { background-color: #9e9e9e; } QSlider::groove:horizontal { height: 3px; } QSlider::groove:vertical { width: 3px; } QSlider::handle { background-color: #686868; } QSlider::handle:hover { background-color: #1c97ea; } QSlider::handle:pressed { background-color: #007acc; } QSlider::handle:horizontal { margin: -8px 0; width: 8px; } QSlider::handle::vertical { margin: 0 -8px; height: 8px; } QLineEdit:hover { border: 1px solid #2b7de1; } QLabel, QLabel:disabled { background-color: none; } QMenu { border: 1px solid #3e3e40; } QMenu QWidget { background-color: #1b1b1c; } QMenu::item { background-color: #1b1b1c; } QMenu::item:selected { background-color: #333334; } QMenu::separator { height: 1px; background-color: #3e3e40; } QMenuBar::item:selected { background-color: #3e3e40; } QScrollBar { background-color: #3e3e42; } QScrollBar:horizontal { height: 18px; margin: 0 18px 0 18px; } QScrollBar:vertical { width: 18px; margin: 18px 0 18px 0; } QScrollBar::handle { background-color: #686868; } QScrollBar::handle:hover { background-color: #9e9e9e; } QScrollBar::handle:pressed { background-color: #efebef; } QScrollBar::handle:horizontal { min-width: 18px; margin: 4px 0 5px 0; } QScrollBar::handle:vertical { min-height: 18px; margin: 0 5px 0 4px; } QScrollBar::add-page, QScrollBar::sub-page { background-color: none; } QScrollBar::add-line, QScrollBar::sub-line { background-color: #3e3e42; subcontrol-origin: margin; } QScrollBar::add-line:horizontal { subcontrol-position: right; width: 18px; } QScrollBar::sub-line:horizontal { subcontrol-position: left; width: 18px; } QScrollBar::add-line:vertical { subcontrol-position: bottom; height: 18px; } QScrollBar::sub-line:vertical { subcontrol-position: top; height: 18px; } QScrollBar::up-arrow, QScrollBar::right-arrow, QScrollBar:down-arrow, QScrollBar:left-arrow { width: 18px; height: 18px; } QScrollBar::down-arrow { image: url(:/org.blueberry.ui.qt/dark/down-arrow.svg); } QScrollBar::down-arrow:disabled { image: url(:/org.blueberry.ui.qt/dark/down-arrow-disabled.svg); } QScrollBar::down-arrow:hover { image: url(:/org.blueberry.ui.qt/dark/down-arrow-hover.svg); } QScrollBar::down-arrow:pressed { image: url(:/org.blueberry.ui.qt/dark/down-arrow-pressed.svg); } QScrollBar::left-arrow { image: url(:/org.blueberry.ui.qt/dark/left-arrow.svg); } QScrollBar::left-arrow:disabled { image: url(:/org.blueberry.ui.qt/dark/left-arrow-disabled.svg); } QScrollBar::left-arrow:hover { image: url(:/org.blueberry.ui.qt/dark/left-arrow-hover.svg); } QScrollBar::left-arrow:pressed { image: url(:/org.blueberry.ui.qt/dark/left-arrow-pressed.svg); } QScrollBar::right-arrow { image: url(:/org.blueberry.ui.qt/dark/right-arrow.svg); } QScrollBar::right-arrow:disabled { image: url(:/org.blueberry.ui.qt/dark/right-arrow-disabled.svg); } QScrollBar::right-arrow:hover { image: url(:/org.blueberry.ui.qt/dark/right-arrow-hover.svg); } QScrollBar::right-arrow:pressed { image: url(:/org.blueberry.ui.qt/dark/right-arrow-pressed.svg); } QScrollBar::up-arrow { image: url(:/org.blueberry.ui.qt/dark/up-arrow.svg); } QScrollBar::up-arrow:disabled { image: url(:/org.blueberry.ui.qt/dark/up-arrow-disabled.svg); } QScrollBar::up-arrow:hover { image: url(:/org.blueberry.ui.qt/dark/up-arrow-hover.svg); } QScrollBar::up-arrow:pressed { image: url(:/org.blueberry.ui.qt/dark/up-arrow-pressed.svg); } QTabWidget::pane { border: 1px solid #434346; } QTabBar::tab { background-color: #434346; border: 1px solid #434346; border-bottom: none; padding: 4px; } QTabBar::tab:middle { border-left: none; } QTabBar::tab:last { border-left: none; } QTabBar::tab:next-selected { border-right: none; } QTabBar::tab:selected { border: 1px solid #434346; border-bottom: none; } QTabBar::tab:!selected { background-color: #2d2d30; } QTabBar::tab:!selected:hover { background-color: #434346; } #TabCloseButton { background-color: none; } QTabBar QToolButton::left-arrow { image: url(:/org.blueberry.ui.qt/dark/tight-left-arrow.svg); } QTabBar QToolButton::left-arrow:hover { image: url(:/org.blueberry.ui.qt/dark/tight-left-arrow-hover.svg); } QTabBar QToolButton::left-arrow:pressed { image: url(:/org.blueberry.ui.qt/dark/tight-left-arrow-pressed.svg); } QTabBar QToolButton::left-arrow:disabled { image: url(:/org.blueberry.ui.qt/dark/tight-left-arrow-disabled.svg); } QTabBar QToolButton::right-arrow { image: url(:/org.blueberry.ui.qt/dark/tight-right-arrow.svg); } QTabBar QToolButton::right-arrow:hover { image: url(:/org.blueberry.ui.qt/dark/tight-right-arrow-hover.svg); } QTabBar QToolButton::right-arrow:pressed { image: url(:/org.blueberry.ui.qt/dark/tight-right-arrow-pressed.svg); } QTabBar QToolButton::right-arrow:disabled { image: url(:/org.blueberry.ui.qt/dark/tight-right-arrow-disabled.svg); } QTreeView::branch:closed:has-children:has-siblings, QTreeView::branch:closed:has-children:!has-siblings { image: url(:/org.blueberry.ui.qt/dark/right-arrow.svg); } QTreeView::branch:closed:has-children:has-siblings:hover, QTreeView::branch:closed:has-children:!has-siblings:hover { image: url(:/org.blueberry.ui.qt/dark/right-arrow-hover.svg); } QTreeView::branch:open:has-children:has-siblings, QTreeView::branch:open:has-children:!has-siblings { image: url(:/org.blueberry.ui.qt/dark/down-arrow.svg); } QTreeView::branch:open:has-children:has-siblings:hover, QTreeView::branch:open:has-children:!has-siblings:hover { image: url(:/org.blueberry.ui.qt/dark/down-arrow-hover.svg); } QTreeView::indicator:unchecked { image: url(:/org.blueberry.ui.qt/dark/checkbox-unchecked.svg); } QTreeView::indicator:unchecked:selected { image: url(:/org.blueberry.ui.qt/dark/checkbox-unchecked-selected.svg); } QTreeView::indicator:checked, QTreeView::indicator:indeterminate { image: url(:/org.blueberry.ui.qt/dark/checkbox-checked.svg); } QTreeView::indicator:checked:selected, QTreeView::indicator:indeterminate:selected { image: url(:/org.blueberry.ui.qt/dark/checkbox-checked-selected.svg); }