ソースを参照

Merge pull request #597 from naucoin/3792-unable-to-remove-one-series-of-an-exam-rebase

3792 unable to remove one series of an exam rebase
Jean-Christophe Fillion-Robin 9 年 前
コミット
33c8999696

+ 2 - 0
Libs/DICOM/Core/Testing/Cpp/CMakeLists.txt

@@ -7,6 +7,7 @@ create_test_sourcelist(Tests ${KIT}CppTests.cpp
   ctkDICOMDatabaseTest3.cpp
   ctkDICOMDatabaseTest4.cpp
   ctkDICOMDatabaseTest5.cpp
+  ctkDICOMDatabaseTest6.cpp
   ctkDICOMItemTest1.cpp
   ctkDICOMIndexerTest1.cpp
   ctkDICOMModelTest1.cpp
@@ -44,6 +45,7 @@ SIMPLE_TEST(ctkDICOMDatabaseTest3
   )
 SIMPLE_TEST(ctkDICOMDatabaseTest4 ${CTKData_DIR}/Data/DICOM/MRHEAD/000055.IMA)
 SIMPLE_TEST(ctkDICOMDatabaseTest5 ${CTKData_DIR}/Data/DICOM/MRHEAD/000055.IMA)
+SIMPLE_TEST(ctkDICOMDatabaseTest6 ${CTKData_DIR}/Data/DICOM/MRHEAD/000055.IMA)
 SIMPLE_TEST(ctkDICOMItemTest1)
 SIMPLE_TEST(ctkDICOMIndexerTest1 )
 

+ 155 - 0
Libs/DICOM/Core/Testing/Cpp/ctkDICOMDatabaseTest6.cpp

@@ -0,0 +1,155 @@
+/*=========================================================================
+
+  Library:   CTK
+
+  Copyright (c) Kitware Inc.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0.txt
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+
+=========================================================================*/
+
+// Qt includes
+#include <QCoreApplication>
+#include <QDir>
+
+// ctkDICOMCore includes
+#include "ctkDICOMDatabase.h"
+
+// STD includes
+#include <iostream>
+#include <cstdlib>
+
+
+int ctkDICOMDatabaseTest6( int argc, char * argv [] )
+{
+  QCoreApplication app(argc, argv);
+
+  if (argc < 2)
+    {
+    std::cerr << "ctkDICOMDatabaseTest6: missing dicom filePath argument";
+    std::cerr << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  QString dicomFilePath(argv[1]);
+
+  ctkDICOMDatabase database;
+  QDir databaseDirectory = QDir::temp();
+  databaseDirectory.remove("ctkDICOMDatabase.sql");
+  databaseDirectory.remove("ctkDICOMTagCache.sql");
+
+  QFileInfo databaseFile(databaseDirectory, QString("database.test"));
+  database.openDatabase(databaseFile.absoluteFilePath());
+
+  bool res = database.initializeDatabase();
+
+  if (!res)
+    {
+    std::cerr << "ctkDICOMDatabase::initializeDatabase() failed." << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  //
+  // Basic test:
+  // - insert the file specified on the command line
+  // - ask for name and descriptions and compare to known results
+  //
+  QString instanceUID("1.2.840.113619.2.135.3596.6358736.4843.1115808177.83");
+
+
+  //
+  // Test the pre load values feature of the database
+  //
+
+  QString preInsertDescription = database.descriptionForSeries(instanceUID);
+  if (!preInsertDescription.isEmpty())
+    {
+      std::cerr
+        << "ctkDICOMDatabase: db should return empty string for unknown "
+        << " instance series description, instead got: "
+        << preInsertDescription.toStdString() << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  database.insert(dicomFilePath, false, false);
+
+  QString filePath = database.fileForInstance(instanceUID);
+  std::cerr << "Instance file " << filePath.toStdString() << std::endl;
+
+  // check for descriptions
+  QHash<QString,QString> descriptions (database.descriptionsForFile(filePath));
+  std::cout << "\tPatient Name: "
+            <<  descriptions["PatientsName"].toStdString()
+            << "\n\tStudy Desciption: "
+            <<  descriptions["StudyDescription"].toStdString()
+            << "\n\tSeries Desciption: "
+            <<  descriptions["SeriesDescription"].toStdString()
+            << std::endl;
+
+  // check for known series description
+  QString knownSeriesDescription("3D Cor T1 FAST IR-prepped GRE");
+
+  QString seriesUID = database.seriesForFile(filePath);
+  QString seriesDescription = database.descriptionForSeries(seriesUID);
+
+  if (seriesDescription != knownSeriesDescription)
+    {
+    std::cerr << "ctkDICOMDatabase: database should return series description of '"
+              << knownSeriesDescription.toStdString()
+              << "', instead returned '" << seriesDescription.toStdString()
+              << "'\n\tinstanceUID = "
+              << instanceUID.toStdString()
+              << "\n\tseriesUID = "
+              << seriesUID.toStdString()
+              << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  // get the study and patient uids
+  QString patientUID, studyUID;
+  studyUID = database.studyForSeries(seriesUID);
+  patientUID = database.patientForStudy(studyUID);
+
+  // check for empty study description
+  QString studyDescription = database.descriptionForStudy(studyUID);
+
+  if (!studyDescription.isEmpty())
+    {
+    std::cerr << "ctkDICOMDatabase: database should return empty study"
+              << " description for studyUID of "
+              << studyUID.toStdString() << ", instead returned '"
+              << studyDescription.toStdString() << "'"
+              << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  // check for known patient name
+  QString knownPatientName("Facial Expression");
+  QString patientName = database.nameForPatient(patientUID);
+  if (patientName != knownPatientName)
+    {
+    std::cerr << "ctkDICOMDatabase: database should return known patient name '"
+              << knownPatientName.toStdString()
+              << "' for patient UID of "
+              << patientUID.toStdString() << ", instead returned '"
+              << patientName.toStdString() << "'"
+              << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  database.closeDatabase();
+
+  std::cerr << "Database is in " << databaseDirectory.path().toStdString() << std::endl;
+
+  return EXIT_SUCCESS;
+}

+ 57 - 0
Libs/DICOM/Core/ctkDICOMDatabase.cpp

@@ -659,6 +659,63 @@ QHash<QString,QString> ctkDICOMDatabase::descriptionsForFile(QString fileName)
 }
 
 //------------------------------------------------------------------------------
+QString ctkDICOMDatabase::descriptionForSeries(const QString seriesUID)
+{
+  Q_D(ctkDICOMDatabase);
+
+  QString result;
+
+  QSqlQuery query(d->Database);
+  query.prepare ( "SELECT SeriesDescription FROM Series WHERE SeriesInstanceUID= ?" );
+  query.bindValue ( 0, seriesUID);
+  query.exec();
+  if (query.next())
+    {
+    result = query.value(0).toString();
+    }
+
+  return result;
+}
+
+//------------------------------------------------------------------------------
+QString ctkDICOMDatabase::descriptionForStudy(const QString studyUID)
+{
+  Q_D(ctkDICOMDatabase);
+
+  QString result;
+
+  QSqlQuery query(d->Database);
+  query.prepare ( "SELECT StudyDescription FROM Studies WHERE StudyInstanceUID= ?" );
+  query.bindValue ( 0, studyUID);
+  query.exec();
+  if (query.next())
+    {
+    result =  query.value(0).toString();
+    }
+
+  return result;
+}
+
+//------------------------------------------------------------------------------
+QString ctkDICOMDatabase::nameForPatient(const QString patientUID)
+{
+  Q_D(ctkDICOMDatabase);
+
+  QString result;
+
+  QSqlQuery query(d->Database);
+  query.prepare ( "SELECT PatientsName FROM Patients WHERE UID= ?" );
+  query.bindValue ( 0, patientUID);
+  query.exec();
+  if (query.next())
+    {
+    result =  query.value(0).toString();
+    }
+
+  return result;
+}
+
+//------------------------------------------------------------------------------
 QStringList ctkDICOMDatabase::seriesForStudy(QString studyUID)
 {
   Q_D(ctkDICOMDatabase);

+ 3 - 0
Libs/DICOM/Core/ctkDICOMDatabase.h

@@ -136,6 +136,9 @@ public:
   Q_INVOKABLE QString patientForStudy(QString studyUID);
   Q_INVOKABLE QStringList filesForSeries (const QString seriesUID);
   Q_INVOKABLE QHash<QString,QString> descriptionsForFile(QString fileName);
+  Q_INVOKABLE QString descriptionForSeries(const QString seriesUID);
+  Q_INVOKABLE QString descriptionForStudy(const QString studyUID);
+  Q_INVOKABLE QString nameForPatient(const QString patientUID);
   Q_INVOKABLE QString fileForInstance (const QString sopInstanceUID);
   Q_INVOKABLE QString seriesForFile (QString fileName);
   Q_INVOKABLE QString instanceForFile (const QString fileName);

+ 1 - 1
Libs/DICOM/Widgets/Resources/UI/ctkDICOMBrowser.ui

@@ -203,7 +203,7 @@
     <string>Remove</string>
    </property>
    <property name="toolTip">
-    <string>Remove from database</string>
+    <string>Remove selected series, studies, patients from database</string>
    </property>
   </action>
   <action name="ActionRepair">

+ 2 - 0
Libs/DICOM/Widgets/Testing/Cpp/CMakeLists.txt

@@ -2,6 +2,7 @@ set(KIT ${PROJECT_NAME})
 
 create_test_sourcelist(Tests ${KIT}CppTests.cpp
   ctkDICOMAppWidgetTest1.cpp
+  ctkDICOMBrowserTest1.cpp
   ctkDICOMItemViewTest1.cpp
   ctkDICOMDirectoryListWidgetTest1.cpp
   ctkDICOMImageTest1.cpp
@@ -27,6 +28,7 @@ target_link_libraries(${KIT}CppTests ${LIBRARY_NAME})
 #
 
 SIMPLE_TEST(ctkDICOMAppWidgetTest1 ${CTKData_DIR}/Data/DICOM/MRHEAD)
+SIMPLE_TEST(ctkDICOMBrowserTest1 ${CTKData_DIR}/Data/DICOM/MRHEAD)
 SIMPLE_TEST(ctkDICOMItemViewTest1 ${CTKData_DIR}/Data/DICOM/MRHEAD/000055.IMA)
 SIMPLE_TEST(ctkDICOMDirectoryListWidgetTest1)
 SIMPLE_TEST(ctkDICOMImageTest1 ${CTKData_DIR}/Data/DICOM/MRHEAD/000055.IMA)

+ 97 - 0
Libs/DICOM/Widgets/Testing/Cpp/ctkDICOMBrowserTest1.cpp

@@ -0,0 +1,97 @@
+/*=========================================================================
+
+  Library:   CTK
+
+  Copyright (c) Kitware Inc.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0.txt
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+
+=========================================================================*/
+
+// Qt includes
+#include <QApplication>
+#include <QDir>
+#include <QTimer>
+
+// ctk includes
+#include "ctkUtils.h"
+
+// ctkDICOMCore includes
+#include "ctkDICOMDatabase.h"
+
+// ctkDICOMWidget includes
+#include "ctkDICOMBrowser.h"
+
+// STD includes
+#include <iostream>
+
+int ctkDICOMBrowserTest1( int argc, char * argv [] )
+{
+  QApplication app(argc, argv);
+
+  qDebug() << "argc = " << argc;
+  for (int i = 0; i < argc; ++i)
+    {
+    qDebug() << "\t" << argv[i];
+    }
+
+  QFileInfo tempFileInfo(QDir::tempPath() + QString("/ctkDICOMBrowserTest1-db"));
+  QString dbDir = tempFileInfo.absoluteFilePath();
+  qDebug() << "\n\nUsing directory: " << dbDir;
+  if (tempFileInfo.exists())
+    {
+    qDebug() << "\n\nRemoving directory: " << dbDir;
+    ctk::removeDirRecursively(dbDir);
+    }
+  qDebug() << "\n\nMaking directory: " << dbDir;
+  QDir dir(dbDir);
+  dir.mkdir(dbDir);
+
+  ctkDICOMBrowser browser;
+  browser.setDatabaseDirectory(dbDir);
+
+  browser.show();
+
+  browser.setDisplayImportSummary(false);
+  qDebug() << "Importing directory " << argv[1];
+
+  // make sure copy/link dialog doesn't pop up, always copy on import
+  QSettings settings;
+  QString settingsString = settings.value("MainWindow/DontConfirmCopyOnImport").toString();
+  settings.setValue("MainWindow/DontConfirmCopyOnImport", QString("0"));
+
+  browser.onImportDirectory(argv[1]);
+
+  // reset to the original copy/import setting
+  settings.setValue("MainWindow/DontConfirmCopyOnImport", settingsString);
+
+  if (browser.patientsAddedDuringImport() != 1
+    || browser.studiesAddedDuringImport() != 1
+    || browser.seriesAddedDuringImport() != 1
+    || browser.instancesAddedDuringImport() != 100)
+    {
+    qDebug() << "\n\nDirectory did not import as expected!\n\n";
+    return EXIT_FAILURE;
+    }
+
+  qDebug() << "\n\nAdded to database directory: " << dbDir;
+
+  if (argc <= 2 || QString(argv[argc - 1]) != "-I")
+    {
+    QTimer::singleShot(200, &app, SLOT(quit()));
+    }
+
+
+
+  return app.exec();
+}

+ 184 - 0
Libs/DICOM/Widgets/ctkDICOMBrowser.cpp

@@ -29,10 +29,12 @@
 #include <QDebug>
 #include <QFile>
 #include <QListView>
+#include <QMenu>
 #include <QMessageBox>
 #include <QProgressDialog>
 #include <QSettings>
 #include <QStringListModel>
+#include <QWidgetAction>
 
 // ctkWidgets includes
 #include "ctkDirectoryButton.h"
@@ -245,6 +247,14 @@ ctkDICOMBrowser::ctkDICOMBrowser(QWidget* _parent):Superclass(_parent),
   connect(d->dicomTableManager, SIGNAL(seriesSelectionChanged(const QItemSelection&, const QItemSelection&)),
           this, SLOT(onModelSelected(const QItemSelection&,const QItemSelection&)));
 
+  // set up context menus for working on selected patients, studies, series
+  connect(d->dicomTableManager, SIGNAL(patientsRightClicked(const QPoint&)),
+          this, SLOT(onPatientsRightClicked(const QPoint&)));
+  connect(d->dicomTableManager, SIGNAL(studiesRightClicked(const QPoint&)),
+          this, SLOT(onStudiesRightClicked(const QPoint&)));
+  connect(d->dicomTableManager, SIGNAL(seriesRightClicked(const QPoint&)),
+          this, SLOT(onSeriesRightClicked(const QPoint&)));
+
   connect(d->DirectoryButton, SIGNAL(directoryChanged(QString)), this, SLOT(setDatabaseDirectory(QString)));
 
   //Initialize import widget
@@ -657,3 +667,177 @@ void ctkDICOMBrowser::onModelSelected(const QItemSelection &item1, const QItemSe
   Q_D(ctkDICOMBrowser);
   d->ActionRemove->setEnabled(true);
 }
+
+//----------------------------------------------------------------------------
+bool ctkDICOMBrowser::confirmDeleteSelectedIUDs(QStringList uids)
+{
+  Q_D(ctkDICOMBrowser);
+
+  if (uids.isEmpty())
+    {
+    return false;
+    }
+
+  ctkMessageBox confirmDeleteDialog;
+  QString message("Do you want to delete the following selected items?");
+
+  // add the information about the selected UIDs
+  int numUIDs = uids.size();
+  for (int i = 0; i < numUIDs; ++i)
+    {
+    QString uid = uids.at(i);
+
+    // try using the given UID to find a descriptive string
+    QString patientName = d->DICOMDatabase->nameForPatient(uid);
+    QString studyDescription = d->DICOMDatabase->descriptionForStudy(uid);
+    QString seriesDescription = d->DICOMDatabase->descriptionForSeries(uid);
+
+    if (!patientName.isEmpty())
+      {
+      message += QString("\n") + patientName;
+      }
+    else if (!studyDescription.isEmpty())
+      {
+      message += QString("\n") + studyDescription;
+      }
+    else if (!seriesDescription.isEmpty())
+      {
+      message += QString("\n") + seriesDescription;
+      }
+    else
+      {
+      // if all other descriptors are empty, use the UID
+      message += QString("\n") + uid;
+      }
+
+    }
+  confirmDeleteDialog.setText(message);
+  confirmDeleteDialog.setIcon(QMessageBox::Question);
+
+  confirmDeleteDialog.addButton("Delete", QMessageBox::AcceptRole);
+  confirmDeleteDialog.addButton("Cancel", QMessageBox::RejectRole);
+  confirmDeleteDialog.setDontShowAgainSettingsKey( "MainWindow/DontConfirmDeleteSelected");
+
+  int response = confirmDeleteDialog.exec();
+
+  if (response == QMessageBox::AcceptRole)
+    {
+    return true;
+    }
+  else
+    {
+    return false;
+    }
+}
+
+//----------------------------------------------------------------------------
+void ctkDICOMBrowser::onPatientsRightClicked(const QPoint &point)
+{
+  Q_D(ctkDICOMBrowser);
+
+  // get the list of patients that are selected
+  QStringList selectedPatientsUIDs = d->dicomTableManager->currentPatientsSelection();
+  int numPatients = selectedPatientsUIDs.size();
+  if (numPatients == 0)
+    {
+    qDebug() << "No patients selected!";
+    return;
+    }
+
+  QMenu *patientsMenu = new QMenu(d->dicomTableManager);
+
+  QString deleteString = QString("Delete ")
+    + QString::number(numPatients)
+    + QString(" selected patients");
+  QAction *deleteAction = new QAction(deleteString, patientsMenu);
+
+  patientsMenu->addAction(deleteAction);
+
+  // the table took care of mapping it to a global position so that the
+  // menu will pop up at the correct place over this table.
+  QAction *selectedAction = patientsMenu->exec(point);
+
+  if (selectedAction == deleteAction
+      && this->confirmDeleteSelectedIUDs(selectedPatientsUIDs))
+    {
+    qDebug() << "Deleting " << numPatients << " patients";
+    foreach (const QString& uid, selectedPatientsUIDs)
+      {
+      d->DICOMDatabase->removePatient(uid);
+      }
+    }
+}
+
+//----------------------------------------------------------------------------
+void ctkDICOMBrowser::onStudiesRightClicked(const QPoint &point)
+{
+  Q_D(ctkDICOMBrowser);
+
+  // get the list of studies that are selected
+  QStringList selectedStudiesUIDs = d->dicomTableManager->currentStudiesSelection();
+  int numStudies = selectedStudiesUIDs.size();
+  if (numStudies == 0)
+    {
+    qDebug() << "No studies selected!";
+    return;
+    }
+
+  QMenu *studiesMenu = new QMenu(d->dicomTableManager);
+
+  QString deleteString = QString("Delete ")
+    + QString::number(numStudies)
+    + QString(" selected studies");
+  QAction *deleteAction = new QAction(deleteString, studiesMenu);
+
+  studiesMenu->addAction(deleteAction);
+
+  // the table took care of mapping it to a global position so that the
+  // menu will pop up at the correct place over this table.
+  QAction *selectedAction = studiesMenu->exec(point);
+
+  if (selectedAction == deleteAction
+      && this->confirmDeleteSelectedIUDs(selectedStudiesUIDs))
+    {
+    foreach (const QString& uid, selectedStudiesUIDs)
+      {
+      d->DICOMDatabase->removeStudy(uid);
+      }
+    }
+}
+
+//----------------------------------------------------------------------------
+void ctkDICOMBrowser::onSeriesRightClicked(const QPoint &point)
+{
+  Q_D(ctkDICOMBrowser);
+
+  // get the list of series that are selected
+  QStringList selectedSeriesUIDs = d->dicomTableManager->currentSeriesSelection();
+  int numSeries = selectedSeriesUIDs.size();
+  if (numSeries == 0)
+    {
+    qDebug() << "No series selected!";
+    return;
+    }
+
+  QMenu *seriesMenu = new QMenu(d->dicomTableManager);
+
+  QString deleteString = QString("Delete ")
+    + QString::number(numSeries)
+    + QString(" selected series");
+  QAction *deleteAction = new QAction(deleteString, seriesMenu);
+
+  seriesMenu->addAction(deleteAction);
+
+  // the table took care of mapping it to a global position so that the
+  // menu will pop up at the correct place over this table.
+  QAction *selectedAction = seriesMenu->exec(point);
+
+  if (selectedAction == deleteAction
+      && this->confirmDeleteSelectedIUDs(selectedSeriesUIDs))
+    {
+    foreach (const QString& uid, selectedSeriesUIDs)
+      {
+      d->DICOMDatabase->removeSeries(uid);
+      }
+    }
+}

+ 19 - 0
Libs/DICOM/Widgets/ctkDICOMBrowser.h

@@ -29,6 +29,7 @@
 
 class ctkDICOMBrowserPrivate;
 class ctkThumbnailLabel;
+class QMenu;
 class QModelIndex;
 class ctkDICOMDatabase;
 class ctkDICOMTableManager;
@@ -112,9 +113,27 @@ Q_SIGNALS:
 
 protected:
     QScopedPointer<ctkDICOMBrowserPrivate> d_ptr;
+
+    /// Confirm with the user that they wish to delete the selected uids.
+    /// Add information about the selected UIDs to a message box, checks
+    /// for patient name, series description, study description, if all
+    /// empty, uses the UID.
+    /// Returns true if the user confirms the delete, false otherwise.
+    /// Remembers if the user doesn't want to show the confirmation again.
+    bool confirmDeleteSelectedIUDs(QStringList uids);
+
 protected Q_SLOTS:
     void onModelSelected(const QItemSelection&, const QItemSelection&);
 
+    /// Called when a right mouse click is made in the patients table
+    void onPatientsRightClicked(const QPoint &point);
+
+    /// Called when a right mouse click is made in the studies table
+    void onStudiesRightClicked(const QPoint &point);
+
+    /// Called when a right mouse click is made in the series table
+    void onSeriesRightClicked(const QPoint &point);
+
     /// To be called when dialog finishes
     void onQueryRetrieveFinished();
 

+ 9 - 0
Libs/DICOM/Widgets/ctkDICOMTableManager.cpp

@@ -100,6 +100,15 @@ void ctkDICOMTableManagerPrivate::init()
 
   QObject::connect(this->seriesTable, SIGNAL(doubleClicked(const QModelIndex&)),
                    q, SIGNAL(seriesDoubleClicked(const QModelIndex&)));
+
+  // For propagating right clicks, the table takes care of translating to a global position
+  QObject::connect(this->patientsTable, SIGNAL(customContextMenuRequested(const QPoint&)),
+                   q, SIGNAL(patientsRightClicked(const QPoint&)));
+  QObject::connect(this->studiesTable, SIGNAL(customContextMenuRequested(const QPoint&)),
+                   q, SIGNAL(studiesRightClicked(const QPoint&)));
+
+  QObject::connect(this->seriesTable, SIGNAL(customContextMenuRequested(const QPoint&)),
+                   q, SIGNAL(seriesRightClicked(const QPoint&)));
 }
 
 //------------------------------------------------------------------------------

+ 7 - 0
Libs/DICOM/Widgets/ctkDICOMTableManager.h

@@ -116,6 +116,13 @@ Q_SIGNALS:
 
   void seriesDoubleClicked(const QModelIndex&);
 
+  // signals to propagate the context menu requests from
+  // the individual tables
+  void patientsRightClicked(const QPoint&);
+  void studiesRightClicked(const QPoint&);
+  void seriesRightClicked(const QPoint&);
+
+
 protected:
 
   virtual void resizeEvent(QResizeEvent *);

+ 18 - 0
Libs/DICOM/Widgets/ctkDICOMTableView.cpp

@@ -115,6 +115,13 @@ void ctkDICOMTableViewPrivate::init()
   QObject::connect(this->tblDicomDatabaseView, SIGNAL(doubleClicked(const QModelIndex&)),
                    q, SIGNAL(doubleClicked(const QModelIndex&)));
 
+  // enable right click menu, with mapping to global position (for use within the DICOM
+  // table manager)
+  this->tblDicomDatabaseView->setContextMenuPolicy(Qt::CustomContextMenu);
+  QObject::connect(this->tblDicomDatabaseView,
+                   SIGNAL(customContextMenuRequested(const QPoint&)),
+                   q, SLOT(onCustomContextMenuRequested(const QPoint&)));
+
   QObject::connect(this->leSearchBox, SIGNAL(textChanged(QString)),
                    this->dicomSQLFilterModel, SLOT(setFilterWildcard(QString)));
 
@@ -399,3 +406,14 @@ int ctkDICOMTableView::tableSectionSize()
   Q_D(ctkDICOMTableView);
   return d->tblDicomDatabaseView->verticalHeader()->defaultSectionSize();
 }
+
+//------------------------------------------------------------------------------
+void ctkDICOMTableView::onCustomContextMenuRequested(const QPoint &point)
+{
+  Q_D(ctkDICOMTableView);
+
+  // translate the local point to a global
+  QPoint globalPosition = d->tblDicomDatabaseView->mapToGlobal(point);
+
+  emit customContextMenuRequested(globalPosition);
+}

+ 7 - 0
Libs/DICOM/Widgets/ctkDICOMTableView.h

@@ -132,6 +132,13 @@ public Q_SLOTS:
    */
   void onUpdateQuery(const QStringList &uids);
 
+  /**
+   * @brief Translates the local point to a global one
+   * @param point the local point to translate to global
+   * Emits customContextMenuRequested with the global point
+   */
+  void onCustomContextMenuRequested(const QPoint &point);
+
 protected Q_SLOTS:
   /**
    * @brief Called when the underlying database changes