diff --git a/Modules/Forms/include/mitkForm.h b/Modules/Forms/include/mitkForm.h index 38644af084..fde5796820 100644 --- a/Modules/Forms/include/mitkForm.h +++ b/Modules/Forms/include/mitkForm.h @@ -1,98 +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(); - void Submit(const fs::path& csvPath) const; 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/src/mitkForm.cpp b/Modules/Forms/src/mitkForm.cpp index dd0b2b4a00..7b846c90c3 100644 --- a/Modules/Forms/src/mitkForm.cpp +++ b/Modules/Forms/src/mitkForm.cpp @@ -1,306 +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 Form::Submit(const fs::path& csvPath) const +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 : m_Sections) + for (const auto& section : form) { for (const auto* question : section.GetQuestions()) { csvFile << ",\"" << question->GetQuestionText() << '"'; } } csvFile << '\n'; } csvFile << '"' << GetCurrentISO8601DateTime() << '"'; - for (const auto& section : m_Sections) + 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/FormsUI/src/QmitkForm.cpp b/Modules/FormsUI/src/QmitkForm.cpp index 8ab4286c0e..8d9c7c9e37 100644 --- a/Modules/FormsUI/src/QmitkForm.cpp +++ b/Modules/FormsUI/src/QmitkForm.cpp @@ -1,339 +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 { - m_Form.Submit(m_ResponsesPath); + 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(); }