Esempio n. 1
0
class IdleDetectionEventFilter(qt.QObject):

  idleTimeoutSeconds = 30
  idleStarted = qt.Signal()
  idleEnded = qt.Signal()

  events = [
    qt.QEvent.MouseMove,
    qt.QEvent.KeyPress,
  ]

  def __init__(self):
    qt.QObject.__init__(self)

    self.idleTimer = qt.QTimer(self)
    self.idleTimer.setSingleShot(True)
    self.idling = False
    self.timeSinceLastEvent = vtk.vtkTimerLog.GetUniversalTime()

  def setInterval(self, interval):
    self.idleTimer.setInterval(interval)

  def start(self):
    self.idleTimer.timeout.connect(self.onIdleStarted)
    self.idleTimer.start()

  def stop(self):
    self.idleTimer.timeout.disconnect(self.onIdleStarted)
    self.idleTimer.stop()

  def eventFilter(self, object, event):
    if event.type() in self.events:
      self.timeSinceLastEvent = vtk.vtkTimerLog.GetUniversalTime()
      self.idleTimer.start()
      if self.idling:
        self.onIdleEnded()
      self.idling = False
    return False

  def getTimeSinceLastEvent(self):
    return vtk.vtkTimerLog.GetUniversalTime() - self.timeSinceLastEvent

  def onIdleStarted(self):
    self.idleStarted.emit()
    self.idling = True

  def onIdleEnded(self):
    self.idleEnded.emit()
Esempio n. 2
0
class UndockedViewWidget(qt.QSplitter):

    closed = qt.Signal()

    def closeEvent(self, event):
        self.closed.emit()
        event.accept()
Esempio n. 3
0
class SlicerDICOMBrowser(VTKObservationMixin, qt.QWidget):
    """Implement the Qt window showing details and possible
    operations to perform on the selected dicom list item.
    This is a helper used in the DICOMWidget class.
    """

    closed = qt.Signal(
    )  # Invoked when the dicom widget is closed using the close method

    def __init__(self, dicomBrowser=None, parent="mainWindow"):
        VTKObservationMixin.__init__(self)
        qt.QWidget.__init__(
            self,
            slicer.util.mainWindow() if parent == "mainWindow" else parent)

        self.pluginInstances = {}
        self.fileLists = []
        self.extensionCheckPending = False

        self.settings = qt.QSettings()

        self.dicomBrowser = dicomBrowser if dicomBrowser is not None else slicer.app.createDICOMBrowserForMainDatabase(
        )

        self.browserPersistent = settingsValue('DICOM/BrowserPersistent',
                                               False,
                                               converter=toBool)
        self.advancedView = settingsValue('DICOM/advancedView',
                                          0,
                                          converter=int)
        self.horizontalTables = settingsValue('DICOM/horizontalTables',
                                              0,
                                              converter=int)

        self.setup()

        self.dicomBrowser.connect('directoryImported()',
                                  self.onDirectoryImported)
        self.dicomBrowser.connect('sendRequested(QStringList)', self.onSend)

        # Load when double-clicked on an item in the browser
        self.dicomBrowser.dicomTableManager().connect(
            'patientsDoubleClicked(QModelIndex)',
            self.patientStudySeriesDoubleClicked)
        self.dicomBrowser.dicomTableManager().connect(
            'studiesDoubleClicked(QModelIndex)',
            self.patientStudySeriesDoubleClicked)
        self.dicomBrowser.dicomTableManager().connect(
            'seriesDoubleClicked(QModelIndex)',
            self.patientStudySeriesDoubleClicked)

    def open(self):
        self.show()

    def close(self):
        self.hide()
        self.closed.emit()

    def onSend(self, fileList):
        if len(fileList):
            sendDialog = DICOMLib.DICOMSendDialog(fileList, self)

    def setup(self, showPreview=False):
        """
        main window is a frame with widgets from the app
        widget repacked into it along with slicer-specific
        extra widgets
        """

        self.setWindowTitle('DICOM Browser')
        self.setLayout(qt.QVBoxLayout())

        self.dicomBrowser.databaseDirectorySelectorVisible = False
        self.dicomBrowser.toolbarVisible = False
        self.dicomBrowser.sendActionVisible = True
        self.dicomBrowser.databaseDirectorySettingsKey = slicer.dicomDatabaseDirectorySettingsKey
        self.dicomBrowser.dicomTableManager().dynamicTableLayout = False
        horizontal = self.settings.setValue('DICOM/horizontalTables', 0)
        self.dicomBrowser.dicomTableManager(
        ).tableOrientation = qt.Qt.Horizontal if horizontal else qt.Qt.Vertical
        self.layout().addWidget(self.dicomBrowser)

        self.userFrame = qt.QWidget()
        self.preview = qt.QWidget()

        #
        # preview related column
        #
        self.previewLayout = qt.QVBoxLayout()
        if showPreview:
            self.previewLayout.addWidget(self.preview)
        else:
            self.preview.hide()

        #
        # action related column (interacting with slicer)
        #
        self.loadableTableFrame = qt.QWidget()
        self.loadableTableFrame.setMaximumHeight(200)
        self.loadableTableLayout = qt.QVBoxLayout(self.loadableTableFrame)
        self.layout().addWidget(self.loadableTableFrame)

        self.loadableTableLayout.addWidget(self.userFrame)
        self.userFrame.hide()

        self.loadableTable = DICOMLoadableTable(self.userFrame)
        self.loadableTable.itemChanged.connect(self.onLoadableTableItemChanged)

        #
        # button row for action column
        #
        self.actionButtonsFrame = qt.QWidget()
        self.actionButtonsFrame.setMaximumHeight(40)
        self.actionButtonsFrame.objectName = 'ActionButtonsFrame'
        self.layout().addWidget(self.actionButtonsFrame)

        self.actionButtonLayout = qt.QHBoxLayout()
        self.actionButtonsFrame.setLayout(self.actionButtonLayout)

        self.uncheckAllButton = qt.QPushButton('Uncheck All')
        self.actionButtonLayout.addWidget(self.uncheckAllButton)
        self.uncheckAllButton.connect('clicked()', self.uncheckAllLoadables)

        self.actionButtonLayout.addStretch(0.05)

        self.examineButton = qt.QPushButton('Examine')
        self.examineButton.setSizePolicy(qt.QSizePolicy.Expanding,
                                         qt.QSizePolicy.Fixed)
        self.actionButtonLayout.addWidget(self.examineButton)
        self.examineButton.enabled = False
        self.examineButton.connect('clicked()', self.examineForLoading)

        self.loadButton = qt.QPushButton('Load')
        self.loadButton.setSizePolicy(qt.QSizePolicy.Expanding,
                                      qt.QSizePolicy.Fixed)
        self.loadButton.toolTip = 'Load selected items into the scene'
        self.actionButtonLayout.addWidget(self.loadButton)
        self.loadButton.connect('clicked()', self.loadCheckedLoadables)

        self.actionButtonLayout.addStretch(0.05)

        self.advancedViewButton = qt.QCheckBox('Advanced')
        self.advancedViewButton.objectName = 'AdvancedViewCheckBox'
        self.actionButtonLayout.addWidget(self.advancedViewButton)
        self.advancedViewButton.checked = self.advancedView
        self.advancedViewButton.toggled.connect(self.onAdvancedViewButton)

        if self.advancedView:
            self.loadableTableFrame.visible = True
        else:
            self.loadableTableFrame.visible = False
            self.examineButton.visible = False
            self.uncheckAllButton.visible = False

        #
        # Series selection
        #
        self.dicomBrowser.dicomTableManager().connect(
            'seriesSelectionChanged(QStringList)', self.onSeriesSelected)

        #
        # Loadable table widget (advanced)
        # DICOM Plugins selection widget is moved to module panel
        #
        self.loadableTableLayout.addWidget(self.loadableTable)
        self.updateButtonStates()

    def updateButtonStates(self):
        if self.advancedView:
            # self.loadButton.enabled =         loadEnabled = loadEnabled or loadablesByPlugin[plugin] != []
            loadablesChecked = self.loadableTable.getNumberOfCheckedItems() > 0
            self.loadButton.enabled = loadablesChecked
            self.examineButton.enabled = len(self.fileLists) != 0
            self.uncheckAllButton.enabled = loadablesChecked
        else:
            # seriesSelected  = self.dicomBrowser.dicomTableManager().seriesTable().tableView().selectedIndexes()
            self.loadButton.enabled = self.fileLists

    def onDirectoryImported(self):
        """The dicom browser will emit multiple directoryImported
        signals during the same operation, so we collapse them
        into a single check for compatible extensions."""
        if not hasattr(slicer.app, 'extensionsManagerModel'):
            # Slicer may not be built with extensions manager support
            return
        if not self.extensionCheckPending:
            self.extensionCheckPending = True

            def timerCallback():
                # Prompting for extension may be undesirable in custom applications.
                # DICOM/PromptForExtensions key can be used to disable this feature.
                promptForExtensionsEnabled = settingsValue(
                    'DICOM/PromptForExtensions', True, converter=toBool)
                if promptForExtensionsEnabled:
                    self.promptForExtensions()
                self.extensionCheckPending = False

            qt.QTimer.singleShot(0, timerCallback)

    def promptForExtensions(self):
        extensionsToOffer = self.checkForExtensions()
        if len(extensionsToOffer) != 0:
            if len(extensionsToOffer) == 1:
                pluralOrNot = " is"
            else:
                pluralOrNot = "s are"
            message = "The following data type%s in your database:\n\n" % pluralOrNot
            displayedTypeDescriptions = []
            for extension in extensionsToOffer:
                typeDescription = extension['typeDescription']
                if typeDescription not in displayedTypeDescriptions:
                    # only display each data type only once
                    message += '  ' + typeDescription + '\n'
                    displayedTypeDescriptions.append(typeDescription)
            message += "\nThe following extension%s not installed, but may help you work with this data:\n\n" % pluralOrNot
            displayedExtensionNames = []
            for extension in extensionsToOffer:
                extensionName = extension['name']
                if extensionName not in displayedExtensionNames:
                    # only display each extension name only once
                    message += '  ' + extensionName + '\n'
                    displayedExtensionNames.append(extensionName)
            message += "\nYou can install extensions using the Extensions Manager option from the View menu."
            slicer.util.infoDisplay(message, parent=self, windowTitle='DICOM')

    def checkForExtensions(self):
        """Check to see if there
        are any registered extensions that might be available to
        help the user work with data in the database.

        1) load extension json description
        2) load info for each series
        3) check if data matches

        then return matches

        See
        https://mantisarchive.slicer.org/view.php?id=4146
        """

        # 1 - load json
        import logging, os, json
        logging.info('Imported a DICOM directory, checking for extensions')
        modulePath = os.path.dirname(slicer.modules.dicom.path)
        extensionDescriptorPath = os.path.join(modulePath,
                                               'DICOMExtensions.json')
        try:
            with open(extensionDescriptorPath) as extensionDescriptorFP:
                extensionDescriptor = extensionDescriptorFP.read()
                dicomExtensions = json.loads(extensionDescriptor)
        except:
            logging.error('Cannot access DICOMExtensions.json file')
            return

        # 2 - get series info
        #  - iterate though metadata - should be fast even with large database
        #  - the fileValue call checks the tag cache so it's fast
        modalityTag = "0008,0060"
        sopClassUIDTag = "0008,0016"
        sopClassUIDs = set()
        modalities = set()
        for patient in slicer.dicomDatabase.patients():
            for study in slicer.dicomDatabase.studiesForPatient(patient):
                for series in slicer.dicomDatabase.seriesForStudy(study):
                    instance0 = slicer.dicomDatabase.filesForSeries(series,
                                                                    1)[0]
                    modality = slicer.dicomDatabase.fileValue(
                        instance0, modalityTag)
                    sopClassUID = slicer.dicomDatabase.fileValue(
                        instance0, sopClassUIDTag)
                    modalities.add(modality)
                    sopClassUIDs.add(sopClassUID)

        # 3 - check if data matches
        extensionsManagerModel = slicer.app.extensionsManagerModel()
        installedExtensions = extensionsManagerModel.installedExtensions
        extensionsToOffer = []
        for extension in dicomExtensions['extensions']:
            extensionName = extension['name']
            if extensionName not in installedExtensions:
                tagValues = extension['tagValues']
                if 'Modality' in tagValues:
                    for modality in tagValues['Modality']:
                        if modality in modalities:
                            extensionsToOffer.append(extension)
                if 'SOPClassUID' in tagValues:
                    for sopClassUID in tagValues['SOPClassUID']:
                        if sopClassUID in sopClassUIDs:
                            extensionsToOffer.append(extension)
        return extensionsToOffer

    def setBrowserPersistence(self, state):
        self.browserPersistent = state
        self.settings.setValue('DICOM/BrowserPersistent',
                               bool(self.browserPersistent))

    def onAdvancedViewButton(self, checked):
        self.advancedView = checked
        advancedWidgets = [
            self.loadableTableFrame, self.examineButton, self.uncheckAllButton
        ]
        for widget in advancedWidgets:
            widget.visible = self.advancedView
        self.updateButtonStates()

        self.settings.setValue('DICOM/advancedView', int(self.advancedView))

    def onHorizontalViewCheckBox(self):
        horizontal = self.horizontalViewCheckBox.checked
        self.dicomBrowser.dicomTableManager(
        ).tableOrientation = qt.Qt.Horizontal if horizontal else qt.Qt.Vertical
        self.settings.setValue('DICOM/horizontalTables', int(horizontal))

    def onSeriesSelected(self, seriesUIDList):
        self.loadableTable.setLoadables([])
        self.fileLists = self.getFileListsForRole(seriesUIDList,
                                                  "SeriesUIDList")
        self.updateButtonStates()

    def getFileListsForRole(self, uidArgument, role):
        fileLists = []
        if role == "Series":
            fileLists.append(slicer.dicomDatabase.filesForSeries(uidArgument))
        if role == "SeriesUIDList":
            for uid in uidArgument:
                uid = uid.replace("'", "")
                fileLists.append(slicer.dicomDatabase.filesForSeries(uid))
        if role == "Study":
            series = slicer.dicomDatabase.seriesForStudy(uidArgument)
            for serie in series:
                fileLists.append(slicer.dicomDatabase.filesForSeries(serie))
        if role == "Patient":
            studies = slicer.dicomDatabase.studiesForPatient(uidArgument)
            for study in studies:
                series = slicer.dicomDatabase.seriesForStudy(study)
                for serie in series:
                    fileList = slicer.dicomDatabase.filesForSeries(serie)
                    fileLists.append(fileList)
        return fileLists

    def uncheckAllLoadables(self):
        self.loadableTable.uncheckAll()

    def onLoadableTableItemChanged(self, item):
        self.updateButtonStates()

    def examineForLoading(self):
        """For selected plugins, give user the option
        of what to load"""

        (self.loadablesByPlugin,
         loadEnabled) = self.getLoadablesFromFileLists(self.fileLists)
        DICOMLib.selectHighestConfidenceLoadables(self.loadablesByPlugin)
        self.loadableTable.setLoadables(self.loadablesByPlugin)
        self.updateButtonStates()

    def getLoadablesFromFileLists(self, fileLists):
        """Take list of file lists, return loadables by plugin dictionary
        """

        loadablesByPlugin = {}
        loadEnabled = False

        # Get selected plugins from application settings
        # Settings are filled in DICOMWidget using DICOMPluginSelector
        settings = qt.QSettings()
        selectedPlugins = []
        if settings.contains('DICOM/disabledPlugins/size'):
            size = settings.beginReadArray('DICOM/disabledPlugins')
            disabledPlugins = []

            for i in range(size):
                settings.setArrayIndex(i)
                disabledPlugins.append(str(settings.allKeys()[0]))
            settings.endArray()

            for pluginClass in slicer.modules.dicomPlugins:
                if pluginClass not in disabledPlugins:
                    selectedPlugins.append(pluginClass)
        else:
            # All DICOM plugins would be enabled by default
            for pluginClass in slicer.modules.dicomPlugins:
                selectedPlugins.append(pluginClass)

        allFileCount = missingFileCount = 0
        for fileList in fileLists:
            for filePath in fileList:
                allFileCount += 1
                if not os.path.exists(filePath):
                    missingFileCount += 1

        messages = []
        if missingFileCount > 0:
            messages.append(
                "Warning: %d of %d selected files listed in the database cannot be found on disk."
                % (missingFileCount, allFileCount))

        if missingFileCount < allFileCount:
            progressDialog = slicer.util.createProgressDialog(parent=self,
                                                              value=0,
                                                              maximum=100)

            def progressCallback(progressDialog, progressLabel, progressValue):
                progressDialog.labelText = '\nChecking %s' % progressLabel
                slicer.app.processEvents()
                progressDialog.setValue(progressValue)
                slicer.app.processEvents()
                cancelled = progressDialog.wasCanceled
                return cancelled

            loadablesByPlugin, loadEnabled = DICOMLib.getLoadablesFromFileLists(
                fileLists,
                selectedPlugins,
                messages,
                lambda progressLabel, progressValue,
                progressDialog=progressDialog: progressCallback(
                    progressDialog, progressLabel, progressValue),
                self.pluginInstances)

            progressDialog.close()

        if messages:
            slicer.util.warningDisplay(
                "Warning: %s\n\nSee python console for error message." %
                ' '.join(messages),
                windowTitle="DICOM",
                parent=self)

        return loadablesByPlugin, loadEnabled

    def isFileListInCheckedLoadables(self, fileList):
        for plugin in self.loadablesByPlugin:
            for loadable in self.loadablesByPlugin[plugin]:
                if len(loadable.files) != len(fileList) or len(
                        loadable.files) == 0:
                    continue
                inputFileListCopy = copy.deepcopy(fileList)
                loadableFileListCopy = copy.deepcopy(loadable.files)
                try:
                    inputFileListCopy.sort()
                    loadableFileListCopy.sort()
                except Exception:
                    pass
                isEqual = True
                for pair in zip(inputFileListCopy, loadableFileListCopy):
                    if pair[0] != pair[1]:
                        print(f"{pair[0]} != {pair[1]}")
                        isEqual = False
                        break
                if not isEqual:
                    continue
                return True
        return False

    def patientStudySeriesDoubleClicked(self):
        if self.advancedViewButton.checkState() == 0:
            # basic mode
            self.loadCheckedLoadables()
        else:
            # advanced mode, just examine the double-clicked item, do not load
            self.examineForLoading()

    def loadCheckedLoadables(self):
        """Invoke the load method on each plugin for the loadable
        (DICOMLoadable or qSlicerDICOMLoadable) instances that are selected"""
        if self.advancedViewButton.checkState() == 0:
            self.examineForLoading()

        self.loadableTable.updateSelectedFromCheckstate()

        # TODO: add check that disables all referenced stuff to be considered?
        # get all the references from the checked loadables
        referencedFileLists = []
        for plugin in self.loadablesByPlugin:
            for loadable in self.loadablesByPlugin[plugin]:
                if hasattr(loadable, 'referencedInstanceUIDs'):
                    instanceFileList = []
                    for instance in loadable.referencedInstanceUIDs:
                        instanceFile = slicer.dicomDatabase.fileForInstance(
                            instance)
                        if instanceFile != '':
                            instanceFileList.append(instanceFile)
                    if len(instanceFileList
                           ) and not self.isFileListInCheckedLoadables(
                               instanceFileList):
                        referencedFileLists.append(instanceFileList)

        # if applicable, find all loadables from the file lists
        loadEnabled = False
        if len(referencedFileLists):
            (self.referencedLoadables,
             loadEnabled) = self.getLoadablesFromFileLists(referencedFileLists)

        automaticallyLoadReferences = int(
            slicer.util.settingsValue('DICOM/automaticallyLoadReferences',
                                      qt.QMessageBox.InvalidRole))
        if slicer.app.commandOptions().testingEnabled:
            automaticallyLoadReferences = qt.QMessageBox.No
        if loadEnabled and automaticallyLoadReferences == qt.QMessageBox.InvalidRole:
            self.showReferenceDialogAndProceed()
        elif loadEnabled and automaticallyLoadReferences == qt.QMessageBox.Yes:
            self.addReferencesAndProceed()
        else:
            self.proceedWithReferencedLoadablesSelection()

        return

    def showReferenceDialogAndProceed(self):
        referencesDialog = DICOMReferencesDialog(
            self, loadables=self.referencedLoadables)
        answer = referencesDialog.exec_()
        if referencesDialog.rememberChoiceAndStopAskingCheckbox.checked is True:
            if answer == qt.QMessageBox.Yes:
                qt.QSettings().setValue('DICOM/automaticallyLoadReferences',
                                        qt.QMessageBox.Yes)
            if answer == qt.QMessageBox.No:
                qt.QSettings().setValue('DICOM/automaticallyLoadReferences',
                                        qt.QMessageBox.No)
        if answer == qt.QMessageBox.Yes:
            # each check box corresponds to a referenced loadable that was selected by examine;
            # if the user confirmed that reference should be loaded, add it to the self.loadablesByPlugin dictionary
            for plugin in self.referencedLoadables:
                for loadable in [
                        loadable_item
                        for loadable_item in self.referencedLoadables[plugin]
                        if loadable_item.selected
                ]:
                    if referencesDialog.checkboxes[loadable].checked:
                        self.loadablesByPlugin[plugin].append(loadable)
                self.loadablesByPlugin[plugin] = list(
                    set(self.loadablesByPlugin[plugin]))
            self.proceedWithReferencedLoadablesSelection()
        elif answer == qt.QMessageBox.No:
            self.proceedWithReferencedLoadablesSelection()

    def addReferencesAndProceed(self):
        for plugin in self.referencedLoadables:
            for loadable in [
                    loadable_item
                    for loadable_item in self.referencedLoadables[plugin]
                    if loadable_item.selected
            ]:
                self.loadablesByPlugin[plugin].append(loadable)
            self.loadablesByPlugin[plugin] = list(
                set(self.loadablesByPlugin[plugin]))
        self.proceedWithReferencedLoadablesSelection()

    def proceedWithReferencedLoadablesSelection(self):
        if not self.warnUserIfLoadableWarningsAndProceed():
            return

        progressDialog = slicer.util.createProgressDialog(parent=self,
                                                          value=0,
                                                          maximum=100)

        def progressCallback(progressDialog, progressLabel, progressValue):
            progressDialog.labelText = '\nLoading %s' % progressLabel
            slicer.app.processEvents()
            progressDialog.setValue(progressValue)
            slicer.app.processEvents()
            cancelled = progressDialog.wasCanceled
            return cancelled

        qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)

        messages = []
        loadedNodeIDs = DICOMLib.loadLoadables(
            self.loadablesByPlugin,
            messages,
            lambda progressLabel, progressValue, progressDialog=progressDialog:
            progressCallback(progressDialog, progressLabel, progressValue))

        loadedFileParameters = {}
        loadedFileParameters['nodeIDs'] = loadedNodeIDs
        slicer.app.ioManager().emitNewFileLoaded(loadedFileParameters)

        qt.QApplication.restoreOverrideCursor()

        progressDialog.close()

        if messages:
            slicer.util.warningDisplay('\n'.join(messages),
                                       windowTitle='DICOM loading')

        self.onLoadingFinished()

    def warnUserIfLoadableWarningsAndProceed(self):
        warningsInSelectedLoadables = False
        details = ""
        for plugin in self.loadablesByPlugin:
            for loadable in self.loadablesByPlugin[plugin]:
                if loadable.selected and loadable.warning != "":
                    warningsInSelectedLoadables = True
                    logging.warning('Warning in DICOM plugin ' +
                                    plugin.loadType +
                                    ' when examining loadable ' +
                                    loadable.name + ': ' + loadable.warning)
                    details += loadable.name + " [" + plugin.loadType + "]: " + loadable.warning + "\n"
        if warningsInSelectedLoadables:
            warning = "Warnings detected during load.  Examine data in Advanced mode for details.  Load anyway?"
            if not slicer.util.confirmOkCancelDisplay(
                    warning, parent=self, detailedText=details):
                return False
        return True

    def onLoadingFinished(self):
        if not self.browserPersistent:
            self.close()