diff --git a/Modules/Multilabel/mitkSegmentationTask.cpp b/Modules/Multilabel/mitkSegmentationTask.cpp index f9c58ebf32..9f5789aea0 100644 --- a/Modules/Multilabel/mitkSegmentationTask.cpp +++ b/Modules/Multilabel/mitkSegmentationTask.cpp @@ -1,163 +1,174 @@ /*============================================================================ 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 "mitkSegmentationTask.h" + +#include #include mitk::SegmentationTask::Subtask::Subtask() : m_Defaults(nullptr) { } mitk::SegmentationTask::Subtask::~Subtask() { } void mitk::SegmentationTask::Subtask::SetDefaults(const Subtask* defaults) { m_Defaults = defaults; } mitk::SegmentationTask::SegmentationTask() { // 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 spacial 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::SegmentationTask::SegmentationTask(const Self& other) : BaseData(other) { } mitk::SegmentationTask::~SegmentationTask() { } size_t mitk::SegmentationTask::GetNumberOfSubtasks() const { return m_Subtasks.size(); } size_t mitk::SegmentationTask::AddSubtask(const Subtask& subtask) { m_Subtasks.push_back(subtask); m_Subtasks.back().SetDefaults(&m_Defaults); return m_Subtasks.size() - 1; } const mitk::SegmentationTask::Subtask* mitk::SegmentationTask::GetSubtask(size_t index) const { return &m_Subtasks.at(index); } mitk::SegmentationTask::Subtask* mitk::SegmentationTask::GetSubtask(size_t index) { return &m_Subtasks.at(index); } const mitk::SegmentationTask::Subtask& mitk::SegmentationTask::GetDefaults() const { return m_Defaults; } void mitk::SegmentationTask::SetDefaults(const Subtask& defaults) { m_Defaults = defaults; for (auto& subtask : m_Subtasks) subtask.SetDefaults(&m_Defaults); } bool mitk::SegmentationTask::IsDone() const { for (size_t i = 0; i < m_Subtasks.size(); ++i) { if (!this->IsDone(i)) return false; } return true; } bool mitk::SegmentationTask::IsDone(size_t index) const { return std::filesystem::exists(this->GetAbsolutePath(m_Subtasks.at(index).GetResult())); } std::filesystem::path mitk::SegmentationTask::GetInputLocation() const { std::string result; this->GetPropertyList()->GetStringProperty("MITK.IO.reader.inputlocation", result); return !result.empty() ? std::filesystem::path(result).lexically_normal() : result; } std::filesystem::path mitk::SegmentationTask::GetBasePath() const { return this->GetInputLocation().remove_filename(); } std::filesystem::path mitk::SegmentationTask::GetAbsolutePath(const std::filesystem::path& path) const { if (path.empty()) return path; auto normalizedPath = path.lexically_normal(); return !normalizedPath.is_absolute() ? this->GetBasePath() / normalizedPath : normalizedPath; } +void mitk::SegmentationTask::SaveSubtask(size_t index, const BaseData* segmentation) +{ + if (segmentation == nullptr) + return; + + auto path = this->GetAbsolutePath(this->GetResult(index)); + IOUtil::Save(segmentation, path.string()); +} + std::vector::const_iterator mitk::SegmentationTask::begin() const { return m_Subtasks.begin(); } std::vector::const_iterator mitk::SegmentationTask::end() const { return m_Subtasks.end(); } std::vector::iterator mitk::SegmentationTask::begin() { return m_Subtasks.begin(); } std::vector::iterator mitk::SegmentationTask::end() { return m_Subtasks.end(); } void mitk::SegmentationTask::SetRequestedRegionToLargestPossibleRegion() { } bool mitk::SegmentationTask::RequestedRegionIsOutsideOfTheBufferedRegion() { return false; } bool mitk::SegmentationTask::VerifyRequestedRegion() { return true; } void mitk::SegmentationTask::SetRequestedRegion(const itk::DataObject*) { } diff --git a/Modules/Multilabel/mitkSegmentationTask.h b/Modules/Multilabel/mitkSegmentationTask.h index b726aec8d8..126eee3b82 100644 --- a/Modules/Multilabel/mitkSegmentationTask.h +++ b/Modules/Multilabel/mitkSegmentationTask.h @@ -1,102 +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. ============================================================================*/ #ifndef mitkSegmentationTask_h #define mitkSegmentationTask_h #include #include #include #include #include namespace mitk { class MITKMULTILABEL_EXPORT SegmentationTask : public BaseData { public: class MITKMULTILABEL_EXPORT Subtask { public: Subtask(); ~Subtask(); void SetDefaults(const Subtask* defaults); mitkSegmentationSubtaskValueMacro(std::string, Name) mitkSegmentationSubtaskValueMacro(std::string, Description) mitkSegmentationSubtaskValueMacro(std::string, Image) mitkSegmentationSubtaskValueMacro(std::string, Segmentation) mitkSegmentationSubtaskValueMacro(std::string, LabelName) mitkSegmentationSubtaskValueMacro(std::string, Preset) mitkSegmentationSubtaskValueMacro(std::string, Result) mitkSegmentationSubtaskValueMacro(bool, Dynamic) private: const Subtask* m_Defaults; }; mitkClassMacro(SegmentationTask, BaseData) itkFactorylessNewMacro(Self) itkCloneMacro(Self) mitkSegmentationTaskValueMacro(std::string, Name) mitkSegmentationTaskValueMacro(std::string, Description) mitkSegmentationTaskValueMacro(std::string, Image) mitkSegmentationTaskValueMacro(std::string, Segmentation) mitkSegmentationTaskValueMacro(std::string, LabelName) mitkSegmentationTaskValueMacro(std::string, Preset) mitkSegmentationTaskValueMacro(std::string, Result) mitkSegmentationTaskValueMacro(bool, Dynamic) size_t GetNumberOfSubtasks() const; size_t AddSubtask(const Subtask& subtask); const Subtask* GetSubtask(size_t index) const; Subtask* GetSubtask(size_t index); const Subtask& GetDefaults() const; void SetDefaults(const Subtask& defaults); bool IsDone() const; bool IsDone(size_t index) const; std::filesystem::path GetInputLocation() const; std::filesystem::path GetBasePath() const; std::filesystem::path GetAbsolutePath(const std::filesystem::path& path) const; + void SaveSubtask(size_t index, const BaseData* segmentation); + 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) SegmentationTask(); SegmentationTask(const Self& other); ~SegmentationTask() override; private: Subtask m_Defaults; std::vector m_Subtasks; }; } #endif diff --git a/Plugins/org.mitk.gui.qt.flow.segmentation/src/internal/QmitkSegmentationFlowControlView.cpp b/Plugins/org.mitk.gui.qt.flow.segmentation/src/internal/QmitkSegmentationFlowControlView.cpp index d1afb073ff..eb830a1cde 100644 --- a/Plugins/org.mitk.gui.qt.flow.segmentation/src/internal/QmitkSegmentationFlowControlView.cpp +++ b/Plugins/org.mitk.gui.qt.flow.segmentation/src/internal/QmitkSegmentationFlowControlView.cpp @@ -1,187 +1,183 @@ /*============================================================================ 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 "org_mitk_gui_qt_flow_segmentation_Activator.h" // Blueberry #include #include #include //MITK #include #include #include #include #include #include #include // Qmitk #include "QmitkSegmentationFlowControlView.h" // Qt #include #include #include const std::string QmitkSegmentationFlowControlView::VIEW_ID = "org.mitk.views.flow.control"; QmitkSegmentationFlowControlView::QmitkSegmentationFlowControlView() : m_Parent(nullptr) { auto notHelperObject = mitk::NodePredicateNot::New( mitk::NodePredicateProperty::New("helper object")); m_SegmentationPredicate = mitk::NodePredicateAnd::New( mitk::TNodePredicateDataType::New(), notHelperObject); m_SegmentationTaskPredicate = mitk::NodePredicateAnd::New( mitk::TNodePredicateDataType::New(), notHelperObject); } void QmitkSegmentationFlowControlView::SetFocus() { m_Controls.btnStoreAndAccept->setFocus(); } void QmitkSegmentationFlowControlView::CreateQtPartControl(QWidget* parent) { // create GUI widgets from the Qt Designer's .ui file m_Controls.setupUi(parent); m_Parent = parent; using Self = QmitkSegmentationFlowControlView; connect(m_Controls.btnStoreAndAccept, &QPushButton::clicked, this, &Self::OnAcceptButtonPushed); connect(m_Controls.segmentationTaskWidget, &QmitkSegmentationTaskWidget::ActiveSubtaskChanged, this, &Self::OnActiveSubtaskChanged); connect(m_Controls.segmentationTaskWidget, &QmitkSegmentationTaskWidget::CurrentSubtaskChanged, this, &Self::OnCurrentSubtaskChanged); m_Controls.segmentationTaskWidget->setVisible(false); m_Controls.labelStored->setVisible(false); UpdateControls(); m_OutputDir = QString::fromStdString(mitk::BaseApplication::instance().config().getString("flow.outputdir", itksys::SystemTools::GetCurrentWorkingDirectory())); m_OutputDir = QDir::fromNativeSeparators(m_OutputDir); m_FileExtension = QString::fromStdString(mitk::BaseApplication::instance().config().getString("flow.outputextension", "nrrd")); } void QmitkSegmentationFlowControlView::OnAcceptButtonPushed() { if (m_Controls.segmentationTaskWidget->isVisible()) { auto* task = m_Controls.segmentationTaskWidget->GetTask(); if (task != nullptr) { auto activeSubtaskIndex = m_Controls.segmentationTaskWidget->GetActiveSubtaskIndex(); if (activeSubtaskIndex.has_value()) { auto segmentationNode = m_Controls.segmentationTaskWidget->GetSegmentationDataNode(activeSubtaskIndex.value()); if (segmentationNode != nullptr) { - auto path = task->GetAbsolutePath(task->GetResult(activeSubtaskIndex.value())); + QApplication::setOverrideCursor(Qt::BusyCursor); - if (!path.empty()) + try { - QApplication::setOverrideCursor(QCursor(Qt::BusyCursor)); - - try - { - mitk::IOUtil::Save(segmentationNode->GetData(), path.string()); - // TODO: Give temporarily displayed feedback to user - } - catch (const mitk::Exception&) - { - } - - QApplication::restoreOverrideCursor(); + task->SaveSubtask(activeSubtaskIndex.value(), segmentationNode->GetData()); + m_Controls.segmentationTaskWidget->OnUnsavedChangesSaved(); } + catch (const mitk::Exception& e) + { + MITK_ERROR << e; + } + + QApplication::restoreOverrideCursor(); } } } } else { auto nodes = this->GetDataStorage()->GetSubset(m_SegmentationPredicate); for (auto node : *nodes) { QString outputpath = m_OutputDir + "/" + QString::fromStdString(node->GetName()) + "." + m_FileExtension; outputpath = QDir::toNativeSeparators(QDir::cleanPath(outputpath)); mitk::IOUtil::Save(node->GetData(), outputpath.toStdString()); } m_Controls.labelStored->setVisible(true); } } void QmitkSegmentationFlowControlView::OnActiveSubtaskChanged(const std::optional&) { this->UpdateControls(); } void QmitkSegmentationFlowControlView::OnCurrentSubtaskChanged(size_t) { this->UpdateControls(); } void QmitkSegmentationFlowControlView::UpdateControls() { auto dataStorage = this->GetDataStorage(); auto hasTask = !dataStorage->GetSubset(m_SegmentationTaskPredicate)->empty(); m_Controls.segmentationTaskWidget->setVisible(hasTask); if (hasTask) { auto activeSubtaskIndex = m_Controls.segmentationTaskWidget->GetActiveSubtaskIndex(); auto hasActiveSubtask = activeSubtaskIndex.has_value(); auto isCurrentSubtask = hasActiveSubtask ? m_Controls.segmentationTaskWidget->GetCurrentSubtaskIndex() == activeSubtaskIndex.value() : false; m_Controls.btnStoreAndAccept->setEnabled(hasActiveSubtask && isCurrentSubtask); } else { auto hasSegmentation = !dataStorage->GetSubset(m_SegmentationPredicate)->empty(); m_Controls.btnStoreAndAccept->setEnabled(hasSegmentation); } } void QmitkSegmentationFlowControlView::NodeAdded(const mitk::DataNode* node) { if (dynamic_cast(node->GetData()) != nullptr) this->UpdateControls(); } void QmitkSegmentationFlowControlView::NodeChanged(const mitk::DataNode* node) { if (dynamic_cast(node->GetData()) != nullptr) this->UpdateControls(); } void QmitkSegmentationFlowControlView::NodeRemoved(const mitk::DataNode* node) { if (dynamic_cast(node->GetData()) != nullptr) this->UpdateControls(); } diff --git a/Plugins/org.mitk.gui.qt.flow.segmentation/src/internal/QmitkSegmentationTaskWidget.cpp b/Plugins/org.mitk.gui.qt.flow.segmentation/src/internal/QmitkSegmentationTaskWidget.cpp index e167262b0e..8802b51838 100644 --- a/Plugins/org.mitk.gui.qt.flow.segmentation/src/internal/QmitkSegmentationTaskWidget.cpp +++ b/Plugins/org.mitk.gui.qt.flow.segmentation/src/internal/QmitkSegmentationTaskWidget.cpp @@ -1,586 +1,689 @@ /*============================================================================ 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 "QmitkSegmentationTaskWidget.h" #include "org_mitk_gui_qt_flow_segmentation_Activator.h" #include #include #include #include #include #include #include #include #include #include #include #include +#include #include namespace { mitk::DataStorage* GetDataStorage() { auto* pluginContext = org_mitk_gui_qt_flow_segmentation_Activator::GetContext(); auto dataStorageServiceReference = pluginContext->getServiceReference(); if (dataStorageServiceReference) { auto* dataStorageService = pluginContext->getService(dataStorageServiceReference); if (dataStorageService != nullptr) { auto dataStorageReference = dataStorageService->GetDataStorage(); pluginContext->ungetService(dataStorageServiceReference); return dataStorageReference->GetDataStorage(); } } return nullptr; } std::filesystem::path GetInputLocation(const mitk::BaseData* data) { std::string result; if (data != nullptr) data->GetPropertyList()->GetStringProperty("MITK.IO.reader.inputlocation", result); return result; } QString ColorString(const QString& string, const QColor& color, const QColor& backgroundColor = QColor::Invalid) { if (!color.isValid() && !backgroundColor.isValid()) return string; auto result = QStringLiteral("%1").arg(string); return result; } } /* This constructor has three objectives: * 1. Do widget initialization that cannot be done in the .ui file * 2. Connect signals and slots * 3. Explicitly trigger a reset to a valid initial widget state */ QmitkSegmentationTaskWidget::QmitkSegmentationTaskWidget(QWidget* parent) : QWidget(parent), m_Ui(new Ui::QmitkSegmentationTaskWidget), m_FileSystemWatcher(new QFileSystemWatcher(this)), - m_CurrentSubtaskIndex(0) + m_CurrentSubtaskIndex(0), + m_UnsavedChanges(false) { m_Ui->setupUi(this); m_Ui->selectionWidget->SetDataStorage(GetDataStorage()); m_Ui->selectionWidget->SetSelectionIsOptional(true); m_Ui->selectionWidget->SetEmptyInfo(QStringLiteral("Select a segmentation task")); m_Ui->selectionWidget->SetAutoSelectNewNodes(true); m_Ui->selectionWidget->SetNodePredicate(mitk::TNodePredicateDataType::New()); m_Ui->progressBar->setStyleSheet(QString("QProgressBar::chunk { background-color: %1; }").arg(QmitkStyleManager::GetIconAccentColor())); using Self = QmitkSegmentationTaskWidget; connect(m_Ui->selectionWidget, &QmitkSingleNodeSelectionWidget::CurrentSelectionChanged, this, &Self::OnSelectionChanged); connect(m_Ui->previousButton, &QToolButton::clicked, this, &Self::OnPreviousButtonClicked); connect(m_Ui->nextButton, &QToolButton::clicked, this, &Self::OnNextButtonClicked); connect(m_Ui->loadButton, &QPushButton::clicked, this, &Self::OnLoadButtonClicked); connect(m_FileSystemWatcher, &QFileSystemWatcher::directoryChanged, this, &Self::OnResultDirectoryChanged); this->OnSelectionChanged(m_Ui->selectionWidget->GetSelectedNodes()); } QmitkSegmentationTaskWidget::~QmitkSegmentationTaskWidget() { } mitk::SegmentationTask* QmitkSegmentationTaskWidget::GetTask() const { return m_Task; } std::optional QmitkSegmentationTaskWidget::GetActiveSubtaskIndex() const { return m_ActiveSubtaskIndex; } size_t QmitkSegmentationTaskWidget::GetCurrentSubtaskIndex() const { return m_CurrentSubtaskIndex; } +void QmitkSegmentationTaskWidget::OnUnsavedChangesSaved() +{ + if (m_UnsavedChanges) + { + m_UnsavedChanges = false; + + if (m_ActiveSubtaskIndex.value() == m_CurrentSubtaskIndex) + this->UpdateDetailsLabel(); + } +} + /* Make sure that the widget transitions into a valid state whenever the * selection changes. */ void QmitkSegmentationTaskWidget::OnSelectionChanged(const QmitkSingleNodeSelectionWidget::NodeList& nodes) { this->UnloadSubtasks(); this->ResetControls(); if (!nodes.empty()) { m_TaskNode = nodes.front(); auto task = dynamic_cast(m_TaskNode->GetData()); if (task != nullptr) { this->OnTaskChanged(task); return; } } this->SetTask(nullptr); m_TaskNode = nullptr; } /* Reset all controls to a default state as a common basis for further * adjustments. */ void QmitkSegmentationTaskWidget::ResetControls() { m_Ui->progressBar->setEnabled(false); m_Ui->progressBar->setFormat(""); m_Ui->progressBar->setValue(0); m_Ui->progressBar->setMaximum(1); m_Ui->previousButton->setEnabled(false); m_Ui->loadButton->setEnabled(false); m_Ui->loadButton->setText(QStringLiteral("Load subtask")); m_Ui->nextButton->setEnabled(false); m_Ui->detailsLabel->clear(); } /* If the segmentation task changed, reset all member variables to expected * default values and reset the file system watcher. */ void QmitkSegmentationTaskWidget::SetTask(mitk::SegmentationTask* task) { if (m_Task != task) { m_Task = task; this->SetCurrentSubtaskIndex(0); this->ResetFileSystemWatcher(); } } void QmitkSegmentationTaskWidget::ResetFileSystemWatcher() { - { - auto paths = m_FileSystemWatcher->directories(); + auto paths = m_FileSystemWatcher->directories(); - if (!paths.empty()) - m_FileSystemWatcher->removePaths(paths); - } + if (!paths.empty()) + m_FileSystemWatcher->removePaths(paths); if (m_Task.IsNotNull()) { for (const auto& subtask : *m_Task) { auto resultPath = m_Task->GetAbsolutePath(subtask.GetResult()).remove_filename(); if (!std::filesystem::exists(resultPath)) { try { std::filesystem::create_directories(resultPath); } catch (const std::filesystem::filesystem_error& e) { MITK_ERROR << e.what(); } } if (std::filesystem::exists(resultPath)) m_FileSystemWatcher->addPath(QString::fromStdString(resultPath.string())); } } } void QmitkSegmentationTaskWidget::OnResultDirectoryChanged(const QString&) { + // TODO: If a segmentation was modified ("Unsaved changes"), saved ("Done"), and then the file is deleted, the status should be "Unsaved changes" instead of "Not done". this->UpdateProgressBar(); this->UpdateDetailsLabel(); } void QmitkSegmentationTaskWidget::UpdateProgressBar() { int progress = 0; for (size_t i = 0; i < m_Task->GetNumberOfSubtasks(); ++i) { if (m_Task->IsDone(i)) ++progress; } m_Ui->progressBar->setValue(progress); } /* Provided that a valid segmentation task is currently selected and the * widget is in its default state, update all controls accordingly. */ void QmitkSegmentationTaskWidget::OnTaskChanged(mitk::SegmentationTask* task) { this->SetTask(task); const auto numSubtasks = task->GetNumberOfSubtasks(); m_Ui->progressBar->setMaximum(numSubtasks); m_Ui->progressBar->setFormat(QStringLiteral("%v/%m Subtask(s) done")); m_Ui->progressBar->setEnabled(true); this->UpdateProgressBar(); m_Ui->loadButton->setEnabled(true); if (numSubtasks > 1) m_Ui->nextButton->setEnabled(true); this->OnSubtaskChanged(); } /* If possible, change the currently displayed subtask to the previous subtask. * Enable/disable navigation buttons according to the subtask's position. */ void QmitkSegmentationTaskWidget::OnPreviousButtonClicked() { const auto maxIndex = m_Task->GetNumberOfSubtasks() - 1; if (m_CurrentSubtaskIndex != 0) { this->SetCurrentSubtaskIndex(m_CurrentSubtaskIndex - 1); this->OnSubtaskChanged(); } if (m_CurrentSubtaskIndex == 0) m_Ui->previousButton->setEnabled(false); if (m_CurrentSubtaskIndex < maxIndex) m_Ui->nextButton->setEnabled(true); } /* If possible, change the currently displayed subtask to the next subtask. * Enable/disable navigation buttons according to the subtask's position. */ void QmitkSegmentationTaskWidget::OnNextButtonClicked() { const auto maxIndex = m_Task->GetNumberOfSubtasks() - 1; if (m_CurrentSubtaskIndex < maxIndex) { this->SetCurrentSubtaskIndex(m_CurrentSubtaskIndex + 1); this->OnSubtaskChanged(); } if (m_CurrentSubtaskIndex != 0) m_Ui->previousButton->setEnabled(true); if (m_CurrentSubtaskIndex >= maxIndex) m_Ui->nextButton->setEnabled(false); } /* Update affected controls when the currently displayed subtask changed. */ void QmitkSegmentationTaskWidget::OnSubtaskChanged() { this->UpdateLoadButton(); this->UpdateDetailsLabel(); } /* Update the load button according to the currently displayed subtask. */ void QmitkSegmentationTaskWidget::UpdateLoadButton() { const auto i = m_CurrentSubtaskIndex; - bool active = m_ActiveSubtaskIndex.has_value() && m_ActiveSubtaskIndex.value() == i; - auto text = !active + auto text = !this->ActivateSubtaskIsShown() ? QStringLiteral("Load subtask") : QStringLiteral("Subtask"); if (m_Task.IsNotNull()) { text += QString(" %1/%2").arg(i + 1).arg(m_Task->GetNumberOfSubtasks()); if (m_Task->HasName(i)) text += QStringLiteral(":\n") + QString::fromStdString(m_Task->GetName(i)); } - m_Ui->loadButton->setDisabled(active); + m_Ui->loadButton->setDisabled(this->ActivateSubtaskIsShown()); m_Ui->loadButton->setText(text); } /* Update the details label according to the currently display subtask. * The text is composed of the status of the subtask and a variable number * of text blocks according to the optional values provided by the subtask. */ void QmitkSegmentationTaskWidget::UpdateDetailsLabel() { const auto i = m_CurrentSubtaskIndex; - bool active = m_ActiveSubtaskIndex.has_value() && m_ActiveSubtaskIndex.value() == i; bool isDone = m_Task->IsDone(i); - auto details = QString("

Status: %1 / ").arg(active + auto details = QString("

Status: %1 / ").arg(this->ActivateSubtaskIsShown() ? ColorString("Active", Qt::white, QColor(Qt::green).darker()) : ColorString("Inactive", Qt::white, QColor(Qt::red).darker())); - details += QString("%1

").arg(isDone - ? ColorString("Done", Qt::white, QColor(Qt::green).darker()) - : ColorString("Undone", Qt::white, QColor(Qt::red).darker())); + if (m_UnsavedChanges && this->ActivateSubtaskIsShown()) + { + details += QString("%1

").arg(ColorString("Unsaved changes", Qt::white, QColor(Qt::red).darker())); + } + else + { + details += QString("%1

").arg(isDone + ? ColorString("Done", Qt::white, QColor(Qt::green).darker()) + : ColorString("Not done", Qt::white, QColor(Qt::red).darker())); + } if (m_Task->HasDescription(i)) details += QString("

Description: %1

").arg(QString::fromStdString(m_Task->GetDescription(i))); QStringList stringList; if (m_Task->HasImage(i)) stringList << QString("Image: %1").arg(QString::fromStdString(m_Task->GetImage(i))); if (m_Task->HasSegmentation(i)) stringList << QString("Segmentation: %1").arg(QString::fromStdString(m_Task->GetSegmentation(i))); if (m_Task->HasLabelName(i)) stringList << QString("Label name: %1").arg(QString::fromStdString(m_Task->GetLabelName(i))); if (m_Task->HasPreset(i)) stringList << QString("Label set preset: %1").arg(QString::fromStdString(m_Task->GetPreset(i))); if (m_Task->HasDynamic(i)) stringList << QString("Segmentation type: %1").arg(m_Task->GetDynamic(i) ? "Dynamic" : "Static"); if (!stringList.empty()) details += QString("

%1

").arg(stringList.join(QStringLiteral("
"))); m_Ui->detailsLabel->setText(details); } /* Load/activate the currently displayed subtask. Unload all data nodes from * previously active subtasks first, but spare and reuse the image if possible. */ void QmitkSegmentationTaskWidget::OnLoadButtonClicked() { + if (m_UnsavedChanges) + { + const auto i = m_ActiveSubtaskIndex.value(); + + const auto title = QString("Load subtask %1").arg(m_CurrentSubtaskIndex + 1); + const auto text = QString("The currently active subtask %1 has unsaved changes.").arg(i + 1); + const auto reply = QMessageBox::question(this, title, text, QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel, QMessageBox::Cancel); + + if (reply == QMessageBox::Cancel) + return; + + if (reply == QMessageBox::Save) + { + QApplication::setOverrideCursor(Qt::BusyCursor); + + try + { + m_Task->SaveSubtask(i, this->GetSegmentationDataNode(i)->GetData()); + this->OnUnsavedChangesSaved(); + } + catch (const mitk::Exception& e) + { + MITK_ERROR << e; + } + + QApplication::restoreOverrideCursor(); + } + } + m_Ui->loadButton->setEnabled(false); QApplication::setOverrideCursor(QCursor(Qt::BusyCursor)); auto* imageNode = this->GetImageDataNode(m_CurrentSubtaskIndex); this->UnloadSubtasks(imageNode); this->LoadSubtask(imageNode); this->OnSubtaskChanged(); QApplication::restoreOverrideCursor(); + + m_UnsavedChanges = false; } -/* If present, return the image data node for the currently displayed subtask. - * Otherwise, return nullptr. +/* If present, return the image data node for the subtask with the specified + * index. Otherwise, return nullptr. */ mitk::DataNode* QmitkSegmentationTaskWidget::GetImageDataNode(size_t index) const { const auto imagePath = m_Task->GetAbsolutePath(m_Task->GetImage(index)); auto imageNodes = GetDataStorage()->GetDerivations(m_TaskNode, mitk::NodePredicateFunction::New([imagePath](const mitk::DataNode* node) { return imagePath == GetInputLocation(node->GetData()); })); return !imageNodes->empty() ? imageNodes->front() : nullptr; } -/* If present, return the segmentation data node for the currently displayed - * subtask. Otherwise, return nullptr. +/* If present, return the segmentation data node for the subtask with the + * specified index. Otherwise, return nullptr. */ mitk::DataNode* QmitkSegmentationTaskWidget::GetSegmentationDataNode(size_t index) const { const auto* imageNode = this->GetImageDataNode(index); if (imageNode != nullptr) { auto segmentations = GetDataStorage()->GetDerivations(imageNode, mitk::TNodePredicateDataType::New()); if (!segmentations->empty()) return segmentations->front(); } return nullptr; } /* Unload all subtask data nodes but spare the passed image data node. */ void QmitkSegmentationTaskWidget::UnloadSubtasks(const mitk::DataNode* skip) { + this->UnsubscribeFromActiveSegmentation(); + if (m_TaskNode.IsNotNull()) { mitk::DataStorage::Pointer dataStorage = GetDataStorage(); auto imageNodes = dataStorage->GetDerivations(m_TaskNode, mitk::TNodePredicateDataType::New()); for (auto imageNode : *imageNodes) { dataStorage->Remove(dataStorage->GetDerivations(imageNode, nullptr, false)); if (imageNode != skip) dataStorage->Remove(imageNode); } } this->SetActiveSubtaskIndex(std::nullopt); } /* Load/activate the currently displayed subtask. The subtask must specify * an image. The segmentation is either created from scratch with an optional * name for the first label, possibly based on a label set preset specified by * the subtask, or loaded as specified by the subtask. If a result file does * exist, it is chosen as segmentation instead. */ void QmitkSegmentationTaskWidget::LoadSubtask(mitk::DataNode::Pointer imageNode) { const auto i = m_CurrentSubtaskIndex; mitk::Image::Pointer image; mitk::LabelSetImage::Pointer segmentation; try { if (imageNode.IsNull()) { const auto path = m_Task->GetAbsolutePath(m_Task->GetImage(i)); image = mitk::IOUtil::Load(path.string()); } const auto path = m_Task->GetAbsolutePath(m_Task->GetResult(i)); if (std::filesystem::exists(path)) { segmentation = mitk::IOUtil::Load(path.string()); } else if (m_Task->HasSegmentation(i)) { const auto path = m_Task->GetAbsolutePath(m_Task->GetSegmentation(i)); segmentation = mitk::IOUtil::Load(path.string()); } } catch (const mitk::Exception&) { return; } auto dataStorage = GetDataStorage(); if (imageNode.IsNull()) { imageNode = mitk::DataNode::New(); imageNode->SetData(image); dataStorage->Add(imageNode, m_TaskNode); mitk::RenderingManager::GetInstance()->InitializeViews(image->GetTimeGeometry()); } else { image = static_cast(imageNode->GetData()); } auto name = "Subtask " + std::to_string(i + 1); imageNode->SetName(name); if (segmentation.IsNull()) { mitk::Image::ConstPointer templateImage = image; if (templateImage->GetDimension() > 3) { if (m_Task->HasDynamic(i)) { if (!m_Task->GetDynamic(i)) templateImage = mitk::SegmentationHelper::GetStaticSegmentationTemplate(image); } else { QmitkStaticDynamicSegmentationDialog dialog(this); dialog.SetReferenceImage(templateImage); dialog.exec(); templateImage = dialog.GetSegmentationTemplate(); } } auto segmentationNode = mitk::LabelSetImageHelper::CreateNewSegmentationNode(imageNode, templateImage, name); segmentation = static_cast(segmentationNode->GetData()); if (m_Task->HasPreset(i)) { const auto path = m_Task->GetAbsolutePath(m_Task->GetPreset(i)); mitk::LabelSetIOHelper::LoadLabelSetImagePreset(path.string(), segmentation); } else { auto label = mitk::LabelSetImageHelper::CreateNewLabel(segmentation); if (m_Task->HasLabelName(i)) label->SetName(m_Task->GetLabelName(i)); segmentation->GetActiveLabelSet()->AddLabel(label); } dataStorage->Add(segmentationNode, imageNode); } else { auto segmentationNode = mitk::DataNode::New(); segmentationNode->SetName(name); segmentationNode->SetData(segmentation); dataStorage->Add(segmentationNode, imageNode); } + m_UnsavedChanges = false; + this->SetActiveSubtaskIndex(i); + this->SubscribeToActiveSegmentation(); +} + +void QmitkSegmentationTaskWidget::SubscribeToActiveSegmentation() +{ + if (m_ActiveSubtaskIndex.has_value()) + { + auto segmentationNode = this->GetSegmentationDataNode(m_ActiveSubtaskIndex.value()); + + if (segmentationNode != nullptr) + { + auto segmentation = static_cast(segmentationNode->GetData()); + + auto command = itk::SimpleMemberCommand::New(); + command->SetCallbackFunction(this, &QmitkSegmentationTaskWidget::OnSegmentationModified); + + m_SegmentationModifiedObserverTag = segmentation->AddObserver(itk::ModifiedEvent(), command); + } + } +} + +void QmitkSegmentationTaskWidget::UnsubscribeFromActiveSegmentation() +{ + if (m_ActiveSubtaskIndex.has_value() && m_SegmentationModifiedObserverTag.has_value()) + { + auto segmentationNode = this->GetSegmentationDataNode(m_ActiveSubtaskIndex.value()); + + if (segmentationNode != nullptr) + { + auto segmentation = static_cast(segmentationNode->GetData()); + segmentation->RemoveObserver(m_SegmentationModifiedObserverTag.value()); + } + + m_SegmentationModifiedObserverTag.reset(); + } +} + +void QmitkSegmentationTaskWidget::OnSegmentationModified() +{ + if (!m_UnsavedChanges) + { + m_UnsavedChanges = true; + + if (m_ActiveSubtaskIndex.value() == m_CurrentSubtaskIndex) + this->UpdateDetailsLabel(); + } } void QmitkSegmentationTaskWidget::SetActiveSubtaskIndex(const std::optional& index) { if (m_ActiveSubtaskIndex != index) { m_ActiveSubtaskIndex = index; emit ActiveSubtaskChanged(m_ActiveSubtaskIndex); } } void QmitkSegmentationTaskWidget::SetCurrentSubtaskIndex(size_t index) { if (m_CurrentSubtaskIndex != index) { m_CurrentSubtaskIndex = index; emit CurrentSubtaskChanged(m_CurrentSubtaskIndex); } } + +bool QmitkSegmentationTaskWidget::ActivateSubtaskIsShown() const +{ + return m_ActiveSubtaskIndex.has_value() && m_ActiveSubtaskIndex == m_CurrentSubtaskIndex; +} diff --git a/Plugins/org.mitk.gui.qt.flow.segmentation/src/internal/QmitkSegmentationTaskWidget.h b/Plugins/org.mitk.gui.qt.flow.segmentation/src/internal/QmitkSegmentationTaskWidget.h index eb33b57b63..794f8b7cd8 100644 --- a/Plugins/org.mitk.gui.qt.flow.segmentation/src/internal/QmitkSegmentationTaskWidget.h +++ b/Plugins/org.mitk.gui.qt.flow.segmentation/src/internal/QmitkSegmentationTaskWidget.h @@ -1,78 +1,85 @@ /*============================================================================ 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 QmitkSegmentationTaskWidget_h #define QmitkSegmentationTaskWidget_h #include #include #include #include #include class QFileSystemWatcher; namespace Ui { class QmitkSegmentationTaskWidget; } class QmitkSegmentationTaskWidget : public QWidget { Q_OBJECT public: explicit QmitkSegmentationTaskWidget(QWidget* parent = nullptr); ~QmitkSegmentationTaskWidget() override; mitk::SegmentationTask* GetTask() const; std::optional GetActiveSubtaskIndex() const; size_t GetCurrentSubtaskIndex() const; mitk::DataNode* GetSegmentationDataNode(size_t index) const; + void OnUnsavedChangesSaved(); signals: void ActiveSubtaskChanged(const std::optional& index); void CurrentSubtaskChanged(size_t index); private: void OnSelectionChanged(const QmitkSingleNodeSelectionWidget::NodeList& nodes); void ResetControls(); void SetTask(mitk::SegmentationTask* task); void ResetFileSystemWatcher(); void OnResultDirectoryChanged(const QString&); void UpdateProgressBar(); void OnTaskChanged(mitk::SegmentationTask* task); void OnPreviousButtonClicked(); void OnNextButtonClicked(); void OnSubtaskChanged(); void UpdateLoadButton(); void UpdateDetailsLabel(); void OnLoadButtonClicked(); mitk::DataNode* GetImageDataNode(size_t index) const; void UnloadSubtasks(const mitk::DataNode* skip = nullptr); void LoadSubtask(mitk::DataNode::Pointer imageNode = nullptr); + void SubscribeToActiveSegmentation(); + void UnsubscribeFromActiveSegmentation(); + void OnSegmentationModified(); void SetActiveSubtaskIndex(const std::optional& index); void SetCurrentSubtaskIndex(size_t index); + bool ActivateSubtaskIsShown() const; Ui::QmitkSegmentationTaskWidget* m_Ui; QFileSystemWatcher* m_FileSystemWatcher; mitk::SegmentationTask::Pointer m_Task; mitk::DataNode::Pointer m_TaskNode; size_t m_CurrentSubtaskIndex; std::optional m_ActiveSubtaskIndex; + std::optional m_SegmentationModifiedObserverTag; + bool m_UnsavedChanges; }; #endif