diff --git a/Libs/DICOM/Widgets/Resources/UI/Icons/more_vert.svg b/Libs/DICOM/Widgets/Resources/UI/Icons/more_vert.svg index e172f878a6..6d94a46582 100644 --- a/Libs/DICOM/Widgets/Resources/UI/Icons/more_vert.svg +++ b/Libs/DICOM/Widgets/Resources/UI/Icons/more_vert.svg @@ -1 +1,38 @@ - \ No newline at end of file + + + + + + diff --git a/Libs/DICOM/Widgets/Resources/UI/ctkDICOMVisualBrowserWidget.ui b/Libs/DICOM/Widgets/Resources/UI/ctkDICOMVisualBrowserWidget.ui index 88805e26fb..07820eb749 100644 --- a/Libs/DICOM/Widgets/Resources/UI/ctkDICOMVisualBrowserWidget.ui +++ b/Libs/DICOM/Widgets/Resources/UI/ctkDICOMVisualBrowserWidget.ui @@ -36,6 +36,52 @@ 2 + + + + color: rgb(0, 0, 0);background-color: rgb(245, 245, 170); + + + QFrame::Box + + + + + + + 0 + 0 + + + + Warning + + + + + + + Update database + + + + + + + Create new database + + + + + + + Select database folder + + + + + + @@ -733,8 +779,8 @@ Please set at least one filter to query the servers - 32 - 32 + 24 + 24 diff --git a/Libs/DICOM/Widgets/ctkDICOMVisualBrowserWidget.cpp b/Libs/DICOM/Widgets/ctkDICOMVisualBrowserWidget.cpp index f26cdddf28..b5735efa3f 100644 --- a/Libs/DICOM/Widgets/ctkDICOMVisualBrowserWidget.cpp +++ b/Libs/DICOM/Widgets/ctkDICOMVisualBrowserWidget.cpp @@ -29,12 +29,14 @@ #include #include #include +#include #include #include #include // CTK includes #include +#include #include #include #include @@ -136,6 +138,7 @@ class ctkDICOMVisualBrowserWidgetPrivate: public Ui_ctkDICOMVisualBrowserWidget void importDirectory(QString directory, ctkDICOMVisualBrowserWidget::ImportDirectoryMode mode); void importFiles(const QStringList& files, ctkDICOMVisualBrowserWidget::ImportDirectoryMode mode); void importOldSettings(); + void showUpdateSchemaDialog(); void updateModalityCheckableComboBox(); void createPatients(); void updateFiltersWarnings(); @@ -228,6 +231,8 @@ class ctkDICOMVisualBrowserWidgetPrivate: public Ui_ctkDICOMVisualBrowserWidget bool IsLoading; ctkDICOMServerNodeWidget2* ServerNodeWidget; + QProgressDialog *UpdateSchemaProgress; + QProgressDialog *ExportProgress; }; CTK_GET_CPP(ctkDICOMVisualBrowserWidget, QString, databaseDirectoryBase, DatabaseDirectoryBase); @@ -289,6 +294,9 @@ ctkDICOMVisualBrowserWidgetPrivate::ctkDICOMVisualBrowserWidgetPrivate(ctkDICOMV this->ServerNodeWidget = new ctkDICOMServerNodeWidget2(); this->ServerNodeWidget->setScheduler(this->Scheduler); this->connectScheduler(); + + this->ExportProgress = nullptr; + this->UpdateSchemaProgress = nullptr; } //---------------------------------------------------------------------------- @@ -303,6 +311,14 @@ void ctkDICOMVisualBrowserWidgetPrivate::init() Q_Q(ctkDICOMVisualBrowserWidget); this->setupUi(q); + this->DatabaseDirectoryProblemFrame->hide(); + QObject::connect(this->SelectDatabaseDirectoryButton, SIGNAL(clicked()), + q, SLOT(selectDatabaseDirectory())); + QObject::connect(this->CreateNewDatabaseButton, SIGNAL(clicked()), + q, SLOT(createNewDatabaseDirectory())); + QObject::connect(this->UpdateDatabaseButton, SIGNAL(clicked()), + q, SLOT(updateDatabase())); + this->WarningPushButton->hide(); QObject::connect(this->FilteringPatientIDSearchBox, SIGNAL(textChanged(QString)), q, SLOT(onFilteringPatientIDChanged())); @@ -327,20 +343,11 @@ void ctkDICOMVisualBrowserWidgetPrivate::init() q, SLOT(onQueryPatients())); this->ServersSettingsCollapsibleGroupBox->layout()->addWidget(this->ServerNodeWidget); - - QSize iconSize(28, 28); - this->PatientsTabWidget->setIconSize(iconSize); this->PatientsTabWidget->clear(); - // setup patients menu on a tool button on the tab bar - QTabBar* tabWidget = this->PatientsTabWidget->tabBar(); - tabWidget->setDocumentMode(true); - tabWidget->setExpanding(true); - + // setup patients menu this->patientsTabMenuToolButton = new QToolButton(q); this->patientsTabMenuToolButton->setObjectName("patientsTabMenuToolButton"); - this->patientsTabMenuToolButton->setIconSize(iconSize); - this->patientsTabMenuToolButton->setFixedHeight(40); this->patientsTabMenuToolButton->setCheckable(false); this->patientsTabMenuToolButton->setChecked(false); this->patientsTabMenuToolButton->setIcon(QIcon(":/Icons/more_vert.svg")); @@ -349,8 +356,7 @@ void ctkDICOMVisualBrowserWidgetPrivate::init() QObject::connect(this->patientsTabMenuToolButton, SIGNAL(clicked()), q, SLOT(onPatientsTabMenuToolButtonClicked())); - this->PatientsTabWidget->setCornerWidget(this->patientsTabMenuToolButton, Qt::TopRightCorner); - + this->PatientsTabWidget->setCornerWidget(this->patientsTabMenuToolButton, Qt::TopLeftCorner); QObject::connect(this->PatientsTabWidget, SIGNAL(currentChanged(int)), q, SLOT(onPatientItemChanged(int))); @@ -485,12 +491,45 @@ void ctkDICOMVisualBrowserWidgetPrivate::importOldSettings() QSettings settings; int dontConfirmCopyOnImport = settings.value("MainWindow/DontConfirmCopyOnImport", static_cast(QMessageBox::InvalidRole)).toInt(); if (dontConfirmCopyOnImport == QMessageBox::AcceptRole) - { + { settings.setValue("DICOM/ImportDirectoryMode", static_cast(ctkDICOMVisualBrowserWidget::ImportDirectoryCopy)); - } + } settings.remove("MainWindow/DontConfirmCopyOnImport"); } +//---------------------------------------------------------------------------- +void ctkDICOMVisualBrowserWidgetPrivate::showUpdateSchemaDialog() +{ + Q_Q(ctkDICOMVisualBrowserWidget); + if (this->UpdateSchemaProgress == 0) + { + // + // Set up the Update Schema Progress Dialog + // + this->UpdateSchemaProgress = new QProgressDialog( + ctkDICOMVisualBrowserWidget::tr("DICOM Schema Update"), ctkDICOMVisualBrowserWidget::tr("Cancel"), 0, 100, q, Qt::WindowTitleHint | Qt::WindowSystemMenuHint); + + // We don't want the progress dialog to resize itself, so we bypass the label by creating our own + QLabel* progressLabel = new QLabel(ctkDICOMVisualBrowserWidget::tr("Initialization...")); + this->UpdateSchemaProgress->setLabel(progressLabel); + this->UpdateSchemaProgress->setWindowModality(Qt::ApplicationModal); + this->UpdateSchemaProgress->setMinimumDuration(0); + this->UpdateSchemaProgress->setValue(0); + + q->connect(DicomDatabase.data(), SIGNAL(schemaUpdateStarted(int)), + this->UpdateSchemaProgress, SLOT(setMaximum(int))); + q->connect(DicomDatabase.data(), SIGNAL(schemaUpdateProgress(int)), + this->UpdateSchemaProgress, SLOT(setValue(int))); + q->connect(DicomDatabase.data(), SIGNAL(schemaUpdateProgress(QString)), + progressLabel, SLOT(setText(QString))); + + // close the dialog + q->connect(this->DicomDatabase.data(), SIGNAL(schemaUpdated()), + this->UpdateSchemaProgress, SLOT(close())); + } + this->UpdateSchemaProgress->show(); +} + //---------------------------------------------------------------------------- void ctkDICOMVisualBrowserWidgetPrivate::updateModalityCheckableComboBox() { @@ -1931,9 +1970,17 @@ void ctkDICOMVisualBrowserWidget::setDatabaseDirectory(const QString &directory) bool success = true; if (!QDir(absDirectory).exists() - || (!QDir(absDirectory).isEmpty() && !QFile(databaseFileName).exists())) + || (!ctk::isDirEmpty(QDir(absDirectory)) && !QFile(databaseFileName).exists())) { logger.warn(tr("Database folder does not contain ctkDICOM.sql file: ") + absDirectory + "\n"); + d->DatabaseDirectoryProblemFrame->show(); + d->DatabaseDirectoryProblemLabel->setText( + //: %1 is the folder path + tr("No valid DICOM database found in folder %1.").arg(absDirectory) + ); + d->UpdateDatabaseButton->hide(); + d->CreateNewDatabaseButton->show(); + d->SelectDatabaseDirectoryButton->show(); success = false; } @@ -1947,12 +1994,21 @@ void ctkDICOMVisualBrowserWidget::setDatabaseDirectory(const QString &directory) } catch (std::exception e) { + Q_UNUSED(e); databaseOpenSuccess = false; } if (!databaseOpenSuccess || d->DicomDatabase->schemaVersionLoaded().isEmpty()) { logger.warn(tr("Database error: %1 \n").arg(d->DicomDatabase->lastError())); d->DicomDatabase->closeDatabase(); + d->DatabaseDirectoryProblemFrame->show(); + d->DatabaseDirectoryProblemLabel->setText( + //: %1 is the folder path + tr("No valid DICOM database found in folder %1.").arg(absDirectory) + ); + d->UpdateDatabaseButton->hide(); + d->CreateNewDatabaseButton->show(); + d->SelectDatabaseDirectoryButton->show(); success = false; } } @@ -1964,10 +2020,23 @@ void ctkDICOMVisualBrowserWidget::setDatabaseDirectory(const QString &directory) logger.warn(tr("Database version mismatch: version of selected database = %1, version required = %2 \n") .arg(d->DicomDatabase->schemaVersionLoaded()).arg(d->DicomDatabase->schemaVersion())); d->DicomDatabase->closeDatabase(); + d->DatabaseDirectoryProblemFrame->show(); + d->DatabaseDirectoryProblemLabel->setText( + //: %1 is the folder path + tr("Incompatible DICOM database version found in folder %1.").arg(absDirectory) + ); + d->UpdateDatabaseButton->show(); + d->CreateNewDatabaseButton->show(); + d->SelectDatabaseDirectoryButton->show(); success = false; } } + if (success) + { + d->DatabaseDirectoryProblemFrame->hide(); + } + // Save new database directory in this object and in application settings. d->DatabaseDirectory = absDirectory; if (!d->DatabaseDirectorySettingsKey.isEmpty()) @@ -1977,6 +2046,8 @@ void ctkDICOMVisualBrowserWidget::setDatabaseDirectory(const QString &directory) settings.sync(); } + this->onShowPatients(); + // pass DICOM database instance to Import widget emit databaseDirectoryChanged(absDirectory); } @@ -2084,6 +2155,123 @@ void ctkDICOMVisualBrowserWidget::onIndexingComplete(int patientsAdded, int stud d->createPatients(); } +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::selectDatabaseDirectory() +{ + Q_D(const ctkDICOMVisualBrowserWidget); + d->DatabaseDirectoryProblemFrame->hide(); + ctkDirectoryButton directoryButton(this); + directoryButton.setDirectory(d->DatabaseDirectory); + QString dir = directoryButton.browse(); + this->setDatabaseDirectory(dir); +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::createNewDatabaseDirectory() +{ + Q_D(ctkDICOMVisualBrowserWidget); + + // Use the current database folder as a basis for the new name + QString baseFolder = this->databaseDirectory(); + if (baseFolder.isEmpty()) + { + baseFolder = d->DefaultDatabaseDirectory; + } + else + { + // only use existing folder name as a basis if it is empty or + // a valid database + if (!ctk::isDirEmpty(QDir(baseFolder))) + { + QString databaseFileName = QDir(baseFolder).filePath("ctkDICOM.sql"); + if (!QFile(databaseFileName).exists()) + { + // current folder is a non-empty and not a DICOM database folder + // create a subfolder for the new DICOM database based on the name + // of default database path + QFileInfo defaultFolderInfo(d->DefaultDatabaseDirectory); + QString defaultSubfolderName = defaultFolderInfo.fileName(); + if (defaultSubfolderName.isEmpty()) + { + defaultSubfolderName = defaultFolderInfo.dir().dirName(); + } + baseFolder += "/" + defaultSubfolderName; + } + } + } + // Remove existing numerical suffix + QString separator = "_"; + bool isSuffixValid = false; + QString suffixStr = baseFolder.split(separator).last(); + int suffixStart = suffixStr.toInt(&isSuffixValid); + if (isSuffixValid) + { + QStringList baseFolderComponents = baseFolder.split(separator); + baseFolderComponents.removeLast(); + baseFolder = baseFolderComponents.join(separator); + } + // Try folder names, starting with the current one, + // incrementing the original numerical suffix. + int attemptsCount = 100; + for (int attempt=0; attemptDatabaseDirectoryProblemFrame->show(); + d->DatabaseDirectoryProblemLabel->setText( + //: %1 is the folder path + tr("Failed to create new database in folder %1.").arg(QDir(baseFolder).absolutePath()) + ); + d->UpdateDatabaseButton->hide(); + d->CreateNewDatabaseButton->show(); + d->SelectDatabaseDirectoryButton->show(); +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::updateDatabase() +{ + Q_D(ctkDICOMVisualBrowserWidget); + d->DatabaseDirectoryProblemFrame->hide(); + d->showUpdateSchemaDialog(); + QString dir = this->databaseDirectory(); + // open DICOM database on the directory + QString databaseFileName = QDir(dir).filePath("ctkDICOM.sql"); + try + { + d->DicomDatabase->openDatabase(databaseFileName); + } + catch (const std::exception& e) + { + Q_UNUSED(e); + std::cerr << "Database error: " << qPrintable(d->DicomDatabase->lastError()) << "\n"; + d->DicomDatabase->closeDatabase(); + return; + } + d->DicomDatabase->updateSchema(); + // Update GUI + this->setDatabaseDirectory(dir); +} + //------------------------------------------------------------------------------ QStringList ctkDICOMVisualBrowserWidget::fileListForCurrentSelection(ctkDICOMModel::IndexType level, QList selectedWidgets) @@ -2905,6 +3093,7 @@ void ctkDICOMVisualBrowserWidget::exportSeries(QString dirPath, QStringList uids } destinationDir += sep; + // create the destination directory if necessary if (!QDir().exists(destinationDir)) { @@ -2922,7 +3111,23 @@ void ctkDICOMVisualBrowserWidget::exportSeries(QString dirPath, QStringList uids } } + // show progress + if (d->ExportProgress == 0) + { + d->ExportProgress = new QProgressDialog(tr("DICOM Export"), tr("Close"), 0, 100, this, Qt::WindowTitleHint | Qt::WindowSystemMenuHint); + d->ExportProgress->setWindowModality(Qt::ApplicationModal); + d->ExportProgress->setMinimumDuration(0); + } + QLabel *exportLabel = new QLabel( + //: %1 is the series number + tr("Exporting series %1").arg(seriesNumber) + ); + d->ExportProgress->setLabel(exportLabel); + d->ExportProgress->setValue(0); + int fileNumber = 0; + int numFiles = filesForSeries.size(); + d->ExportProgress->setMaximum(numFiles); foreach (const QString& filePath, filesForSeries) { // File name example: my/destination/folder/000001.dcm @@ -2930,11 +3135,12 @@ void ctkDICOMVisualBrowserWidget::exportSeries(QString dirPath, QStringList uids if (!QFile::exists(filePath)) { + d->ExportProgress->setValue(numFiles); //: %1 is the file path QString errorString = tr("Export source file not found:\n\n%1" "\n\nHalting export.\n\nError may be fixed via Repair.") .arg(filePath); - ctkMessageBox copyErrorMessageBox(this); + ctkMessageBox copyErrorMessageBox; copyErrorMessageBox.setText(errorString); copyErrorMessageBox.setIcon(QMessageBox::Warning); copyErrorMessageBox.exec(); @@ -2942,6 +3148,7 @@ void ctkDICOMVisualBrowserWidget::exportSeries(QString dirPath, QStringList uids } if (QFile::exists(destinationFileName)) { + d->ExportProgress->setValue(numFiles); //: %1 is the destination file name QString errorString = tr("Export destination file already exists:\n\n%1" "\n\nHalting export.") @@ -2956,6 +3163,7 @@ void ctkDICOMVisualBrowserWidget::exportSeries(QString dirPath, QStringList uids bool copyResult = QFile::copy(filePath, destinationFileName); if (!copyResult) { + d->ExportProgress->setValue(numFiles); //: %1 and %2 refers to source and destination file paths QString errorString = tr("Failed to copy\n\n%1\n\nto\n\n%2" "\n\nHalting export.") @@ -2969,8 +3177,10 @@ void ctkDICOMVisualBrowserWidget::exportSeries(QString dirPath, QStringList uids } fileNumber++; + d->ExportProgress->setValue(fileNumber); } - } + d->ExportProgress->setValue(numFiles); + } } //------------------------------------------------------------------------------ diff --git a/Libs/DICOM/Widgets/ctkDICOMVisualBrowserWidget.h b/Libs/DICOM/Widgets/ctkDICOMVisualBrowserWidget.h index 313ed5bcf6..08d7ec944d 100644 --- a/Libs/DICOM/Widgets/ctkDICOMVisualBrowserWidget.h +++ b/Libs/DICOM/Widgets/ctkDICOMVisualBrowserWidget.h @@ -299,6 +299,16 @@ public Q_SLOTS: void onIndexingProgressDetail(const QString&); void onIndexingComplete(int patientsAdded, int studiesAdded, int seriesAdded, int imagesAdded); + /// Show pop-up window for the user to select database directory + void selectDatabaseDirectory(); + + /// Create new database directory. + /// Current database directory used as a basis. + void createNewDatabaseDirectory(); + + /// Update database in-place to required schema version + void updateDatabase(); + void onFilteringPatientIDChanged(); void onFilteringPatientNameChanged(); void onFilteringStudyDescriptionChanged(); diff --git a/Libs/Widgets/ctkDirectoryButton.cpp b/Libs/Widgets/ctkDirectoryButton.cpp index 85ebc1135b..672b0c3db9 100644 --- a/Libs/Widgets/ctkDirectoryButton.cpp +++ b/Libs/Widgets/ctkDirectoryButton.cpp @@ -250,7 +250,7 @@ void ctkDirectoryButton::setAcceptMode(QFileDialog::AcceptMode mode) } //----------------------------------------------------------------------------- -void ctkDirectoryButton::browse() +QString ctkDirectoryButton::browse() { // See https://bugreports.qt-project.org/browse/QTBUG-10244 class ExcludeReadOnlyFilterProxyModel : public QSortFilterProxyModel @@ -308,9 +308,10 @@ void ctkDirectoryButton::browse() // An empty directory means either that the user cancelled the dialog or the selected directory is readonly if (dir.isEmpty()) { - return; + return ""; } this->setDirectory(dir); + return dir; } //----------------------------------------------------------------------------- diff --git a/Libs/Widgets/ctkDirectoryButton.h b/Libs/Widgets/ctkDirectoryButton.h index ae53a0f03e..2d92dfb36d 100644 --- a/Libs/Widgets/ctkDirectoryButton.h +++ b/Libs/Widgets/ctkDirectoryButton.h @@ -161,12 +161,12 @@ class CTK_WIDGETS_EXPORT ctkDirectoryButton: public QWidget /// set horizontal policy to QSizePolicy::Ignored and set elideMode to /// Qt::ElideMiddle (or anything else than Qt::ElideNone). void setElideMode(Qt::TextElideMode newMode); - Qt::TextElideMode elideMode()const; + Qt::TextElideMode elideMode()const; public Q_SLOTS: /// browse() opens a pop up where the user can select a new directory for the /// button. browse() is automatically called when the button is clicked. - void browse(); + QString browse(); Q_SIGNALS: /// directoryChanged is emitted when the current directory changes.