Quellcode durchsuchen

ENH: add ability to delete individual series, studies, patients

Add to the browser Remove button the information that it will
delete all selected series, studies, patients.

Add support for right click custom context menus in the
patient, study, series tables. Translate the local points
to global ones in terms of the table view and signal
right clicked with the global position so that the browser
can position the context menu appropriately.

Add the context menu to the DICOM browser to allow
deleting at just one level, with number of items
selected information, and a confirmation message box with descriptive
information that can be set to not be shown again.

Added a test for the DICOM browser, with the ability
to open it in interactive mode to test the right clicks.

Add query wrappers for the DICOM database to get the patient name
as well as descriptions for series and study. They will return empty
strings on failure.
Use the new queries to give more meaningful information on the right
click pop up menu in the DICOM browser.
Added a test for the new DICOM database accessors.

Slicer bug:
http://www.na-mic.org/Bug/view.php?id=3792

When Slicer makes a custom DICOM browser, it moves the table views
directly into the new window, extracting them from the DICOM
table.
With this change, the table view takes care of mapping the point to
global, the table manager propagates the signal as a table specific
XRightClicked signal, and the browser responds to it with a table
specific context menu.

Slicer Issue #3792
Nicole Aucoin vor 9 Jahren
Ursprung
Commit
ea653d2cfc

+ 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