Example #1
0
class ScorePartsWidget(QSplitter):
    def __init__(self, parent):
        super(ScorePartsWidget, self).__init__(parent)
        
        self.typesLabel = QLabel()
        self.typesView = QTreeView(
            selectionMode=QTreeView.ExtendedSelection,
            selectionBehavior=QTreeView.SelectRows,
            animated=True,
            headerHidden=True)
        self.scoreLabel = QLabel()
        self.scoreView = widgets.treewidget.TreeWidget(
            selectionMode=QTreeView.ExtendedSelection,
            selectionBehavior=QTreeView.SelectRows,
            headerHidden=True,
            animated=True,
            dragDropMode=QTreeView.InternalMove)
        self.addButton = QPushButton(icon = icons.get("list-add"))
        self.removeButton = QPushButton(icon = icons.get("list-remove"))
        self.upButton = QToolButton(icon = icons.get("go-up"))
        self.downButton = QToolButton(icon = icons.get("go-down"))
        self.partSettings = QStackedWidget()
        
        w = QWidget()
        self.addWidget(w)
        layout = QVBoxLayout(spacing=0)
        w.setLayout(layout)
        
        layout.addWidget(self.typesLabel)
        layout.addWidget(self.typesView)
        layout.addWidget(self.addButton)
        
        w = QWidget()
        self.addWidget(w)
        layout = QVBoxLayout(spacing=0)
        w.setLayout(layout)
        
        layout.addWidget(self.scoreLabel)
        layout.addWidget(self.scoreView)
        
        box = QHBoxLayout(spacing=0)
        layout.addLayout(box)
        
        box.addWidget(self.removeButton)
        box.addWidget(self.upButton)
        box.addWidget(self.downButton)
        
        self.addWidget(self.partSettings)

        self.typesView.setModel(parts.model())
        app.translateUI(self)
        
        # signal connections
        self.addButton.clicked.connect(self.slotAddButtonClicked)
        self.removeButton.clicked.connect(self.slotRemoveButtonClicked)
        self.typesView.doubleClicked.connect(self.slotDoubleClicked)
        self.scoreView.currentItemChanged.connect(self.slotCurrentItemChanged)
        self.upButton.clicked.connect(self.scoreView.moveSelectedChildrenUp)
        self.downButton.clicked.connect(self.scoreView.moveSelectedChildrenDown)
        
    def translateUI(self):
        bold = "<b>{0}</b>".format
        self.typesLabel.setText(bold(_("Available parts:")))
        self.scoreLabel.setText(bold(_("Score:")))
        self.addButton.setText(_("&Add"))
        self.removeButton.setText(_("&Remove"))
        self.upButton.setToolTip(_("Move up"))
        self.downButton.setToolTip(_("Move down"))

    def slotDoubleClicked(self, index):
        self.addParts([index])
        
    def slotAddButtonClicked(self):
        self.addParts(self.typesView.selectedIndexes())

    def addParts(self, indexes):
        """Adds the parts for the given indexes."""
        # add to current if that is a container type
        currentItem = self.scoreView.currentItem()
        for index in indexes:
            category = index.internalPointer()
            if category:
                part = category.items[index.row()]
                box = QGroupBox(self.partSettings)
                self.partSettings.addWidget(box)
                # determine the parent: current or root
                if currentItem and issubclass(part, currentItem.part.accepts()):
                    parent = currentItem
                    parent.setExpanded(True)
                else:
                    parent = self.scoreView
                item = PartItem(parent, part, box)
    
    def slotCurrentItemChanged(self, item):
        if isinstance(item, PartItem):
            self.partSettings.setCurrentWidget(item.box)
    
    def slotRemoveButtonClicked(self):
        self.scoreView.removeSelectedItems()
       
    def clear(self):
        """Called when the user clicks the clear button on this page."""
        self.scoreView.clear()

    def rootPartItem(self):
        """Returns the invisibleRootItem(), representing the tree of parts in the score view."""
        return self.scoreView.invisibleRootItem()
Example #2
0
class SimulationPanel(QWidget):

    def __init__(self):
        QWidget.__init__(self)

        layout = QVBoxLayout()

        simulation_mode_layout = QHBoxLayout()
        simulation_mode_layout.addSpacing(10)
        simulation_mode_model = SimulationModeModel()
        simulation_mode_model.observable().attach(SimulationModeModel.CURRENT_CHOICE_CHANGED_EVENT, self.toggleSimulationMode)
        simulation_mode_combo = ComboChoice(simulation_mode_model, "Simulation mode", "run/simulation_mode")
        simulation_mode_layout.addWidget(QLabel(simulation_mode_combo.getLabel()), 0, Qt.AlignVCenter)
        simulation_mode_layout.addWidget(simulation_mode_combo, 0, Qt.AlignVCenter)

        # simulation_mode_layout.addStretch()
        simulation_mode_layout.addSpacing(20)

        self.run_button = QToolButton()
        self.run_button.setIconSize(QSize(32, 32))
        self.run_button.setText("Start Simulation")
        self.run_button.setIcon(util.resourceIcon("ide/gear_in_play"))
        self.run_button.clicked.connect(self.runSimulation)
        self.run_button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
        HelpedWidget.addHelpToWidget(self.run_button, "run/start_simulation")

        simulation_mode_layout.addWidget(self.run_button)
        simulation_mode_layout.addStretch(1)

        layout.addSpacing(5)
        layout.addLayout(simulation_mode_layout)
        layout.addSpacing(10)

        self.simulation_stack = QStackedWidget()
        self.simulation_stack.setLineWidth(1)
        self.simulation_stack.setFrameStyle(QFrame.StyledPanel)

        layout.addWidget(self.simulation_stack)

        self.simulation_widgets = {}

        self.addSimulationConfigPanel(EnsembleExperimentPanel())
        self.addSimulationConfigPanel(EnsembleSmootherPanel())
        self.addSimulationConfigPanel(MultipleDataAssimilationPanel())
        self.addSimulationConfigPanel(IteratedEnsembleSmootherPanel())

        self.setLayout(layout)


    def addSimulationConfigPanel(self, panel):
        assert isinstance(panel, SimulationConfigPanel)

        panel.toggleAdvancedOptions(False)
        self.simulation_stack.addWidget(panel)
        self.simulation_widgets[panel.getSimulationModel()] = panel

        panel.simulationConfigurationChanged.connect(self.validationStatusChanged)


    def getActions(self):
        return []


    def toggleAdvancedMode(self, show_advanced):
        for panel in self.simulation_widgets.values():
            panel.toggleAdvancedOptions(show_advanced)


    def getCurrentSimulationMode(self):
        return SimulationModeModel().getCurrentChoice()


    def runSimulation(self):
        case_name = CaseSelectorModel().getCurrentChoice()
        message = "Are you sure you want to use case '%s' for initialization of the initial ensemble when running the simulations?" % case_name
        start_simulations = QMessageBox.question(self, "Start simulations?", message, QMessageBox.Yes | QMessageBox.No )

        if start_simulations == QMessageBox.Yes:
            run_model = self.getCurrentSimulationMode()

            dialog = RunDialog(run_model)
            dialog.startSimulation()
            dialog.exec_()

            CaseList().externalModificationNotification() # simulations may have added new cases.


    def toggleSimulationMode(self):
        widget = self.simulation_widgets[self.getCurrentSimulationMode()]
        self.simulation_stack.setCurrentWidget(widget)
        self.validationStatusChanged()


    def validationStatusChanged(self):
        widget = self.simulation_widgets[self.getCurrentSimulationMode()]
        self.run_button.setEnabled(widget.isConfigurationValid())
Example #3
0
class SimulationPanel(QWidget):
    def __init__(self):
        QWidget.__init__(self)

        layout = QVBoxLayout()

        self._simulation_mode_combo = QComboBox()
        addHelpToWidget(self._simulation_mode_combo, "run/simulation_mode")

        self._simulation_mode_combo.currentIndexChanged.connect(
            self.toggleSimulationMode)

        simulation_mode_layout = QHBoxLayout()
        simulation_mode_layout.addSpacing(10)
        simulation_mode_layout.addWidget(QLabel("Simulation mode:"), 0,
                                         Qt.AlignVCenter)
        simulation_mode_layout.addWidget(self._simulation_mode_combo, 0,
                                         Qt.AlignVCenter)

        simulation_mode_layout.addSpacing(20)

        self.run_button = QToolButton()
        self.run_button.setIconSize(QSize(32, 32))
        self.run_button.setText("Start Simulation")
        self.run_button.setIcon(resourceIcon("ide/gear_in_play"))
        self.run_button.clicked.connect(self.runSimulation)
        self.run_button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
        addHelpToWidget(self.run_button, "run/start_simulation")

        simulation_mode_layout.addWidget(self.run_button)
        simulation_mode_layout.addStretch(1)

        layout.addSpacing(5)
        layout.addLayout(simulation_mode_layout)
        layout.addSpacing(10)

        self._simulation_stack = QStackedWidget()
        self._simulation_stack.setLineWidth(1)
        self._simulation_stack.setFrameStyle(QFrame.StyledPanel)

        layout.addWidget(self._simulation_stack)

        self._simulation_widgets = {}
        """ :type: dict[BaseRunModel,SimulationConfigPanel]"""

        self.addSimulationConfigPanel(EnsembleExperimentPanel())
        self.addSimulationConfigPanel(EnsembleSmootherPanel())
        self.addSimulationConfigPanel(IteratedEnsembleSmootherPanel())
        self.addSimulationConfigPanel(MultipleDataAssimilationPanel())

        self.setLayout(layout)

    def addSimulationConfigPanel(self, panel):
        assert isinstance(panel, SimulationConfigPanel)

        panel.toggleAdvancedOptions(False)
        self._simulation_stack.addWidget(panel)

        simulation_model = panel.getSimulationModel()

        self._simulation_widgets[simulation_model] = panel
        self._simulation_mode_combo.addItem(str(simulation_model),
                                            simulation_model)
        panel.simulationConfigurationChanged.connect(
            self.validationStatusChanged)

    def getActions(self):
        return []

    def toggleAdvancedMode(self, show_advanced):
        for panel in self._simulation_widgets.values():
            panel.toggleAdvancedOptions(show_advanced)

    def getCurrentSimulationModel(self):
        data = self._simulation_mode_combo.itemData(
            self._simulation_mode_combo.currentIndex(), Qt.UserRole)
        return data.toPyObject()

    def getSimulationArguments(self):
        """ @rtype: dict[str,object]"""
        simulation_widget = self._simulation_widgets[
            self.getCurrentSimulationModel()]
        return simulation_widget.getSimulationArguments()

    def runSimulation(self):
        case_name = getCurrentCaseName()
        message = "Are you sure you want to use case '%s' for initialization of the initial ensemble when running the simulations?" % case_name
        start_simulations = QMessageBox.question(
            self, "Start simulations?", message,
            QMessageBox.Yes | QMessageBox.No)

        if start_simulations == QMessageBox.Yes:
            run_model = self.getCurrentSimulationModel()
            arguments = self.getSimulationArguments()
            dialog = RunDialog(run_model, arguments, self)
            dialog.startSimulation()
            dialog.exec_()

            ERT.emitErtChange()  # simulations may have added new cases.

    def toggleSimulationMode(self):
        widget = self._simulation_widgets[self.getCurrentSimulationModel()]
        self._simulation_stack.setCurrentWidget(widget)
        self.validationStatusChanged()

    def validationStatusChanged(self):
        widget = self._simulation_widgets[self.getCurrentSimulationModel()]
        self.run_button.setEnabled(widget.isConfigurationValid())
class DataExportGui(QWidget):
    """
    Manages all GUI elements in the data selection applet.
    This class itself is the central widget and also owns/manages the applet drawer widgets.
    """
    ###########################################
    ### AppletGuiInterface Concrete Methods ###
    ###########################################
    
    def centralWidget( self ):
        return self

    def appletDrawer(self):
        return self.drawer

    def menus( self ):
        return []

    def viewerControlWidget(self):
        return self._viewerControlWidgetStack

    def setImageIndex(self, index):
        pass

    def stopAndCleanUp(self):
        for editor in self.layerViewerGuis.values():
            self.viewerStack.removeWidget( editor )
            editor.stopAndCleanUp()
        self.layerViewerGuis.clear()

    def imageLaneAdded(self, laneIndex):
        pass

    def imageLaneRemoved(self, laneIndex, finalLength):
        pass

    def allowLaneSelectionChange(self):
        return False

    ###########################################
    ###########################################
    
    def __init__(self, parentApplet, topLevelOperator):
        super(DataExportGui, self).__init__()

        self.drawer = None
        self.topLevelOperator = topLevelOperator

        self.threadRouter = ThreadRouter(self)
        self._thunkEventHandler = ThunkEventHandler(self)
        
        self._initAppletDrawerUic()
        self.initCentralUic()
        self.initViewerControls()
        
        self.parentApplet = parentApplet
        self.progressSignal = parentApplet.progressSignal
        
        @threadRoutedWithRouter(self.threadRouter)
        def handleNewDataset( multislot, index ):
            # Make room in the GUI table
            self.batchOutputTableWidget.insertRow( index )
            
            # Update the table row data when this slot has new data
            # We can't bind in the row here because the row may change in the meantime.
            multislot[index].notifyReady( bind( self.updateTableForSlot ) )
            if multislot[index].ready():
                self.updateTableForSlot( multislot[index] )

            multislot[index].notifyUnready( self._updateExportButtons )
            multislot[index].notifyReady( self._updateExportButtons )

        self.topLevelOperator.ExportPath.notifyInserted( bind( handleNewDataset ) )
        
        # For each dataset that already exists, update the GUI
        for i, subslot in enumerate(self.topLevelOperator.ExportPath):
            handleNewDataset( self.topLevelOperator.ExportPath, i )
            if subslot.ready():
                self.updateTableForSlot(subslot)

        @threadRoutedWithRouter(self.threadRouter)
        def handleLaneRemoved( multislot, index, finalLength ):
            if self.batchOutputTableWidget.rowCount() <= finalLength:
                return

            # Remove the row we don't need any more
            self.batchOutputTableWidget.removeRow( index )

            # Remove the viewer for this dataset
            imageMultiSlot = self.topLevelOperator.Inputs[index]
            if imageMultiSlot in self.layerViewerGuis.keys():
                layerViewerGui = self.layerViewerGuis[imageMultiSlot]
                self.viewerStack.removeWidget( layerViewerGui )
                self._viewerControlWidgetStack.removeWidget( layerViewerGui.viewerControlWidget() )
                layerViewerGui.stopAndCleanUp()

        self.topLevelOperator.Inputs.notifyRemove( bind( handleLaneRemoved ) )
    
    def _initAppletDrawerUic(self, drawerPath=None):
        """
        Load the ui file for the applet drawer, which we own.
        """
        if drawerPath is None:
            localDir = os.path.split(__file__)[0]
            drawerPath = os.path.join( localDir, "dataExportDrawer.ui")
        self.drawer = uic.loadUi(drawerPath)

        self.drawer.settingsButton.clicked.connect( self._chooseSettings )
        self.drawer.exportAllButton.clicked.connect( self.exportAllResults )
        self.drawer.exportAllButton.setIcon( QIcon(ilastikIcons.Save) )
        self.drawer.deleteAllButton.clicked.connect( self.deleteAllResults )
        self.drawer.deleteAllButton.setIcon( QIcon(ilastikIcons.Clear) )
        
        @threadRoutedWithRouter(self.threadRouter)
        def _handleNewSelectionNames( *args ):
            input_names = self.topLevelOperator.SelectionNames.value
            self.drawer.inputSelectionCombo.addItems( input_names )
        self.topLevelOperator.SelectionNames.notifyDirty( _handleNewSelectionNames )
        _handleNewSelectionNames()

        def _handleInputComboSelectionChanged( index ):
            assert index < len(self.topLevelOperator.SelectionNames.value)
            if self.drawer.inputSelectionCombo.currentText() == self.topLevelOperator.TableOnlyName.value:
                self.topLevelOperator.TableOnly.setValue(True)
            else:
                self.topLevelOperator.TableOnly.setValue(False)
                self.topLevelOperator.InputSelection.setValue( index )
        self.drawer.inputSelectionCombo.currentIndexChanged.connect( _handleInputComboSelectionChanged )

    def initCentralUic(self):
        """
        Load the GUI from the ui file into this class and connect it with event handlers.
        """
        # Load the ui file into this class (find it in our own directory)
        localDir = os.path.split(__file__)[0]
        uic.loadUi(localDir+"/dataExport.ui", self)

        self.batchOutputTableWidget.resizeRowsToContents()
        self.batchOutputTableWidget.resizeColumnsToContents()
        self.batchOutputTableWidget.setAlternatingRowColors(True)
        self.batchOutputTableWidget.setShowGrid(False)
        self.batchOutputTableWidget.horizontalHeader().setResizeMode(0, QHeaderView.Interactive)
        
        self.batchOutputTableWidget.horizontalHeader().resizeSection(Column.Dataset, 200)
        self.batchOutputTableWidget.horizontalHeader().resizeSection(Column.ExportLocation, 250)
        self.batchOutputTableWidget.horizontalHeader().resizeSection(Column.Action, 100)

        self.batchOutputTableWidget.verticalHeader().hide()

        # Set up handlers
        self.batchOutputTableWidget.itemSelectionChanged.connect(self.handleTableSelectionChange)

        # Set up the viewer area
        self.initViewerStack()
        self.splitter.setSizes([150, 850])
    
    def initViewerStack(self):
        self.layerViewerGuis = {}
        self.viewerStack.addWidget( QWidget() )
        
    def initViewerControls(self):
        self._viewerControlWidgetStack = QStackedWidget(parent=self)

    def showEvent(self, event):
        super( DataExportGui, self ).showEvent(event)
        self.showSelectedDataset()
    
    def hideEvent(self, event):
        super( DataExportGui, self ).hideEvent(event)
        
        # Make sure all 'on disk' layers are discarded so we aren't using those files any more.
        for opLaneView in self.topLevelOperator:
            opLaneView.cleanupOnDiskView()    

    def _chooseSettings(self):
        opExportModelOp, opSubRegion = get_model_op( self.topLevelOperator )
        if opExportModelOp is None:
            QMessageBox.information( self, 
                                     "Image not ready for export", 
                                     "Export isn't possible yet: No images are ready for export.  "
                                     "Please configure upstream pipeline with valid settings, "
                                     "check that images were specified in the (batch) input applet and try again." )
            return
        
        settingsDlg = DataExportOptionsDlg(self, opExportModelOp)
        if settingsDlg.exec_() == DataExportOptionsDlg.Accepted:
            # Copy the settings from our 'model op' into the real op
            setting_slots = [ opExportModelOp.RegionStart,
                              opExportModelOp.RegionStop,
                              opExportModelOp.InputMin,
                              opExportModelOp.InputMax,
                              opExportModelOp.ExportMin,
                              opExportModelOp.ExportMax,
                              opExportModelOp.ExportDtype,
                              opExportModelOp.OutputAxisOrder,
                              opExportModelOp.OutputFilenameFormat,
                              opExportModelOp.OutputInternalPath,
                              opExportModelOp.OutputFormat ]

            # Disconnect the special 'transaction' slot to prevent these 
            #  settings from triggering many calls to setupOutputs.
            self.topLevelOperator.TransactionSlot.disconnect()

            for model_slot in setting_slots:
                real_inslot = getattr(self.topLevelOperator, model_slot.name)
                if model_slot.ready():
                    real_inslot.setValue( model_slot.value )
                else:
                    real_inslot.disconnect()

            # Re-connect the 'transaction' slot to apply all settings at once.
            self.topLevelOperator.TransactionSlot.setValue(True)

            # Discard the temporary model op
            opExportModelOp.cleanUp()
            opSubRegion.cleanUp()

            # Update the gui with the new export paths            
            for index, slot in enumerate(self.topLevelOperator.ExportPath):
                self.updateTableForSlot(slot)

    def getSlotIndex(self, multislot, subslot ):
        # Which index is this slot?
        for index, slot in enumerate(multislot):
            if slot == subslot:
                return index
        return -1

    @threadRouted
    def updateTableForSlot(self, slot):
        """
        Update the table row that corresponds to the given slot of the top-level operator (could be either input slot)
        """
        row = self.getSlotIndex( self.topLevelOperator.ExportPath, slot )
        assert row != -1, "Unknown input slot!"

        if not self.topLevelOperator.ExportPath[row].ready() or\
           not self.topLevelOperator.RawDatasetInfo[row].ready():
            return
        
        try:
            nickname = self.topLevelOperator.RawDatasetInfo[row].value.nickname
            exportPath = self.topLevelOperator.ExportPath[row].value
        except Slot.SlotNotReadyError:
            # Sadly, it is possible to get here even though we checked for .ready() immediately beforehand.
            # That's because the graph has a diamond-shaped DAG of connections, but the graph has no transaction mechanism
            # (It's therefore possible for RawDatasetInfo[row] to be ready() even though it's upstream partner is NOT ready.
            return
                
        self.batchOutputTableWidget.setItem( row, Column.Dataset, QTableWidgetItem( decode_to_qstring(nickname, 'utf-8') ) )
        self.batchOutputTableWidget.setItem( row, Column.ExportLocation, QTableWidgetItem( decode_to_qstring(exportPath) ) )

        exportNowButton = QPushButton("Export")
        exportNowButton.setToolTip("Generate individual batch output dataset.")
        exportNowButton.clicked.connect( bind(self.exportResultsForSlot, self.topLevelOperator[row] ) )
        self.batchOutputTableWidget.setCellWidget( row, Column.Action, exportNowButton )

        # Select a row if there isn't one already selected.
        selectedRanges = self.batchOutputTableWidget.selectedRanges()
        if len(selectedRanges) == 0:
            self.batchOutputTableWidget.selectRow(0)

    def setEnabledIfAlive(self, widget, enable):
        if not sip.isdeleted(widget):
            widget.setEnabled(enable)
    def _updateExportButtons(self, *args):
        """Called when at least one dataset became 'unready', so we have to disable the export button."""
        all_ready = True
        # Enable/disable the appropriate export buttons in the table.
        # Use ThunkEvents to ensure that this happens in the Gui thread.        
        for row, slot in enumerate( self.topLevelOperator.ImageToExport ):
            all_ready &= slot.ready()
            export_button = self.batchOutputTableWidget.cellWidget( row, Column.Action )
            if export_button is not None:
                executable_event = ThunkEvent( partial(self.setEnabledIfAlive, export_button, slot.ready()) )
                QApplication.instance().postEvent( self, executable_event )

        # Disable the "Export all" button unless all slots are ready.
        executable_event = ThunkEvent( partial(self.setEnabledIfAlive, self.drawer.exportAllButton, all_ready) )
        QApplication.instance().postEvent( self, executable_event )

    def handleTableSelectionChange(self):
        """
        Any time the user selects a new item, select the whole row.
        """
        self.selectEntireRow()
        self.showSelectedDataset()
    
    def selectEntireRow(self):
        # FIXME: There is a better way to do this...
        # Figure out which row is selected
        selectedItemRows = set()
        selectedRanges = self.batchOutputTableWidget.selectedRanges()
        for rng in selectedRanges:
            for row in range(rng.topRow(), rng.bottomRow()+1):
                selectedItemRows.add(row)
        
        # Disconnect from selection change notifications while we do this
        self.batchOutputTableWidget.itemSelectionChanged.disconnect(self.handleTableSelectionChange)
        for row in selectedItemRows:
            self.batchOutputTableWidget.selectRow(row)

        # Reconnect now that we're finished
        self.batchOutputTableWidget.itemSelectionChanged.connect(self.handleTableSelectionChange)

    def exportSlots(self, laneViewList ):
        try:
            # Set the busy flag so the workflow knows not to allow 
            #  upstream changes or shell changes while we're exporting
            self.parentApplet.busy = True
            self.parentApplet.appletStateUpdateRequested.emit()
            
            # Disable our own gui
            QApplication.instance().postEvent( self, ThunkEvent( partial(self.setEnabledIfAlive, self.drawer, False) ) )
            QApplication.instance().postEvent( self, ThunkEvent( partial(self.setEnabledIfAlive, self, False) ) )
            
            # Start with 1% so the progress bar shows up
            self.progressSignal.emit(0)
            self.progressSignal.emit(1)

            def signalFileProgress(slotIndex, percent):
                self.progressSignal.emit( (100*slotIndex + percent) / len(laneViewList) ) 

            # Client hook
            self.parentApplet.prepare_for_entire_export()

            for i, opLaneView in enumerate(laneViewList):
                lane_index = self.topLevelOperator.innerOperators.index(opLaneView)
                logger.debug("Exporting result {}".format(i))

                # If the operator provides a progress signal, use it.
                slotProgressSignal = opLaneView.progressSignal
                slotProgressSignal.subscribe( partial(signalFileProgress, i) )

                try:
                    # Client hook
                    self.parentApplet.prepare_lane_for_export(lane_index)

                    # Export the image
                    opLaneView.run_export()
                    
                    # Client hook
                    self.parentApplet.post_process_lane_export(lane_index)
                except Exception as ex:
                    if opLaneView.ExportPath.ready():
                        msg = "Failed to generate export file: \n"
                        msg += opLaneView.ExportPath.value
                        msg += "\n{}".format( ex )
                    else:
                        msg = "Failed to generate export file."
                        msg += "\n{}".format( ex )
                    log_exception( logger, msg )
                    self.showExportError(msg)

                # We're finished with this file. 
                self.progressSignal.emit( 100*(i+1)/float(len(laneViewList)) )

            # Client hook
            self.parentApplet.post_process_entire_export()
                
            # Ensure the shell knows we're really done.
            self.progressSignal.emit(100)
        except:
            # Cancel our progress.
            self.progressSignal.emit(0, True)
            raise
        finally:
            # We're not busy any more.  Tell the workflow.
            self.parentApplet.busy = False
            self.parentApplet.appletStateUpdateRequested.emit()
            
            # Re-enable our own gui
            QApplication.instance().postEvent( self, ThunkEvent( partial(self.setEnabledIfAlive, self.drawer, True) ) )
            QApplication.instance().postEvent( self, ThunkEvent( partial(self.setEnabledIfAlive, self, True) ) )


    def postProcessLane(self, lane_index):
        """
        Called immediately after the result for each lane is exported.
        Can be overridden by subclasses for post-processing purposes.
        """
        pass
        
    @threadRouted
    def showExportError(self, msg):
        QMessageBox.critical(self, "Failed to export", msg )

    def exportResultsForSlot(self, opLane):
        # Make sure all 'on disk' layers are discarded so we aren't using those files any more.
        for opLaneView in self.topLevelOperator:
            opLaneView.cleanupOnDiskView()
        
        # Do this in a separate thread so the UI remains responsive
        exportThread = threading.Thread(target=bind(self.exportSlots, [opLane]), name="DataExportThread")
        exportThread.start()
    
    def exportAllResults(self):
        # Make sure all 'on disk' layers are discarded so we aren't using those files any more.
        for opLaneView in self.topLevelOperator:
            opLaneView.cleanupOnDiskView()

        # Do this in a separate thread so the UI remains responsive
        exportThread = threading.Thread(target=bind(self.exportSlots, self.topLevelOperator), name="DataExportThread")
        exportThread.start()

    def deleteAllResults(self):
        for innerOp in self.topLevelOperator:
            operatorView = innerOp
            operatorView.cleanupOnDiskView()
            pathComp = PathComponents(operatorView.ExportPath.value, operatorView.WorkingDirectory.value)
            if os.path.exists(pathComp.externalPath):
                os.remove(pathComp.externalPath)
            operatorView.setupOnDiskView()
            # we need to toggle the dirts state in order to enforce a frech dirty signal
            operatorView.Dirty.setValue( False )
            operatorView.Dirty.setValue( True )

    def showSelectedDataset(self):
        """
        Show the exported file in the viewer
        """
        # Get the selected row and corresponding slot value
        selectedRanges = self.batchOutputTableWidget.selectedRanges()
        if len(selectedRanges) == 0:
            return
        row = selectedRanges[0].topRow()
        
        # Hide all layers that come from the disk.
        for opLaneView in self.topLevelOperator:
            opLaneView.cleanupOnDiskView()

        # Activate the 'on disk' layers for this lane (if possible)
        opLane = self.topLevelOperator.getLane(row)
        opLane.setupOnDiskView()
        
        # Create if necessary
        imageMultiSlot = self.topLevelOperator.Inputs[row]
        if imageMultiSlot not in self.layerViewerGuis.keys():
            layerViewer = self.createLayerViewer(opLane)

            # Maximize the x-y view by default.
            layerViewer.volumeEditorWidget.quadview.ensureMaximized(2)
            
            self.layerViewerGuis[imageMultiSlot] = layerViewer
            self.viewerStack.addWidget( layerViewer )
            self._viewerControlWidgetStack.addWidget( layerViewer.viewerControlWidget() )

        # Show the right one
        layerViewer = self.layerViewerGuis[imageMultiSlot]
        self.viewerStack.setCurrentWidget( layerViewer )
        self._viewerControlWidgetStack.setCurrentWidget( layerViewer.viewerControlWidget() )


    def createLayerViewer(self, opLane):
        """
        This method provides an instance of LayerViewerGui for the given data lane.
        If this GUI class is subclassed, this method can be reimplemented to provide 
        custom layer types for the exported layers.
        """
        return DataExportLayerViewerGui(self.parentApplet, opLane)
Example #5
0
class DataSelectionGui(QWidget):
    """
    Manages all GUI elements in the data selection applet.
    This class itself is the central widget and also owns/manages the applet drawer widgets.
    """

    ###########################################
    ### AppletGuiInterface Concrete Methods ###
    ###########################################

    def centralWidget( self ):
        return self

    def appletDrawer( self ):
        return self._drawer

    def menus( self ):
        return []

    def viewerControlWidget(self):
        return self._viewerControlWidgetStack

    def setImageIndex(self, imageIndex):
        if imageIndex is not None:
            self.laneSummaryTableView.selectRow(imageIndex)
            for detailWidget in self._detailViewerWidgets:
                detailWidget.selectRow(imageIndex)

    def stopAndCleanUp(self):
        self._cleaning_up = True
        for editor in self.volumeEditors.values():
            self.viewerStack.removeWidget( editor )
            self._viewerControlWidgetStack.removeWidget( editor.viewerControlWidget() )
            editor.stopAndCleanUp()
        self.volumeEditors.clear()

    def imageLaneAdded(self, laneIndex):
        if len(self.laneSummaryTableView.selectedIndexes()) == 0:
            self.laneSummaryTableView.selectRow(laneIndex)
        
        # We don't have any real work to do because this gui initiated the lane addition in the first place
        if self.guiMode != GuiMode.Batch:
            if(len(self.topLevelOperator.DatasetGroup) != laneIndex+1):
                import warnings
                warnings.warn("DataSelectionGui.imageLaneAdded(): length of dataset multislot out of sync with laneindex [%s != %s + 1]" % (len(self.topLevelOperator.DatasetGroup), laneIndex))

    def imageLaneRemoved(self, laneIndex, finalLength):
        # There's nothing to do here because the GUI already 
        #  handles operator resizes via slot callbacks.
        pass

    def allowLaneSelectionChange(self):
        return False

    ###########################################
    ###########################################

    class UserCancelledError(Exception):
        # This exception type is raised when the user cancels the 
        #  addition of dataset files in the middle of the process somewhere.
        # It isn't an error -- it's used for control flow.
        pass

    def __init__(self, parentApplet, dataSelectionOperator, serializer, instructionText, guiMode=GuiMode.Normal, max_lanes=None, show_axis_details=False):
        """
        Constructor.
        
        :param dataSelectionOperator: The top-level operator.  Must be of type :py:class:`OpMultiLaneDataSelectionGroup`.
        :param serializer: The applet's serializer.  Must be of type :py:class:`DataSelectionSerializer`
        :param instructionText: A string to display in the applet drawer.
        :param guiMode: Either ``GuiMode.Normal`` or ``GuiMode.Batch``.  Currently, there is no difference between normal and batch mode.
        :param max_lanes: The maximum number of lanes that the user is permitted to add to this workflow.  If ``None``, there is no maximum.
        """
        super(DataSelectionGui, self).__init__()
        self._cleaning_up = False
        self.parentApplet = parentApplet
        self._max_lanes = max_lanes
        self._default_h5_volumes = {}
        self.show_axis_details = show_axis_details

        self._viewerControls = QWidget()
        self.topLevelOperator = dataSelectionOperator
        self.guiMode = guiMode
        self.serializer = serializer
        self.threadRouter = ThreadRouter(self)

        self._initCentralUic()
        self._initAppletDrawerUic(instructionText)
        
        self._viewerControlWidgetStack = QStackedWidget(self)

        def handleImageRemove(multislot, index, finalLength):
            # Remove the viewer for this dataset
            datasetSlot = self.topLevelOperator.DatasetGroup[index]
            if datasetSlot in self.volumeEditors.keys():
                editor = self.volumeEditors[datasetSlot]
                self.viewerStack.removeWidget( editor )
                self._viewerControlWidgetStack.removeWidget( editor.viewerControlWidget() )
                editor.stopAndCleanUp()

        self.topLevelOperator.DatasetGroup.notifyRemove( bind( handleImageRemove ) )
        
        opWorkflow = self.topLevelOperator.parent
        assert hasattr(opWorkflow.shell, 'onSaveProjectActionTriggered'), \
            "This class uses the IlastikShell.onSaveProjectActionTriggered function.  Did you rename it?"


    def _initCentralUic(self):
        """
        Load the GUI from the ui file into this class and connect it with event handlers.
        """
        # Load the ui file into this class (find it in our own directory)
        localDir = os.path.split(__file__)[0]+'/'
        uic.loadUi(localDir+"/dataSelection.ui", self)

        self._initTableViews()
        self._initViewerStack()
        self.splitter.setSizes( [150, 850] )

    def _initAppletDrawerUic(self, instructionText):
        """
        Load the ui file for the applet drawer, which we own.
        """
        localDir = os.path.split(__file__)[0]+'/'
        self._drawer = uic.loadUi(localDir+"/dataSelectionDrawer.ui")
        self._drawer.instructionLabel.setText( instructionText )

    def _initTableViews(self):
        self.fileInfoTabWidget.setTabText( 0, "Summary" )
        self.laneSummaryTableView.setModel( DataLaneSummaryTableModel(self, self.topLevelOperator) )
        self.laneSummaryTableView.dataLaneSelected.connect( self.showDataset )
        self.laneSummaryTableView.addFilesRequested.connect( self.addFiles )
        self.laneSummaryTableView.addStackRequested.connect( self.addStack )
        self.laneSummaryTableView.removeLanesRequested.connect( self.handleRemoveLaneButtonClicked )

        # These two helper functions enable/disable an 'add files' button for a given role  
        #  based on the the max lane index for that role and the overall permitted max_lanes
        def _update_button_status(viewer, role_index):
            if self._max_lanes:
                viewer.setEnabled( self._findFirstEmptyLane(role_index) < self._max_lanes )

        def _handle_lane_added( button, role_index, lane_slot, lane_index ):
            def _handle_role_slot_added( role_slot, added_slot_index, *args ):
                if added_slot_index == role_index:
                    role_slot.notifyReady( bind(_update_button_status, button, role_index) )
                    role_slot.notifyUnready( bind(_update_button_status, button, role_index) )
            lane_slot[lane_index].notifyInserted( _handle_role_slot_added )

        self._retained = [] # Retain menus so they don't get deleted
        self._detailViewerWidgets = []
        for roleIndex, role in enumerate(self.topLevelOperator.DatasetRoles.value):
            detailViewer = DatasetDetailedInfoTableView(self)
            detailViewer.setModel(DatasetDetailedInfoTableModel(self,
                self.topLevelOperator, roleIndex))
            self._detailViewerWidgets.append( detailViewer )

            # Button
            detailViewer.addFilesRequested.connect(
                    partial(self.addFiles, roleIndex))
            detailViewer.addStackRequested.connect(
                    partial(self.addStack, roleIndex))
            detailViewer.addRemoteVolumeRequested.connect(
                    partial(self.addDvidVolume, roleIndex))

            # Monitor changes to each lane so we can enable/disable the 'add lanes' button for each tab
            self.topLevelOperator.DatasetGroup.notifyInserted( bind( _handle_lane_added, detailViewer, roleIndex ) )
            self.topLevelOperator.DatasetGroup.notifyRemoved( bind( _update_button_status, detailViewer, roleIndex ) )
            
            # While we're at it, do the same for the buttons in the summary table, too
            self.topLevelOperator.DatasetGroup.notifyInserted( bind( _handle_lane_added, self.laneSummaryTableView.addFilesButtons[roleIndex], roleIndex ) )
            self.topLevelOperator.DatasetGroup.notifyRemoved( bind( _update_button_status, self.laneSummaryTableView.addFilesButtons[roleIndex], roleIndex ) )
            
            # Context menu
            detailViewer.replaceWithFileRequested.connect(
                    partial(self.handleReplaceFile, roleIndex) )
            detailViewer.replaceWithStackRequested.connect(
                    partial(self.addStack, roleIndex) )
            detailViewer.editRequested.connect(
                    partial(self.editDatasetInfo, roleIndex) )
            detailViewer.resetRequested.connect(
                    partial(self.handleClearDatasets, roleIndex) )

            # Drag-and-drop
            detailViewer.addFilesRequestedDrop.connect( partial( self.addFileNames, roleIndex=roleIndex ) )

            # Selection handling
            def showFirstSelectedDataset( _roleIndex, lanes ):
                if lanes:
                    self.showDataset( lanes[0], _roleIndex )
            detailViewer.dataLaneSelected.connect( partial(showFirstSelectedDataset, roleIndex) )

            self.fileInfoTabWidget.insertTab(roleIndex, detailViewer, role)
                
        self.fileInfoTabWidget.currentChanged.connect( self.handleSwitchTabs )
        self.fileInfoTabWidget.setCurrentIndex(0)

    def handleSwitchTabs(self, tabIndex ):
        if tabIndex < len(self._detailViewerWidgets):
            roleIndex = tabIndex # If summary tab is moved to the front, change this line.
            detailViewer = self._detailViewerWidgets[roleIndex]
            selectedLanes = detailViewer.selectedLanes
            if selectedLanes:
                self.showDataset( selectedLanes[0], roleIndex )

    def _initViewerStack(self):
        self.volumeEditors = {}
        self.viewerStack.addWidget( QWidget() )

    def handleRemoveLaneButtonClicked(self):
        """
        The user clicked the "Remove" button.
        Remove the currently selected row(s) from both the GUI and the top-level operator.
        """
        # Figure out which lanes to remove
        selectedIndexes = self.laneSummaryTableView.selectedIndexes()
        rows = set()
        for modelIndex in selectedIndexes:
            rows.add( modelIndex.row() )

        # Don't remove the last row, which is just buttons.
        rows.discard( self.laneSummaryTableView.model().rowCount()-1 )

        # Remove in reverse order so row numbers remain consistent
        for row in reversed(sorted(rows)):
            # Remove lanes from the operator.
            # The table model will notice the changes and update the rows accordingly.
            finalSize = len(self.topLevelOperator.DatasetGroup) - 1
            self.topLevelOperator.DatasetGroup.removeSlot(row, finalSize)
    
    @threadRouted
    def showDataset(self, laneIndex, roleIndex=None):
        if self._cleaning_up:
            return
        if laneIndex == -1:
            self.viewerStack.setCurrentIndex(0)
            return
        
        assert threading.current_thread().name == "MainThread"
        
        if laneIndex >= len(self.topLevelOperator.DatasetGroup):
            return
        datasetSlot = self.topLevelOperator.DatasetGroup[laneIndex]

        # Create if necessary
        if datasetSlot not in self.volumeEditors.keys():
            class DatasetViewer(LayerViewerGui):
                def moveToTop(self, roleIndex):
                    opLaneView = self.topLevelOperatorView
                    if not opLaneView.DatasetRoles.ready():
                        return
                    datasetRoles = opLaneView.DatasetRoles.value
                    if roleIndex >= len(datasetRoles):
                        return
                    roleName = datasetRoles[roleIndex]
                    try:
                        layerIndex = [l.name for l in self.layerstack].index(roleName)
                    except ValueError:
                        return
                    else:
                        self.layerstack.selectRow(layerIndex)
                        self.layerstack.moveSelectedToTop()

                def setupLayers(self):
                    opLaneView = self.topLevelOperatorView
                    if not opLaneView.DatasetRoles.ready():
                        return []
                    layers = []
                    datasetRoles = opLaneView.DatasetRoles.value
                    for roleIndex, slot in enumerate(opLaneView.ImageGroup):
                        if slot.ready():
                            roleName = datasetRoles[roleIndex]
                            layer = self.createStandardLayerFromSlot(slot)
                            layer.name = roleName
                            layers.append(layer)
                    return layers

            opLaneView = self.topLevelOperator.getLane(laneIndex)
            layerViewer = DatasetViewer(self.parentApplet, opLaneView, crosshair=False)
            
            # Maximize the x-y view by default.
            layerViewer.volumeEditorWidget.quadview.ensureMaximized(2)

            self.volumeEditors[datasetSlot] = layerViewer
            self.viewerStack.addWidget( layerViewer )
            self._viewerControlWidgetStack.addWidget( layerViewer.viewerControlWidget() )

        # Show the right one
        viewer = self.volumeEditors[datasetSlot]
        displayedRole = self.fileInfoTabWidget.currentIndex()
        viewer.moveToTop(displayedRole)
        self.viewerStack.setCurrentWidget( viewer )
        self._viewerControlWidgetStack.setCurrentWidget( viewer.viewerControlWidget() )


    def handleReplaceFile(self, roleIndex, startingLane):
        self.addFiles(roleIndex, startingLane)

    def addFiles(self, roleIndex, startingLane=None):
        """
        The user clicked the "Add File" button.
        Ask him to choose a file (or several) and add them to both
          the GUI table and the top-level operator inputs.
        """
        # Find the directory of the most recently opened image file
        mostRecentImageFile = PreferencesManager().get( 'DataSelection', 'recent image' )
        if mostRecentImageFile is not None:
            defaultDirectory = os.path.split(mostRecentImageFile)[0]
        else:
            defaultDirectory = os.path.expanduser('~')

        # Launch the "Open File" dialog
        fileNames = self.getImageFileNamesToOpen(self, defaultDirectory)

        # If the user didn't cancel
        if len(fileNames) > 0:
            PreferencesManager().set('DataSelection', 'recent image', fileNames[0])
            try:
                self.addFileNames(fileNames, roleIndex, startingLane)
            except Exception as ex:
                log_exception( logger )
                QMessageBox.critical(self, "Error loading file", str(ex))

    @classmethod
    def getImageFileNamesToOpen(cls, parent_window, defaultDirectory):
        """
        Launch an "Open File" dialog to ask the user for one or more image files.
        """
        extensions = OpDataSelection.SupportedExtensions
        filter_strs = ["*." + x for x in extensions]
        filters = ["{filt} ({filt})".format(filt=x) for x in filter_strs]
        filt_all_str = "Image files (" + ' '.join(filter_strs) + ')'

        fileNames = []
        
        if ilastik_config.getboolean("ilastik", "debug"):
            # use Qt dialog in debug mode (more portable?)
            file_dialog = QFileDialog(parent_window, "Select Images")
            file_dialog.setOption(QFileDialog.DontUseNativeDialog, True)
            # do not display file types associated with a filter
            # the line for "Image files" is too long otherwise
            file_dialog.setFilters([filt_all_str] + filters)
            file_dialog.setNameFilterDetailsVisible(False)
            # select multiple files
            file_dialog.setFileMode(QFileDialog.ExistingFiles)
            file_dialog.setDirectory( defaultDirectory )

            if file_dialog.exec_():
                fileNames = file_dialog.selectedFiles()
        else:
            # otherwise, use native dialog of the present platform
            fileNames = QFileDialog.getOpenFileNames(parent_window, "Select Images", defaultDirectory, filt_all_str)
        # Convert from QtString to python str
        fileNames = map(encode_from_qstring, fileNames)
        return fileNames

    def _findFirstEmptyLane(self, roleIndex):
        opTop = self.topLevelOperator
        
        # Determine the number of files this role already has
        # Search for the last valid value.
        firstNewLane = 0
        for laneIndex, slot in reversed(zip(range(len(opTop.DatasetGroup)), opTop.DatasetGroup)):
            if slot[roleIndex].ready():
                firstNewLane = laneIndex+1
                break
        return firstNewLane

    def addFileNames(self, fileNames, roleIndex, startingLane=None, rois=None):
        """
        Add the given filenames to both the GUI table and the top-level operator inputs.
        If startingLane is None, the filenames will be *appended* to the role's list of files.
        
        If rois is provided, it must be a list of (start,stop) tuples (one for each fileName)
        """
        # What lanes will we touch?
        startingLane, endingLane = self._determineLaneRange(fileNames, roleIndex, startingLane)
        if startingLane is None:
            # Something went wrong.
            return

        # If we're only adding new lanes, NOT modifying existing lanes...
        adding_only = startingLane == len(self.topLevelOperator)

        # Create a list of DatasetInfos
        try:
            infos = self._createDatasetInfos(roleIndex, fileNames, rois)
        except DataSelectionGui.UserCancelledError:
            return
        
        # If no exception was thrown so far, set up the operator now
        loaded_all = self._configureOpWithInfos(roleIndex, startingLane, endingLane, infos)
        
        if loaded_all:
            # Now check the resulting slots.
            # If they should be copied to the project file, say so.
            self._reconfigureDatasetLocations(roleIndex, startingLane, endingLane)
    
            self._checkDataFormatWarnings(roleIndex, startingLane, endingLane)
    
            # If we succeeded in adding all images, show the first one.
            self.showDataset(startingLane, roleIndex)

        # Notify the workflow that we just added some new lanes.
        if adding_only:
            workflow = self.parentApplet.topLevelOperator.parent
            workflow.handleNewLanesAdded()

        # Notify the workflow that something that could affect applet readyness has occurred.
        self.parentApplet.appletStateUpdateRequested.emit()

        self.updateInternalPathVisiblity()

    def _determineLaneRange(self, fileNames, roleIndex, startingLane=None):
        """
        Determine which lanes should be configured if the user wants to add the 
            given fileNames to the specified role, starting at startingLane.
        If startingLane is None, assume the user wants to APPEND the 
            files to the role's slots.
        """
        if startingLane is None or startingLane == -1:
            startingLane = len(self.topLevelOperator.DatasetGroup)
            endingLane = startingLane+len(fileNames)-1
        else:
            assert startingLane < len(self.topLevelOperator.DatasetGroup)
            max_files = len(self.topLevelOperator.DatasetGroup) - \
                    startingLane
            if len(fileNames) > max_files:
                msg = "You selected {num_selected} files for {num_slots} "\
                      "slots. To add new files use the 'Add new...' option "\
                      "in the context menu or the button in the last row."\
                              .format(num_selected=len(fileNames),
                                      num_slots=max_files)
                QMessageBox.critical( self, "Too many files", msg )
                return (None, None)
            endingLane = min(startingLane+len(fileNames)-1,
                    len(self.topLevelOperator.DatasetGroup))
            
        if self._max_lanes and endingLane >= self._max_lanes:
            msg = "You may not add more than {} file(s) to this workflow.  Please try again.".format( self._max_lanes )
            QMessageBox.critical( self, "Too many files", msg )
            return (None, None)

        return (startingLane, endingLane)

    def _createDatasetInfos(self, roleIndex, filePaths, rois):
        """
        Create a list of DatasetInfos for the given filePaths and rois
        rois may be None, in which case it is ignored.
        """
        if rois is None:
            rois = [None]*len(filePaths)
        assert len(rois) == len(filePaths)

        infos = []
        for filePath, roi in zip(filePaths, rois):
            info = self._createDatasetInfo(roleIndex, filePath, roi)
            infos.append(info)
        return infos

    def _createDatasetInfo(self, roleIndex, filePath, roi):
        """
        Create a DatasetInfo object for the given filePath and roi.
        roi may be None, in which case it is ignored.
        """
        cwd = self.topLevelOperator.WorkingDirectory.value
        datasetInfo = DatasetInfo(filePath, cwd=cwd)
        datasetInfo.subvolume_roi = roi # (might be None)
                
        absPath, relPath = getPathVariants(filePath, cwd)
        
        # If the file is in a totally different tree from the cwd,
        # then leave the path as absolute.  Otherwise, override with the relative path.
        if relPath is not None and len(os.path.commonprefix([cwd, absPath])) > 1:
            datasetInfo.filePath = relPath
            
        h5Exts = ['.ilp', '.h5', '.hdf5']
        if os.path.splitext(datasetInfo.filePath)[1] in h5Exts:
            datasetNames = self.getPossibleInternalPaths( absPath )
            if len(datasetNames) == 0:
                raise RuntimeError("HDF5 file %s has no image datasets" % datasetInfo.filePath)
            elif len(datasetNames) == 1:
                datasetInfo.filePath += str(datasetNames[0])
            else:
                # If exactly one of the file's datasets matches a user's previous choice, use it.
                if roleIndex not in self._default_h5_volumes:
                    self._default_h5_volumes[roleIndex] = set()
                previous_selections = self._default_h5_volumes[roleIndex]
                possible_auto_selections = previous_selections.intersection(datasetNames)
                if len(possible_auto_selections) == 1:
                    datasetInfo.filePath += str(list(possible_auto_selections)[0])
                else:
                    # Ask the user which dataset to choose
                    dlg = H5VolumeSelectionDlg(datasetNames, self)
                    if dlg.exec_() == QDialog.Accepted:
                        selected_index = dlg.combo.currentIndex()
                        selected_dataset = str(datasetNames[selected_index])
                        datasetInfo.filePath += selected_dataset
                        self._default_h5_volumes[roleIndex].add( selected_dataset )
                    else:
                        raise DataSelectionGui.UserCancelledError()

        # Allow labels by default if this gui isn't being used for batch data.
        datasetInfo.allowLabels = ( self.guiMode == GuiMode.Normal )
        return datasetInfo

    def _configureOpWithInfos(self, roleIndex, startingLane, endingLane, infos):
        """
        Attempt to configure the specified role and lanes of the 
        top-level operator with the given DatasetInfos.
        
        Returns True if all lanes were configured successfully, or False if something went wrong.
        """
        opTop = self.topLevelOperator
        originalSize = len(opTop.DatasetGroup)

        # Resize the slot if necessary            
        if len( opTop.DatasetGroup ) < endingLane+1:
            opTop.DatasetGroup.resize( endingLane+1 )
        
        # Configure each subslot
        for laneIndex, info in zip(range(startingLane, endingLane+1), infos):
            try:
                self.topLevelOperator.DatasetGroup[laneIndex][roleIndex].setValue( info )
            except DatasetConstraintError as ex:
                return_val = [False]
                # Give the user a chance to fix the problem
                self.handleDatasetConstraintError(info, info.filePath, ex, roleIndex, laneIndex, return_val)
                if not return_val[0]:
                    # Not successfully repaired.  Roll back the changes and give up.
                    opTop.DatasetGroup.resize( originalSize )
                    return False
            except OpDataSelection.InvalidDimensionalityError as ex:
                    opTop.DatasetGroup.resize( originalSize )
                    QMessageBox.critical( self, "Dataset has different dimensionality", ex.message )
                    return False
            except Exception as ex:
                msg = "Wasn't able to load your dataset into the workflow.  See error log for details."
                log_exception( logger, msg )
                QMessageBox.critical( self, "Dataset Load Error", msg )
                opTop.DatasetGroup.resize( originalSize )
                return False
        
        return True

    def _reconfigureDatasetLocations(self, roleIndex, startingLane, endingLane):
        """
        Check the metadata for the given slots.  
        If the data is stored a format that is poorly optimized for 3D access, 
        then configure it to be copied to the project file.
        Finally, save the project if we changed something. 
        """
        save_needed = False
        opTop = self.topLevelOperator
        for lane_index in range(startingLane, endingLane+1):
            output_slot = opTop.ImageGroup[lane_index][roleIndex]
            if output_slot.meta.prefer_2d and 'z' in output_slot.meta.axistags:
                shape = numpy.array(output_slot.meta.shape)
                total_volume = numpy.prod(shape)
                
                # Only copy to the project file if the total volume is reasonably small
                if total_volume < 0.5e9:
                    info_slot = opTop.DatasetGroup[lane_index][roleIndex]
                    info = info_slot.value
                    info.location = DatasetInfo.Location.ProjectInternal
                    info_slot.setValue( info, check_changed=False )
                    save_needed = True

        if save_needed:
            logger.info("Some of your data cannot be accessed efficiently in 3D in its current format."
                        "  It will now be copied to the project file.")
            opWorkflow = self.topLevelOperator.parent
            opWorkflow.shell.onSaveProjectActionTriggered()

    def _checkDataFormatWarnings(self, roleIndex, startingLane, endingLane):
        warn_needed = False
        opTop = self.topLevelOperator
        for lane_index in range(startingLane, endingLane+1):
            output_slot = opTop.ImageGroup[lane_index][roleIndex]
            if output_slot.meta.inefficient_format:
                warn_needed = True

        if warn_needed:        
            QMessageBox.warning( self, "Inefficient Data Format", 
                              "Your data cannot be accessed efficiently in its current format.  "
                              "Check the console output for details.\n"
                              "(For HDF5 files, be sure to enable chunking on your dataset.)" )

    @threadRouted
    def handleDatasetConstraintError(self, info, filename, ex, roleIndex, laneIndex, return_val=[False]):
        msg = "Can't use default properties for dataset:\n\n" + \
              filename + "\n\n" + \
              "because it violates a constraint of the {} applet.\n\n".format( ex.appletName ) + \
              ex.message + "\n\n" + \
              "Please enter valid dataset properties to continue."
        QMessageBox.warning( self, "Dataset Needs Correction", msg )
        
        # The success of this is 'returned' via our special out-param
        # (We can't return a value from this func because it is @threadRouted.
        successfully_repaired = self.repairDatasetInfo( info, roleIndex, laneIndex )
        return_val[0] = successfully_repaired

    def repairDatasetInfo(self, info, roleIndex, laneIndex):
        """Open the dataset properties editor and return True if the new properties are acceptable."""
        defaultInfos = {}
        defaultInfos[laneIndex] = info
        editorDlg = DatasetInfoEditorWidget(self, self.topLevelOperator, roleIndex, [laneIndex], defaultInfos, show_axis_details=self.show_axis_details)
        dlg_state = editorDlg.exec_()
        return ( dlg_state == QDialog.Accepted )

    @classmethod
    def getPossibleInternalPaths(cls, absPath, min_ndim=3, max_ndim=5):
        datasetNames = []
        # Open the file as a read-only so we can get a list of the internal paths
        with h5py.File(absPath, 'r') as f:
            # Define a closure to collect all of the dataset names in the file.
            def accumulateDatasetPaths(name, val):
                if type(val) == h5py._hl.dataset.Dataset and min_ndim <= len(val.shape) <= max_ndim:
                    datasetNames.append( '/' + name )
            # Visit every group/dataset in the file
            f.visititems(accumulateDatasetPaths)
        return datasetNames

    def addStack(self, roleIndex, laneIndex):
        """
        The user clicked the "Import Stack Files" button.
        """
        stackDlg = StackFileSelectionWidget(self)
        stackDlg.exec_()
        if stackDlg.result() != QDialog.Accepted :
            return
        files = stackDlg.selectedFiles
        sequence_axis = stackDlg.sequence_axis
        if len(files) == 0:
            return

        cwd = self.topLevelOperator.WorkingDirectory.value
        info = DatasetInfo(os.path.pathsep.join(files), cwd=cwd)

        originalNumLanes = len(self.topLevelOperator.DatasetGroup)

        if laneIndex is None or laneIndex == -1:
            laneIndex = len(self.topLevelOperator.DatasetGroup)
        if len(self.topLevelOperator.DatasetGroup) < laneIndex+1:
            self.topLevelOperator.DatasetGroup.resize(laneIndex+1)

        def importStack():
            self.parentApplet.busy = True
            self.parentApplet.appletStateUpdateRequested.emit()

            # Serializer will update the operator for us, which will propagate to the GUI.
            try:
                self.serializer.importStackAsLocalDataset( info, sequence_axis )
                try:
                    self.topLevelOperator.DatasetGroup[laneIndex][roleIndex].setValue(info)
                except DatasetConstraintError as ex:
                    # Give the user a chance to repair the problem.
                    filename = files[0] + "\n...\n" + files[-1]
                    return_val = [False]
                    self.handleDatasetConstraintError( info, filename, ex, roleIndex, laneIndex, return_val )
                    if not return_val[0]:
                        # Not successfully repaired.  Roll back the changes and give up.
                        self.topLevelOperator.DatasetGroup.resize(originalNumLanes)
            finally:
                self.parentApplet.busy = False
                self.parentApplet.appletStateUpdateRequested.emit()

        req = Request( importStack )
        req.notify_finished( lambda result: self.showDataset(laneIndex, roleIndex) )
        req.notify_failed( partial(self.handleFailedStackLoad, files, originalNumLanes ) )
        req.submit()

    @threadRouted
    def handleFailedStackLoad(self, files, originalNumLanes, exc, exc_info):
        msg = "Failed to load stack due to the following error:\n{}".format( exc )
        msg += "\nAttempted stack files were:\n"
        msg += "\n".join(files)
        log_exception( logger, msg, exc_info )
        QMessageBox.critical(self, "Failed to load image stack", msg)
        self.topLevelOperator.DatasetGroup.resize(originalNumLanes)

    def handleClearDatasets(self, roleIndex, selectedRows):
        for row in selectedRows:
            self.topLevelOperator.DatasetGroup[row][roleIndex].disconnect()

        # Remove all operators that no longer have any connected slots        
        laneIndexes = range( len(self.topLevelOperator.DatasetGroup) )
        for laneIndex, multislot in reversed(zip(laneIndexes, self.topLevelOperator.DatasetGroup)):
            any_ready = False
            for slot in multislot:
                any_ready |= slot.ready()
            if not any_ready:
                self.topLevelOperator.DatasetGroup.removeSlot( laneIndex, len(self.topLevelOperator.DatasetGroup)-1 )

        # Notify the workflow that something that could affect applet readyness has occurred.
        self.parentApplet.appletStateUpdateRequested.emit()

    def editDatasetInfo(self, roleIndex, laneIndexes):
        editorDlg = DatasetInfoEditorWidget(self, self.topLevelOperator, roleIndex, laneIndexes, show_axis_details=self.show_axis_details)
        editorDlg.exec_()
        self.parentApplet.appletStateUpdateRequested.emit()

    def updateInternalPathVisiblity(self):
        for view in self._detailViewerWidgets:
            model = view.model()
            view.setColumnHidden(DatasetDetailedInfoColumn.InternalID,
                                 not model.hasInternalPaths())
    
    def addDvidVolume(self, roleIndex, laneIndex):
        # TODO: Provide list of recently used dvid hosts, loaded from user preferences
        recent_hosts_pref = PreferencesManager.Setting("DataSelection", "Recent DVID Hosts")
        recent_hosts = recent_hosts_pref.get()
        if not recent_hosts:
            recent_hosts = ["localhost:8000"]
        recent_hosts = filter(lambda h: h, recent_hosts)
            
        from dvidDataSelectionBrowser import DvidDataSelectionBrowser
        browser = DvidDataSelectionBrowser(recent_hosts, parent=self)
        if browser.exec_() == DvidDataSelectionBrowser.Rejected:
            return

        if None in browser.get_selection():
            QMessageBox.critical("Couldn't use your selection.")
            return

        rois = None
        hostname, dset_uuid, volume_name, uuid = browser.get_selection()
        dvid_url = 'http://{hostname}/api/node/{uuid}/{volume_name}'.format( **locals() )
        subvolume_roi = browser.get_subvolume_roi()

        # Relocate host to top of 'recent' list, and limit list to 10 items.
        try:
            i = recent_hosts.index(recent_hosts)
            del recent_hosts[i]
        except ValueError:
            pass
        finally:
            recent_hosts.insert(0, hostname)        
            recent_hosts = recent_hosts[:10]

        # Save pref
        recent_hosts_pref.set(recent_hosts)

        if subvolume_roi is None:
            self.addFileNames([dvid_url], roleIndex, laneIndex)
        else:
            # In ilastik, we display the dvid volume axes in C-order, despite the dvid convention of F-order
            # Transpose the subvolume roi to match
            # (see implementation of OpDvidVolume)
            start, stop = subvolume_roi
            start = tuple(reversed(start))
            stop = tuple(reversed(stop))
            self.addFileNames([dvid_url], roleIndex, laneIndex, [(start, stop)])
Example #6
0
class NodeWindow(QMainWindow):
    def __init__(self, data, scheme, parent=None):
        super(NodeWindow, self).__init__(parent)
        self.pathWidget = PathWidget(self.openWidgetByPath, data.path())
        self.setStatusBar(self.pathWidget)
#        layout_set_sm_and_mrg(self.layout)
        self.cachedWidgets = {}
        self.currentStructuredWidget = None
        self.stacked = QStackedWidget(self)
        self.setCentralWidget(self.stacked)

        self.data, self.scheme = data, scheme
        self.data.add_set_notify(self.change_caption)
        self.toolbar = QToolBar()
        self.toolbar.addActions((self.parent().actionSave,self.parent().actionSaveAs,))
        self.addToolBar(self.toolbar)
        self.setUnifiedTitleAndToolBarOnMac(True)
        self.messageBoxChanged = None
        self.reallyQuit = False
        self.change_caption()

        if "ExcelScheme" in self.scheme.get_meta():
            actionExcelExport = QAction("Export to excel", self)
            self.toolbar.addAction(actionExcelExport)
            actionExcelExport.triggered.connect(self.excel_export)
            actionExcelMerge = QAction("Merge from excel", self)
            actionExcelMerge.triggered.connect(self.excel_import)
            self.toolbar.addAction(actionExcelMerge)

        self.tree_widget = DataTreeWidget(self.data, self)
        dock = QDockWidget(self)
        dock.setWidget(self.tree_widget)
        self.addDockWidget(Qt.LeftDockWidgetArea, dock)
        self.tree_widget.pathChanged.connect(self._open_widget_by_path)

        self.openWidgetByPath(Path())


    def change_caption(self):
        changed = ""
        if self.data.changed:
            changed = "* "
        self.setWindowTitle("{} {}".format(changed, self.get_window_caption()))

    def get_window_caption(self):
        return os.path.basename(self.parent().save_filename or "New Data")

    def openWidgetByPath(self, path):
        self._open_widget_by_path(path)
        self.tree_widget.pathChange(path)

    def _open_widget_by_path(self, path):
#        #fixme
#        try:
            if path in self.cachedWidgets:
    #            if self.currentStructuredWidget:
    #                self.currentStructuredWidget.hide()
                self.currentStructuredWidget = self.cachedWidgets[path]
                self.stacked.setCurrentWidget(self.currentStructuredWidget)
                self.pathWidget.setPath(path)
            else:
                if "Type" not in path.get(self.scheme): #fimxe soon
                    self.cachedWidgets[path] = StructuredWidget(unicode(path), path.get(self.data, reduce_sub_elements=True), path.get(self.scheme), self.openWidgetByPath, self)
                    self.stacked.addWidget(self.cachedWidgets[path])
                    self._open_widget_by_path(path)
                else:
                    print ""
                    pass
#        except KeyError:
#            pass

    def closeEvent(self, event):
        if self.reallyQuit or not self.data.changed:
            event.accept()
        else:
            self.dialogChanged()
            event.ignore()

    def dialogChanged(self):
        if not self.messageBoxChanged:
            self.messageBoxChanged = QMessageBox("SDI",
                "The document has been modified.\n"+
                    "Do you want to save your changes?",
                QMessageBox.Warning,
                QMessageBox.Yes | QMessageBox.Default,
                QMessageBox.No,
                QMessageBox.Cancel | QMessageBox.Escape,
                self
            )
            self.messageBoxChanged.setWindowModality (Qt.WindowModal )
            self.messageBoxChanged.finished.connect(self.finishClose)
        self.messageBoxChanged.show()

    def finishClose(self, value):
        if value==QMessageBox.Yes:
            self.reallyQuit = self.parent().save_data()
            if not self.reallyQuit:
                return
        elif value==QMessageBox.No:
            self.reallyQuit = True
        elif value==QMessageBox.Cancel:
            return
        self.close()

    def excel_export(self):
        excel_filename = unicode(QFileDialog.getSaveFileName(self, "Save File",
                            "New excel file.xls", "Excel files (*.xls)"))
        if excel_filename:
            export_to_excel(self.data, self.scheme, excel_filename)

    def excel_import(self):
        excel_filename = unicode(QFileDialog.getOpenFileName(self, "Open File", get_home_dir(),  "Excel files (*.xls)"))
        if excel_filename:
            data_to_merge = import_from_excel(self.scheme, excel_filename)
            self.parent()._merge_data(data_to_merge)
Example #7
0
class ScorePartsWidget(QSplitter):
    def __init__(self, parent):
        super(ScorePartsWidget, self).__init__(parent)

        self.typesLabel = QLabel()
        self.typesView = QTreeView(selectionMode=QTreeView.ExtendedSelection,
                                   selectionBehavior=QTreeView.SelectRows,
                                   animated=True,
                                   headerHidden=True)
        self.scoreLabel = QLabel()
        self.scoreView = widgets.treewidget.TreeWidget(
            selectionMode=QTreeView.ExtendedSelection,
            selectionBehavior=QTreeView.SelectRows,
            headerHidden=True,
            animated=True,
            dragDropMode=QTreeView.InternalMove)
        self.addButton = QPushButton(icon=icons.get("list-add"))
        self.removeButton = QPushButton(icon=icons.get("list-remove"))
        self.upButton = QToolButton(icon=icons.get("go-up"))
        self.downButton = QToolButton(icon=icons.get("go-down"))
        self.partSettings = QStackedWidget()

        w = QWidget()
        self.addWidget(w)
        layout = QVBoxLayout(spacing=0)
        w.setLayout(layout)

        layout.addWidget(self.typesLabel)
        layout.addWidget(self.typesView)
        layout.addWidget(self.addButton)

        w = QWidget()
        self.addWidget(w)
        layout = QVBoxLayout(spacing=0)
        w.setLayout(layout)

        layout.addWidget(self.scoreLabel)
        layout.addWidget(self.scoreView)

        box = QHBoxLayout(spacing=0)
        layout.addLayout(box)

        box.addWidget(self.removeButton)
        box.addWidget(self.upButton)
        box.addWidget(self.downButton)

        self.addWidget(self.partSettings)

        self.typesView.setModel(parts.model())
        app.translateUI(self)

        # signal connections
        self.addButton.clicked.connect(self.slotAddButtonClicked)
        self.removeButton.clicked.connect(self.slotRemoveButtonClicked)
        self.typesView.doubleClicked.connect(self.slotDoubleClicked)
        self.scoreView.currentItemChanged.connect(self.slotCurrentItemChanged)
        self.upButton.clicked.connect(self.scoreView.moveSelectedChildrenUp)
        self.downButton.clicked.connect(
            self.scoreView.moveSelectedChildrenDown)

    def translateUI(self):
        bold = "<b>{0}</b>".format
        self.typesLabel.setText(bold(_("Available parts:")))
        self.scoreLabel.setText(bold(_("Score:")))
        self.addButton.setText(_("&Add"))
        self.removeButton.setText(_("&Remove"))
        self.upButton.setToolTip(_("Move up"))
        self.downButton.setToolTip(_("Move down"))

    def slotDoubleClicked(self, index):
        self.addParts([index])

    def slotAddButtonClicked(self):
        self.addParts(self.typesView.selectedIndexes())

    def addParts(self, indexes):
        """Adds the parts for the given indexes."""
        # add to current if that is a container type
        currentItem = self.scoreView.currentItem()
        for index in indexes:
            category = index.internalPointer()
            if category:
                part = category.items[index.row()]
                box = QGroupBox(self.partSettings)
                self.partSettings.addWidget(box)
                # determine the parent: current or root
                if currentItem and issubclass(part,
                                              currentItem.part.accepts()):
                    parent = currentItem
                    parent.setExpanded(True)
                else:
                    parent = self.scoreView
                item = PartItem(parent, part, box)

    def slotCurrentItemChanged(self, item):
        if isinstance(item, PartItem):
            self.partSettings.setCurrentWidget(item.box)

    def slotRemoveButtonClicked(self):
        self.scoreView.removeSelectedItems()

    def clear(self):
        """Called when the user clicks the clear button on this page."""
        self.scoreView.clear()

    def rootPartItem(self):
        """Returns the invisibleRootItem(), representing the tree of parts in the score view."""
        return self.scoreView.invisibleRootItem()
Example #8
0
class DataSelectionGui(QWidget):
    """
    Manages all GUI elements in the data selection applet.
    This class itself is the central widget and also owns/manages the applet drawer widgets.
    """

    ###########################################
    ### AppletGuiInterface Concrete Methods ###
    ###########################################

    def centralWidget( self ):
        return self

    def appletDrawer( self ):
        return self._drawer

    def menus( self ):
        return []

    def viewerControlWidget(self):
        return self._viewerControlWidgetStack

    def setImageIndex(self, imageIndex):
        if imageIndex is not None:
            self.laneSummaryTableView.selectRow(imageIndex)
            for detailWidget in self._detailViewerWidgets:
                detailWidget.datasetDetailTableView.selectRow(imageIndex)

    def stopAndCleanUp(self):
        for editor in self.volumeEditors.values():
            self.viewerStack.removeWidget( editor )
            self._viewerControlWidgetStack.removeWidget( editor.viewerControlWidget() )
            editor.stopAndCleanUp()
        self.volumeEditors.clear()

    def imageLaneAdded(self, laneIndex):
        if len(self.laneSummaryTableView.selectedIndexes()) == 0:
            self.laneSummaryTableView.selectRow(laneIndex)
        
        # We don't have any real work to do because this gui initiated the lane addition in the first place
        if self.guiMode != GuiMode.Batch:
            if(len(self.topLevelOperator.DatasetGroup) != laneIndex+1):
                import warnings
                warnings.warn("DataSelectionGui.imageLaneAdded(): length of dataset multislot out of sync with laneindex [%s != %s + 1]" % (len(self.topLevelOperator.Dataset), laneIndex))

    def imageLaneRemoved(self, laneIndex, finalLength):
        # We assume that there's nothing to do here because THIS GUI initiated the lane removal
        if self.guiMode != GuiMode.Batch:
            assert len(self.topLevelOperator.DatasetGroup) == finalLength

    ###########################################
    ###########################################

    def __init__(self, dataSelectionOperator, serializer, guiControlSignal, guiMode=GuiMode.Normal, title="Input Selection"):
        with Tracer(traceLogger):
            super(DataSelectionGui, self).__init__()

            self.title = title

            self._viewerControls = QWidget()
            self.topLevelOperator = dataSelectionOperator
            self.guiMode = guiMode
            self.serializer = serializer
            self.guiControlSignal = guiControlSignal
            self.threadRouter = ThreadRouter(self)

            self._initCentralUic()
            self._initAppletDrawerUic()
            
            self._viewerControlWidgetStack = QStackedWidget(self)

            def handleImageRemoved(multislot, index, finalLength):
                # Remove the viewer for this dataset
                imageSlot = self.topLevelOperator.Image[index]
                if imageSlot in self.volumeEditors.keys():
                    editor = self.volumeEditors[imageSlot]
                    self.viewerStack.removeWidget( editor )
                    self._viewerControlWidgetStack.removeWidget( editor.viewerControlWidget() )
                    editor.stopAndCleanUp()

            self.topLevelOperator.Image.notifyRemove( bind( handleImageRemoved ) )

    def _initCentralUic(self):
        """
        Load the GUI from the ui file into this class and connect it with event handlers.
        """
        # Load the ui file into this class (find it in our own directory)
        localDir = os.path.split(__file__)[0]+'/'
        uic.loadUi(localDir+"/dataSelection.ui", self)

        self._initTableViews()
        self._initViewerStack()
        self.splitter.setSizes( [150, 850] )

    def _initAppletDrawerUic(self):
        """
        Load the ui file for the applet drawer, which we own.
        """
        localDir = os.path.split(__file__)[0]+'/'
        self._drawer = uic.loadUi(localDir+"/dataSelectionDrawer.ui")

    def _initTableViews(self):
        self.fileInfoTabWidget.setTabText( 0, "Summary" )
        self.laneSummaryTableView.setModel( DataLaneSummaryTableModel(self, self.topLevelOperator) )
        self.laneSummaryTableView.dataLaneSelected.connect( self.showDataset )
        self.laneSummaryTableView.addFilesRequested.connect( self.handleAddFiles )
        self.laneSummaryTableView.addStackRequested.connect( self.handleAddStack )
        self.laneSummaryTableView.addByPatternRequested.connect( self.handleAddByPattern )
        self.removeLaneButton.clicked.connect( self.handleRemoveLaneButtonClicked )
        self.laneSummaryTableView.removeLanesRequested.connect( self.handleRemoveLaneButtonClicked )

        self._retained = [] # Retain menus so they don't get deleted
        self._detailViewerWidgets = []
        for roleIndex, role in enumerate(self.topLevelOperator.DatasetRoles.value):
            detailViewer = DataDetailViewerWidget( self, self.topLevelOperator, roleIndex )
            self._detailViewerWidgets.append( detailViewer )

            # Button
            menu = QMenu(parent=self)
            menu.setObjectName("addFileButton_role_{}".format( roleIndex ))
            menu.addAction( "Add File(s)..." ).triggered.connect( partial(self.handleAddFiles, roleIndex) )
            menu.addAction( "Add Volume from Stack..." ).triggered.connect( partial(self.handleAddStack, roleIndex) )
            menu.addAction( "Add Many by Pattern..." ).triggered.connect( partial(self.handleAddByPattern, roleIndex) )
            detailViewer.appendButton.setMenu( menu )
            self._retained.append(menu)
            
            # Context menu            
            detailViewer.datasetDetailTableView.replaceWithFileRequested.connect( partial(self.handleReplaceFile, roleIndex) )
            detailViewer.datasetDetailTableView.replaceWithStackRequested.connect( partial(self.replaceWithStack, roleIndex) )
            detailViewer.datasetDetailTableView.editRequested.connect( partial(self.editDatasetInfo, roleIndex) )
            detailViewer.datasetDetailTableView.resetRequested.connect( partial(self.handleClearDatasets, roleIndex) )

            # Drag-and-drop
            detailViewer.datasetDetailTableView.addFilesRequested.connect( partial( self.addFileNames, roleIndex=roleIndex ) )

            # Selection handling
            def showFirstSelectedDataset( _roleIndex, lanes ):
                if lanes:
                    self.showDataset( lanes[0], _roleIndex )
            detailViewer.datasetDetailTableView.dataLaneSelected.connect( partial(showFirstSelectedDataset, roleIndex) )

            self.fileInfoTabWidget.insertTab(roleIndex, detailViewer, role)
                
        self.fileInfoTabWidget.currentChanged.connect( self.handleSwitchTabs )
        self.fileInfoTabWidget.setCurrentIndex(0)

    def handleSwitchTabs(self, tabIndex ):
        if tabIndex < len(self._detailViewerWidgets):
            roleIndex = tabIndex # If summary tab is moved to the front, change this line.
            detailViewer = self._detailViewerWidgets[roleIndex]
            selectedLanes = detailViewer.datasetDetailTableView.selectedLanes
            if selectedLanes:
                self.showDataset( selectedLanes[0], roleIndex )

    def _initViewerStack(self):
        self.volumeEditors = {}
        self.viewerStack.addWidget( QWidget() )

    def handleRemoveLaneButtonClicked(self):
        """
        The user clicked the "Remove" button.
        Remove the currently selected row(s) from both the GUI and the top-level operator.
        """
        # Figure out which lanes to remove
        selectedIndexes = self.laneSummaryTableView.selectedIndexes()
        rows = set()
        for modelIndex in selectedIndexes:
            rows.add( modelIndex.row() )
        rows.discard( self.laneSummaryTableView.model().rowCount() )

        # Remove in reverse order so row numbers remain consistent
        for row in reversed(sorted(rows)):
            # Remove from the GUI
            self.laneSummaryTableView.model().removeRow(row)
            # Remove from the operator
            finalSize = len(self.topLevelOperator.DatasetGroup) - 1
            self.topLevelOperator.DatasetGroup.removeSlot(row, finalSize)
    
            # The gui and the operator should be in sync
            assert self.laneSummaryTableView.model().rowCount() == len(self.topLevelOperator.DatasetGroup)+1

    def showDataset(self, laneIndex, roleIndex=None):
        if laneIndex == -1:
            self.viewerStack.setCurrentIndex(0)
            return
        
        assert threading.current_thread().name == "MainThread"
        imageSlot = self.topLevelOperator.Image[laneIndex]

        # Create if necessary
        if imageSlot not in self.volumeEditors.keys():
            
            class DatasetViewer(LayerViewerGui):
                def moveToTop(self, roleIndex):
                    opLaneView = self.topLevelOperatorView
                    if not opLaneView.DatasetRoles.ready():
                        return
                    datasetRoles = opLaneView.DatasetRoles.value
                    roleName = datasetRoles[roleIndex]
                    try:
                        layerIndex = [l.name for l in self.layerstack].index(roleName)
                    except ValueError:
                        return
                    else:
                        self.layerstack.selectRow(layerIndex)
                        self.layerstack.moveSelectedToTop()

                def setupLayers(self):
                    opLaneView = self.topLevelOperatorView
                    if not opLaneView.DatasetRoles.ready():
                        return []
                    layers = []
                    datasetRoles = opLaneView.DatasetRoles.value
                    for roleIndex, slot in enumerate(opLaneView.ImageGroup):
                        if slot.ready():
                            roleName = datasetRoles[roleIndex]
                            layer = self.createStandardLayerFromSlot(slot)
                            layer.name = roleName
                            layers.append(layer)
                    return layers

            opLaneView = self.topLevelOperator.getLane(laneIndex)
            layerViewer = DatasetViewer(opLaneView, crosshair=False)
            
            # Maximize the x-y view by default.
            layerViewer.volumeEditorWidget.quadview.ensureMaximized(2)

            self.volumeEditors[imageSlot] = layerViewer
            self.viewerStack.addWidget( layerViewer )
            self._viewerControlWidgetStack.addWidget( layerViewer.viewerControlWidget() )

        # Show the right one
        viewer = self.volumeEditors[imageSlot]
        displayedRole = self.fileInfoTabWidget.currentIndex()
        viewer.moveToTop(displayedRole)
        self.viewerStack.setCurrentWidget( viewer )
        self._viewerControlWidgetStack.setCurrentWidget( viewer.viewerControlWidget() )


    def handleAddFiles(self, roleIndex):
        self.addFiles(roleIndex)

    def handleReplaceFile(self, roleIndex, startingLane):
        self.addFiles(roleIndex, startingLane)

    def addFiles(self, roleIndex, startingLane=None):
        """
        The user clicked the "Add File" button.
        Ask him to choose a file (or several) and add them to both
          the GUI table and the top-level operator inputs.
        """
        # Find the directory of the most recently opened image file
        mostRecentImageFile = PreferencesManager().get( 'DataSelection', 'recent image' )
        if mostRecentImageFile is not None:
            defaultDirectory = os.path.split(mostRecentImageFile)[0]
        else:
            defaultDirectory = os.path.expanduser('~')

        # Launch the "Open File" dialog
        fileNames = self.getImageFileNamesToOpen(defaultDirectory)

        # If the user didn't cancel
        if len(fileNames) > 0:
            PreferencesManager().set('DataSelection', 'recent image', fileNames[0])
            try:
                self.addFileNames(fileNames, roleIndex, startingLane)
            except RuntimeError as e:
                QMessageBox.critical(self, "Error loading file", str(e))

    def handleAddByPattern(self, roleIndex):
        # Find the most recent directory

        # TODO: remove code duplication
        mostRecentDirectory = PreferencesManager().get( 'DataSelection', 'recent mass directory' )
        if mostRecentDirectory is not None:
            defaultDirectory = os.path.split(mostRecentDirectory)[0]
        else:
            defaultDirectory = os.path.expanduser('~')

        fileNames = self.getMass(defaultDirectory)

        # If the user didn't cancel
        if len(fileNames) > 0:
            PreferencesManager().set('DataSelection', 'recent mass directory', os.path.split(fileNames[0])[0])
            try:
                self.addFileNames(fileNames, roleIndex)
            except RuntimeError as e:
                QMessageBox.critical(self, "Error loading file", str(e))


    def getImageFileNamesToOpen(self, defaultDirectory):
        """
        Launch an "Open File" dialog to ask the user for one or more image files.
        """
        extensions = OpDataSelection.SupportedExtensions
        filt = "Image files (" + ' '.join('*.' + x for x in extensions) + ')'
        options = QFileDialog.Options()
        if ilastik_config.getboolean("ilastik", "debug"):
            options |=  QFileDialog.DontUseNativeDialog
        fileNames = QFileDialog.getOpenFileNames( self, "Select Images", 
                                 defaultDirectory, filt, options=options )
        # Convert from QtString to python str
        fileNames = [str(s) for s in fileNames]
        return fileNames

    def _findFirstEmptyLane(self, roleIndex):
        opTop = self.topLevelOperator
        
        # Determine the number of files this role already has
        # Search for the last valid value.
        firstNewLane = 0
        for laneIndex, slot in reversed(zip(range(len(opTop.DatasetGroup)), opTop.DatasetGroup)):
            if slot[roleIndex].ready():
                firstNewLane = laneIndex+1
                break
        return firstNewLane

    def addFileNames(self, fileNames, roleIndex, startingLane=None):
        """
        Add the given filenames to both the GUI table and the top-level operator inputs.
        If startingLane is None, the filenames will be *appended* to the role's list of files.
        """
        infos = []

        if startingLane is None:        
            startingLane = self._findFirstEmptyLane(roleIndex)
            endingLane = startingLane+len(fileNames)-1
        else:
            assert startingLane < len(self.topLevelOperator.DatasetGroup)
            endingLane = startingLane+len(fileNames)-1

        # Assign values to the new inputs we just allocated.
        # The GUI will be updated by callbacks that are listening to slot changes
        for i, filePath in enumerate(fileNames):
            datasetInfo = DatasetInfo()
            cwd = self.topLevelOperator.WorkingDirectory.value
            
            if not areOnSameDrive(filePath,cwd):
                QMessageBox.critical(self, "Drive Error","Data must be on same drive as working directory.")
                return
                
            absPath, relPath = getPathVariants(filePath, cwd)
            
            # Relative by default, unless the file is in a totally different tree from the working directory.
            if len(os.path.commonprefix([cwd, absPath])) > 1:
                datasetInfo.filePath = relPath
            else:
                datasetInfo.filePath = absPath
                
            datasetInfo.nickname = PathComponents(absPath).filenameBase

            h5Exts = ['.ilp', '.h5', '.hdf5']
            if os.path.splitext(datasetInfo.filePath)[1] in h5Exts:
                datasetNames = self.getPossibleInternalPaths( absPath )
                if len(datasetNames) > 0:
                    datasetInfo.filePath += str(datasetNames[0])
                else:
                    raise RuntimeError("HDF5 file %s has no image datasets" % datasetInfo.filePath)

            # Allow labels by default if this gui isn't being used for batch data.
            datasetInfo.allowLabels = ( self.guiMode == GuiMode.Normal )
            infos.append(datasetInfo)

        # if no exception was thrown, set up the operator now
        opTop = self.topLevelOperator
        originalSize = len(opTop.DatasetGroup)
            
        if len( opTop.DatasetGroup ) < endingLane+1:
            opTop.DatasetGroup.resize( endingLane+1 )
        for laneIndex, info in zip(range(startingLane, endingLane+1), infos):
            try:
                self.topLevelOperator.DatasetGroup[laneIndex][roleIndex].setValue( info )
            except DatasetConstraintError as ex:
                # Give the user a chance to fix the problem
                if not self.handleDatasetConstraintError(info, info.filePath, ex, roleIndex, laneIndex):
                    opTop.DatasetGroup.resize( originalSize )
                    break
            except:
                QMessageBox.critical( self, "Dataset Load Error", "Wasn't able to load your dataset into the workflow.  See console for details." )
                opTop.DatasetGroup.resize( originalSize )
                raise
        self.updateInternalPathVisiblity()

    @threadRouted
    def handleDatasetConstraintError(self, info, filename, ex, roleIndex, laneIndex):
        msg = "Can't use default properties for dataset:\n\n" + \
              filename + "\n\n" + \
              "because it violates a constraint of the {} applet.\n\n".format( ex.appletName ) + \
              ex.message + "\n\n" + \
              "Please enter valid dataset properties to continue."
        QMessageBox.warning( self, "Dataset Needs Correction", msg )
        
        return self.repairDatasetInfo( info, roleIndex, laneIndex )

    def repairDatasetInfo(self, info, roleIndex, laneIndex):
        defaultInfos = {}
        defaultInfos[laneIndex] = info
        editorDlg = DatasetInfoEditorWidget(self, self.topLevelOperator, roleIndex, [laneIndex], defaultInfos)
        return ( editorDlg.exec_() == QDialog.Accepted )

    def getPossibleInternalPaths(self, absPath):
        datasetNames = []
        # Open the file as a read-only so we can get a list of the internal paths
        with h5py.File(absPath, 'r') as f:
            # Define a closure to collect all of the dataset names in the file.
            def accumulateDatasetPaths(name, val):
                if type(val) == h5py._hl.dataset.Dataset and 3 <= len(val.shape) <= 5:
                    datasetNames.append( '/' + name )
            # Visit every group/dataset in the file
            f.visititems(accumulateDatasetPaths)
        return datasetNames

    def handleAddStack(self, roleIndex):
        self.replaceWithStack(roleIndex, laneIndex=None)

    def replaceWithStack(self, roleIndex, laneIndex):
        """
        The user clicked the "Import Stack Files" button.
        """
        stackDlg = StackFileSelectionWidget(self)
        stackDlg.exec_()
        if stackDlg.result() != QDialog.Accepted :
            return
        files = stackDlg.selectedFiles
        if len(files) == 0:
            return

        info = DatasetInfo()
        info.filePath = "//".join( files )
        prefix = os.path.commonprefix(files)
        info.nickname = PathComponents(prefix).filenameBase + "..."

        # Allow labels by default if this gui isn't being used for batch data.
        info.allowLabels = ( self.guiMode == GuiMode.Normal )
        info.fromstack = True

        originalNumLanes = len(self.topLevelOperator.DatasetGroup)

        if laneIndex is None:
            laneIndex = self._findFirstEmptyLane(roleIndex)
        if len(self.topLevelOperator.DatasetGroup) < laneIndex+1:
            self.topLevelOperator.DatasetGroup.resize(laneIndex+1)

        def importStack():
            self.guiControlSignal.emit( ControlCommand.DisableAll )
            # Serializer will update the operator for us, which will propagate to the GUI.
            try:
                self.serializer.importStackAsLocalDataset( info )
                try:
                    self.topLevelOperator.DatasetGroup[laneIndex][roleIndex].setValue(info)
                except DatasetConstraintError as ex:
                    # Give the user a chance to repair the problem.
                    filename = files[0] + "\n...\n" + files[-1]
                    if not self.handleDatasetConstraintError( info, filename, ex, roleIndex, laneIndex ):
                        self.topLevelOperator.DatasetGroup.resize(originalNumLanes)
            finally:
                self.guiControlSignal.emit( ControlCommand.Pop )

        req = Request( importStack )
        req.notify_failed( partial(self.handleFailedStackLoad, files, originalNumLanes ) )
        req.submit()

    @threadRouted
    def handleFailedStackLoad(self, files, originalNumLanes, exc, exc_info):
        import traceback
        traceback.print_tb(exc_info[2])
        msg = "Failed to load stack due to the following error:\n{}".format( exc )
        msg += "Attempted stack files were:"
        for f in files:
            msg += f + "\n"
        QMessageBox.critical(self, "Failed to load image stack", msg)
        self.topLevelOperator.DatasetGroup.resize(originalNumLanes)

    def getMass(self, defaultDirectory):
        # TODO: launch dialog and get files

        # Convert from QtString to python str
        loader = MassFileLoader(defaultDirectory=defaultDirectory)
        loader.exec_()
        if loader.result() == QDialog.Accepted:
            fileNames = [str(s) for s in loader.filenames]
        else:
            fileNames = []
        return fileNames

    def handleClearDatasets(self, roleIndex, selectedRows):
        for row in selectedRows:
            self.topLevelOperator.DatasetGroup[row][roleIndex].disconnect()

        # Remove all operators that no longer have any connected slots        
        last_valid = -1
        laneIndexes = range( len(self.topLevelOperator.DatasetGroup) )
        for laneIndex, multislot in reversed(zip(laneIndexes, self.topLevelOperator.DatasetGroup)):
            any_ready = False
            for slot in multislot:
                any_ready |= slot.ready()
            if not any_ready:
                self.topLevelOperator.DatasetGroup.removeSlot( laneIndex, len(self.topLevelOperator.DatasetGroup)-1 )

    def editDatasetInfo(self, roleIndex, laneIndexes):
        editorDlg = DatasetInfoEditorWidget(self, self.topLevelOperator, roleIndex, laneIndexes)
        editorDlg.exec_()

    def updateInternalPathVisiblity(self):
        for widget in self._detailViewerWidgets:
            view = widget.datasetDetailTableView
            model = view.model()
            view.setColumnHidden(DatasetDetailedInfoColumn.InternalID,
                                 not model.hasInternalPaths())
Example #9
0
class DataSelectionGui(QWidget):
    """
    Manages all GUI elements in the data selection applet.
    This class itself is the central widget and also owns/manages the applet drawer widgets.
    """

    ###########################################
    ### AppletGuiInterface Concrete Methods ###
    ###########################################

    def centralWidget(self):
        return self

    def appletDrawer(self):
        return self._drawer

    def menus(self):
        return []

    def viewerControlWidget(self):
        return self._viewerControlWidgetStack

    def setImageIndex(self, imageIndex):
        if imageIndex is not None:
            self.laneSummaryTableView.selectRow(imageIndex)
            for detailWidget in self._detailViewerWidgets:
                detailWidget.datasetDetailTableView.selectRow(imageIndex)

    def stopAndCleanUp(self):
        for editor in self.volumeEditors.values():
            self.viewerStack.removeWidget(editor)
            self._viewerControlWidgetStack.removeWidget(
                editor.viewerControlWidget())
            editor.stopAndCleanUp()
        self.volumeEditors.clear()

    def imageLaneAdded(self, laneIndex):
        if len(self.laneSummaryTableView.selectedIndexes()) == 0:
            self.laneSummaryTableView.selectRow(laneIndex)

        # We don't have any real work to do because this gui initiated the lane addition in the first place
        if self.guiMode != GuiMode.Batch:
            if (len(self.topLevelOperator.DatasetGroup) != laneIndex + 1):
                import warnings
                warnings.warn(
                    "DataSelectionGui.imageLaneAdded(): length of dataset multislot out of sync with laneindex [%s != %s + 1]"
                    % (len(self.topLevelOperator.Dataset), laneIndex))

    def imageLaneRemoved(self, laneIndex, finalLength):
        # We assume that there's nothing to do here because THIS GUI initiated the lane removal
        if self.guiMode != GuiMode.Batch:
            assert len(self.topLevelOperator.DatasetGroup) == finalLength

    ###########################################
    ###########################################

    def __init__(self,
                 dataSelectionOperator,
                 serializer,
                 guiControlSignal,
                 instructionText,
                 guiMode=GuiMode.Normal,
                 max_lanes=None):
        """
        Constructor.
        
        :param dataSelectionOperator: The top-level operator.  Must be of type :py:class:`OpMultiLaneDataSelectionGroup`.
        :param serializer: The applet's serializer.  Must be of type :py:class:`DataSelectionSerializer`
        :param instructionText: A string to display in the applet drawer.
        :param guiMode: Either ``GuiMode.Normal`` or ``GuiMode.Batch``.  Currently, there is no difference between normal and batch mode.
        :param max_lanes: The maximum number of lanes that the user is permitted to add to this workflow.  If ``None``, there is no maximum.
        """
        super(DataSelectionGui, self).__init__()

        self._max_lanes = max_lanes

        self._viewerControls = QWidget()
        self.topLevelOperator = dataSelectionOperator
        self.guiMode = guiMode
        self.serializer = serializer
        self.guiControlSignal = guiControlSignal
        self.threadRouter = ThreadRouter(self)

        self._initCentralUic()
        self._initAppletDrawerUic(instructionText)

        self._viewerControlWidgetStack = QStackedWidget(self)

        def handleImageRemoved(multislot, index, finalLength):
            # Remove the viewer for this dataset
            imageSlot = self.topLevelOperator.Image[index]
            if imageSlot in self.volumeEditors.keys():
                editor = self.volumeEditors[imageSlot]
                self.viewerStack.removeWidget(editor)
                self._viewerControlWidgetStack.removeWidget(
                    editor.viewerControlWidget())
                editor.stopAndCleanUp()

        self.topLevelOperator.Image.notifyRemove(bind(handleImageRemoved))

    def _initCentralUic(self):
        """
        Load the GUI from the ui file into this class and connect it with event handlers.
        """
        # Load the ui file into this class (find it in our own directory)
        localDir = os.path.split(__file__)[0] + '/'
        uic.loadUi(localDir + "/dataSelection.ui", self)

        self._initTableViews()
        self._initViewerStack()
        self.splitter.setSizes([150, 850])

    def _initAppletDrawerUic(self, instructionText):
        """
        Load the ui file for the applet drawer, which we own.
        """
        localDir = os.path.split(__file__)[0] + '/'
        self._drawer = uic.loadUi(localDir + "/dataSelectionDrawer.ui")
        self._drawer.instructionLabel.setText(instructionText)

    def _initTableViews(self):
        self.fileInfoTabWidget.setTabText(0, "Summary")
        self.laneSummaryTableView.setModel(
            DataLaneSummaryTableModel(self, self.topLevelOperator))
        self.laneSummaryTableView.dataLaneSelected.connect(self.showDataset)
        self.laneSummaryTableView.addFilesRequested.connect(
            self.handleAddFiles)
        self.laneSummaryTableView.addStackRequested.connect(
            self.handleAddStack)
        self.laneSummaryTableView.addByPatternRequested.connect(
            self.handleAddByPattern)
        self.removeLaneButton.clicked.connect(
            self.handleRemoveLaneButtonClicked)
        self.laneSummaryTableView.removeLanesRequested.connect(
            self.handleRemoveLaneButtonClicked)

        # These two helper functions enable/disable an 'add files' button for a given role
        #  based on the the max lane index for that role and the overall permitted max_lanes
        def _update_button_status(button, role_index):
            if self._max_lanes:
                button.setEnabled(
                    self._findFirstEmptyLane(role_index) < self._max_lanes)

        def _handle_lane_added(button, role_index, slot, lane_index):
            slot[lane_index][role_index].notifyReady(
                bind(_update_button_status, button, role_index))
            slot[lane_index][role_index].notifyUnready(
                bind(_update_button_status, button, role_index))

        self._retained = []  # Retain menus so they don't get deleted
        self._detailViewerWidgets = []
        for roleIndex, role in enumerate(
                self.topLevelOperator.DatasetRoles.value):
            detailViewer = DataDetailViewerWidget(self, self.topLevelOperator,
                                                  roleIndex)
            self._detailViewerWidgets.append(detailViewer)

            # Button
            menu = QMenu(parent=self)
            menu.setObjectName("addFileButton_role_{}".format(roleIndex))
            menu.addAction("Add File(s)...").triggered.connect(
                partial(self.handleAddFiles, roleIndex))
            menu.addAction("Add Volume from Stack...").triggered.connect(
                partial(self.handleAddStack, roleIndex))
            menu.addAction("Add Many by Pattern...").triggered.connect(
                partial(self.handleAddByPattern, roleIndex))
            detailViewer.appendButton.setMenu(menu)
            self._retained.append(menu)

            # Monitor changes to each lane so we can enable/disable the 'add lanes' button for each tab
            self.topLevelOperator.DatasetGroup.notifyInserted(
                bind(_handle_lane_added, detailViewer.appendButton, roleIndex))
            self.topLevelOperator.DatasetGroup.notifyRemoved(
                bind(_update_button_status, detailViewer.appendButton,
                     roleIndex))

            # While we're at it, do the same for the buttons in the summary table, too
            self.topLevelOperator.DatasetGroup.notifyInserted(
                bind(_handle_lane_added,
                     self.laneSummaryTableView.addFilesButtons[roleIndex],
                     roleIndex))
            self.topLevelOperator.DatasetGroup.notifyRemoved(
                bind(_update_button_status,
                     self.laneSummaryTableView.addFilesButtons[roleIndex],
                     roleIndex))

            # Context menu
            detailViewer.datasetDetailTableView.replaceWithFileRequested.connect(
                partial(self.handleReplaceFile, roleIndex))
            detailViewer.datasetDetailTableView.replaceWithStackRequested.connect(
                partial(self.replaceWithStack, roleIndex))
            detailViewer.datasetDetailTableView.editRequested.connect(
                partial(self.editDatasetInfo, roleIndex))
            detailViewer.datasetDetailTableView.resetRequested.connect(
                partial(self.handleClearDatasets, roleIndex))

            # Drag-and-drop
            detailViewer.datasetDetailTableView.addFilesRequested.connect(
                partial(self.addFileNames, roleIndex=roleIndex))

            # Selection handling
            def showFirstSelectedDataset(_roleIndex, lanes):
                if lanes:
                    self.showDataset(lanes[0], _roleIndex)

            detailViewer.datasetDetailTableView.dataLaneSelected.connect(
                partial(showFirstSelectedDataset, roleIndex))

            self.fileInfoTabWidget.insertTab(roleIndex, detailViewer, role)

        self.fileInfoTabWidget.currentChanged.connect(self.handleSwitchTabs)
        self.fileInfoTabWidget.setCurrentIndex(0)

    def handleSwitchTabs(self, tabIndex):
        if tabIndex < len(self._detailViewerWidgets):
            roleIndex = tabIndex  # If summary tab is moved to the front, change this line.
            detailViewer = self._detailViewerWidgets[roleIndex]
            selectedLanes = detailViewer.datasetDetailTableView.selectedLanes
            if selectedLanes:
                self.showDataset(selectedLanes[0], roleIndex)

    def _initViewerStack(self):
        self.volumeEditors = {}
        self.viewerStack.addWidget(QWidget())

    def handleRemoveLaneButtonClicked(self):
        """
        The user clicked the "Remove" button.
        Remove the currently selected row(s) from both the GUI and the top-level operator.
        """
        # Figure out which lanes to remove
        selectedIndexes = self.laneSummaryTableView.selectedIndexes()
        rows = set()
        for modelIndex in selectedIndexes:
            rows.add(modelIndex.row())

        # Don't remove the last row, which is just buttons.
        rows.discard(self.laneSummaryTableView.model().rowCount() - 1)

        # Remove in reverse order so row numbers remain consistent
        for row in reversed(sorted(rows)):
            # Remove from the GUI
            self.laneSummaryTableView.model().removeRow(row)
            # Remove from the operator
            finalSize = len(self.topLevelOperator.DatasetGroup) - 1
            self.topLevelOperator.DatasetGroup.removeSlot(row, finalSize)

            # The gui and the operator should be in sync (model has one extra row for the button row)
            assert self.laneSummaryTableView.model().rowCount() == len(
                self.topLevelOperator.DatasetGroup) + 1

    def showDataset(self, laneIndex, roleIndex=None):
        if laneIndex == -1:
            self.viewerStack.setCurrentIndex(0)
            return

        assert threading.current_thread().name == "MainThread"
        imageSlot = self.topLevelOperator.Image[laneIndex]

        # Create if necessary
        if imageSlot not in self.volumeEditors.keys():

            class DatasetViewer(LayerViewerGui):
                def moveToTop(self, roleIndex):
                    opLaneView = self.topLevelOperatorView
                    if not opLaneView.DatasetRoles.ready():
                        return
                    datasetRoles = opLaneView.DatasetRoles.value
                    if roleIndex >= len(datasetRoles):
                        return
                    roleName = datasetRoles[roleIndex]
                    try:
                        layerIndex = [l.name
                                      for l in self.layerstack].index(roleName)
                    except ValueError:
                        return
                    else:
                        self.layerstack.selectRow(layerIndex)
                        self.layerstack.moveSelectedToTop()

                def setupLayers(self):
                    opLaneView = self.topLevelOperatorView
                    if not opLaneView.DatasetRoles.ready():
                        return []
                    layers = []
                    datasetRoles = opLaneView.DatasetRoles.value
                    for roleIndex, slot in enumerate(opLaneView.ImageGroup):
                        if slot.ready():
                            roleName = datasetRoles[roleIndex]
                            layer = self.createStandardLayerFromSlot(slot)
                            layer.name = roleName
                            layers.append(layer)
                    return layers

            opLaneView = self.topLevelOperator.getLane(laneIndex)
            layerViewer = DatasetViewer(opLaneView, crosshair=False)

            # Maximize the x-y view by default.
            layerViewer.volumeEditorWidget.quadview.ensureMaximized(2)

            self.volumeEditors[imageSlot] = layerViewer
            self.viewerStack.addWidget(layerViewer)
            self._viewerControlWidgetStack.addWidget(
                layerViewer.viewerControlWidget())

        # Show the right one
        viewer = self.volumeEditors[imageSlot]
        displayedRole = self.fileInfoTabWidget.currentIndex()
        viewer.moveToTop(displayedRole)
        self.viewerStack.setCurrentWidget(viewer)
        self._viewerControlWidgetStack.setCurrentWidget(
            viewer.viewerControlWidget())

    def handleAddFiles(self, roleIndex):
        self.addFiles(roleIndex)

    def handleReplaceFile(self, roleIndex, startingLane):
        self.addFiles(roleIndex, startingLane)

    def addFiles(self, roleIndex, startingLane=None):
        """
        The user clicked the "Add File" button.
        Ask him to choose a file (or several) and add them to both
          the GUI table and the top-level operator inputs.
        """
        # Find the directory of the most recently opened image file
        mostRecentImageFile = PreferencesManager().get('DataSelection',
                                                       'recent image')
        if mostRecentImageFile is not None:
            defaultDirectory = os.path.split(mostRecentImageFile)[0]
        else:
            defaultDirectory = os.path.expanduser('~')

        # Launch the "Open File" dialog
        fileNames = self.getImageFileNamesToOpen(defaultDirectory)

        # If the user didn't cancel
        if len(fileNames) > 0:
            PreferencesManager().set('DataSelection', 'recent image',
                                     fileNames[0])
            try:
                self.addFileNames(fileNames, roleIndex, startingLane)
            except RuntimeError as e:
                QMessageBox.critical(self, "Error loading file", str(e))

    def handleAddByPattern(self, roleIndex):
        # Find the most recent directory

        # TODO: remove code duplication
        mostRecentDirectory = PreferencesManager().get(
            'DataSelection', 'recent mass directory')
        if mostRecentDirectory is not None:
            defaultDirectory = os.path.split(mostRecentDirectory)[0]
        else:
            defaultDirectory = os.path.expanduser('~')

        fileNames = self.getMass(defaultDirectory)

        # If the user didn't cancel
        if len(fileNames) > 0:
            PreferencesManager().set('DataSelection', 'recent mass directory',
                                     os.path.split(fileNames[0])[0])
            try:
                self.addFileNames(fileNames, roleIndex)
            except RuntimeError as e:
                QMessageBox.critical(self, "Error loading file", str(e))

    def getImageFileNamesToOpen(self, defaultDirectory):
        """
        Launch an "Open File" dialog to ask the user for one or more image files.
        """
        extensions = OpDataSelection.SupportedExtensions
        filt = "Image files (" + ' '.join('*.' + x for x in extensions) + ')'
        options = QFileDialog.Options()
        if ilastik_config.getboolean("ilastik", "debug"):
            options |= QFileDialog.DontUseNativeDialog
        fileNames = QFileDialog.getOpenFileNames(self,
                                                 "Select Images",
                                                 defaultDirectory,
                                                 filt,
                                                 options=options)
        # Convert from QtString to python str
        fileNames = [str(s) for s in fileNames]
        return fileNames

    def _findFirstEmptyLane(self, roleIndex):
        opTop = self.topLevelOperator

        # Determine the number of files this role already has
        # Search for the last valid value.
        firstNewLane = 0
        for laneIndex, slot in reversed(
                zip(range(len(opTop.DatasetGroup)), opTop.DatasetGroup)):
            if slot[roleIndex].ready():
                firstNewLane = laneIndex + 1
                break
        return firstNewLane

    def addFileNames(self, fileNames, roleIndex, startingLane=None):
        """
        Add the given filenames to both the GUI table and the top-level operator inputs.
        If startingLane is None, the filenames will be *appended* to the role's list of files.
        """
        infos = []

        if startingLane is None:
            startingLane = self._findFirstEmptyLane(roleIndex)
            endingLane = startingLane + len(fileNames) - 1
        else:
            assert startingLane < len(self.topLevelOperator.DatasetGroup)
            endingLane = startingLane + len(fileNames) - 1

        if self._max_lanes and endingLane >= self._max_lanes:
            msg = "You may not add more than {} file(s) to this workflow.  Please try again.".format(
                self._max_lanes)
            QMessageBox.critical(self, "Too many files", msg)
            return

        # Assign values to the new inputs we just allocated.
        # The GUI will be updated by callbacks that are listening to slot changes
        for i, filePath in enumerate(fileNames):
            datasetInfo = DatasetInfo()
            cwd = self.topLevelOperator.WorkingDirectory.value

            if not areOnSameDrive(filePath, cwd):
                QMessageBox.critical(
                    self, "Drive Error",
                    "Data must be on same drive as working directory.")
                return

            absPath, relPath = getPathVariants(filePath, cwd)

            # Relative by default, unless the file is in a totally different tree from the working directory.
            if len(os.path.commonprefix([cwd, absPath])) > 1:
                datasetInfo.filePath = relPath
            else:
                datasetInfo.filePath = absPath

            datasetInfo.nickname = PathComponents(absPath).filenameBase

            h5Exts = ['.ilp', '.h5', '.hdf5']
            if os.path.splitext(datasetInfo.filePath)[1] in h5Exts:
                datasetNames = self.getPossibleInternalPaths(absPath)
                if len(datasetNames) > 0:
                    datasetInfo.filePath += str(datasetNames[0])
                else:
                    raise RuntimeError("HDF5 file %s has no image datasets" %
                                       datasetInfo.filePath)

            # Allow labels by default if this gui isn't being used for batch data.
            datasetInfo.allowLabels = (self.guiMode == GuiMode.Normal)
            infos.append(datasetInfo)

        # if no exception was thrown, set up the operator now
        opTop = self.topLevelOperator
        originalSize = len(opTop.DatasetGroup)

        if len(opTop.DatasetGroup) < endingLane + 1:
            opTop.DatasetGroup.resize(endingLane + 1)
        for laneIndex, info in zip(range(startingLane, endingLane + 1), infos):
            try:
                self.topLevelOperator.DatasetGroup[laneIndex][
                    roleIndex].setValue(info)
            except DatasetConstraintError as ex:
                return_val = [False]
                # Give the user a chance to fix the problem
                self.handleDatasetConstraintError(info, info.filePath, ex,
                                                  roleIndex, laneIndex,
                                                  return_val)
                if not return_val[0]:
                    # Not successfully repaired.  Roll back the changes and give up.
                    opTop.DatasetGroup.resize(originalSize)
                    break
            except OpDataSelection.InvalidDimensionalityError as ex:
                opTop.DatasetGroup.resize(originalSize)
                QMessageBox.critical(self,
                                     "Dataset has different dimensionality",
                                     ex.message)
                break
            except:
                QMessageBox.critical(
                    self, "Dataset Load Error",
                    "Wasn't able to load your dataset into the workflow.  See console for details."
                )
                opTop.DatasetGroup.resize(originalSize)
                raise
        self.updateInternalPathVisiblity()

    @threadRouted
    def handleDatasetConstraintError(self,
                                     info,
                                     filename,
                                     ex,
                                     roleIndex,
                                     laneIndex,
                                     return_val=[False]):
        msg = "Can't use default properties for dataset:\n\n" + \
              filename + "\n\n" + \
              "because it violates a constraint of the {} applet.\n\n".format( ex.appletName ) + \
              ex.message + "\n\n" + \
              "Please enter valid dataset properties to continue."
        QMessageBox.warning(self, "Dataset Needs Correction", msg)

        # The success of this is 'returned' via our special out-param
        # (We can't return a value from this func because it is @threadRouted.
        successfully_repaired = self.repairDatasetInfo(info, roleIndex,
                                                       laneIndex)
        return_val[0] = successfully_repaired

    def repairDatasetInfo(self, info, roleIndex, laneIndex):
        """Open the dataset properties editor and return True if the new properties are acceptable."""
        defaultInfos = {}
        defaultInfos[laneIndex] = info
        editorDlg = DatasetInfoEditorWidget(self, self.topLevelOperator,
                                            roleIndex, [laneIndex],
                                            defaultInfos)
        dlg_state = editorDlg.exec_()
        return (dlg_state == QDialog.Accepted)

    def getPossibleInternalPaths(self, absPath):
        datasetNames = []
        # Open the file as a read-only so we can get a list of the internal paths
        with h5py.File(absPath, 'r') as f:
            # Define a closure to collect all of the dataset names in the file.
            def accumulateDatasetPaths(name, val):
                if type(val) == h5py._hl.dataset.Dataset and 3 <= len(
                        val.shape) <= 5:
                    datasetNames.append('/' + name)

            # Visit every group/dataset in the file
            f.visititems(accumulateDatasetPaths)
        return datasetNames

    def handleAddStack(self, roleIndex):
        self.replaceWithStack(roleIndex, laneIndex=None)

    def replaceWithStack(self, roleIndex, laneIndex):
        """
        The user clicked the "Import Stack Files" button.
        """
        stackDlg = StackFileSelectionWidget(self)
        stackDlg.exec_()
        if stackDlg.result() != QDialog.Accepted:
            return
        files = stackDlg.selectedFiles
        if len(files) == 0:
            return

        info = DatasetInfo()
        info.filePath = "//".join(files)
        prefix = os.path.commonprefix(files)
        info.nickname = PathComponents(prefix).filenameBase
        # Add an underscore for each wildcard digit
        num_wildcards = len(files[-1]) - len(prefix) - len(
            os.path.splitext(files[-1])[1])
        info.nickname += "_" * num_wildcards

        # Allow labels by default if this gui isn't being used for batch data.
        info.allowLabels = (self.guiMode == GuiMode.Normal)
        info.fromstack = True

        originalNumLanes = len(self.topLevelOperator.DatasetGroup)

        if laneIndex is None:
            laneIndex = self._findFirstEmptyLane(roleIndex)
        if len(self.topLevelOperator.DatasetGroup) < laneIndex + 1:
            self.topLevelOperator.DatasetGroup.resize(laneIndex + 1)

        def importStack():
            self.guiControlSignal.emit(ControlCommand.DisableAll)
            # Serializer will update the operator for us, which will propagate to the GUI.
            try:
                self.serializer.importStackAsLocalDataset(info)
                try:
                    self.topLevelOperator.DatasetGroup[laneIndex][
                        roleIndex].setValue(info)
                except DatasetConstraintError as ex:
                    # Give the user a chance to repair the problem.
                    filename = files[0] + "\n...\n" + files[-1]
                    return_val = [False]
                    self.handleDatasetConstraintError(info, filename, ex,
                                                      roleIndex, laneIndex,
                                                      return_val)
                    if not return_val[0]:
                        # Not successfully repaired.  Roll back the changes and give up.
                        self.topLevelOperator.DatasetGroup.resize(
                            originalNumLanes)
            finally:
                self.guiControlSignal.emit(ControlCommand.Pop)

        req = Request(importStack)
        req.notify_failed(
            partial(self.handleFailedStackLoad, files, originalNumLanes))
        req.submit()

    @threadRouted
    def handleFailedStackLoad(self, files, originalNumLanes, exc, exc_info):
        import traceback
        traceback.print_tb(exc_info[2])
        msg = "Failed to load stack due to the following error:\n{}".format(
            exc)
        msg += "Attempted stack files were:"
        for f in files:
            msg += f + "\n"
        QMessageBox.critical(self, "Failed to load image stack", msg)
        self.topLevelOperator.DatasetGroup.resize(originalNumLanes)

    def getMass(self, defaultDirectory):
        # TODO: launch dialog and get files

        # Convert from QtString to python str
        loader = MassFileLoader(defaultDirectory=defaultDirectory)
        loader.exec_()
        if loader.result() == QDialog.Accepted:
            fileNames = [str(s) for s in loader.filenames]
        else:
            fileNames = []
        return fileNames

    def handleClearDatasets(self, roleIndex, selectedRows):
        for row in selectedRows:
            self.topLevelOperator.DatasetGroup[row][roleIndex].disconnect()

        # Remove all operators that no longer have any connected slots
        last_valid = -1
        laneIndexes = range(len(self.topLevelOperator.DatasetGroup))
        for laneIndex, multislot in reversed(
                zip(laneIndexes, self.topLevelOperator.DatasetGroup)):
            any_ready = False
            for slot in multislot:
                any_ready |= slot.ready()
            if not any_ready:
                self.topLevelOperator.DatasetGroup.removeSlot(
                    laneIndex,
                    len(self.topLevelOperator.DatasetGroup) - 1)

    def editDatasetInfo(self, roleIndex, laneIndexes):
        editorDlg = DatasetInfoEditorWidget(self, self.topLevelOperator,
                                            roleIndex, laneIndexes)
        editorDlg.exec_()

    def updateInternalPathVisiblity(self):
        for widget in self._detailViewerWidgets:
            view = widget.datasetDetailTableView
            model = view.model()
            view.setColumnHidden(DatasetDetailedInfoColumn.InternalID,
                                 not model.hasInternalPaths())
Example #10
0
class SimulationPanel(QWidget):
    def __init__(self):
        QWidget.__init__(self)

        layout = QVBoxLayout()

        simulation_mode_layout = QHBoxLayout()
        simulation_mode_layout.addSpacing(10)
        simulation_mode_model = SimulationModeModel()
        simulation_mode_model.observable().attach(SimulationModeModel.CURRENT_CHOICE_CHANGED_EVENT, self.toggleSimulationMode)
        simulation_mode_combo = ComboChoice(simulation_mode_model, "Simulation mode", "run/simulation_mode")
        simulation_mode_layout.addWidget(QLabel(simulation_mode_combo.getLabel()), 0, Qt.AlignVCenter)
        simulation_mode_layout.addWidget(simulation_mode_combo, 0, Qt.AlignVCenter)

        # simulation_mode_layout.addStretch()
        simulation_mode_layout.addSpacing(20)

        self.run_button = QToolButton()
        self.run_button.setIconSize(QSize(32, 32))
        self.run_button.setText("Start Simulation")
        self.run_button.setIcon(util.resourceIcon("ide/gear_in_play"))
        self.run_button.clicked.connect(self.runSimulation)
        self.run_button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
        HelpedWidget.addHelpToWidget(self.run_button, "run/start_simulation")

        simulation_mode_layout.addWidget(self.run_button)
        simulation_mode_layout.addStretch(1)

        layout.addSpacing(5)
        layout.addLayout(simulation_mode_layout)
        layout.addSpacing(10)

        self.simulation_stack = QStackedWidget()
        self.simulation_stack.setLineWidth(1)
        self.simulation_stack.setFrameStyle(QFrame.StyledPanel)

        layout.addWidget(self.simulation_stack)

        self.simulation_widgets = {}

        self.addSimulationConfigPanel(EnsembleExperimentPanel())
        self.addSimulationConfigPanel(EnsembleSmootherPanel())
        self.addSimulationConfigPanel(IteratedEnsembleSmootherPanel())

        self.setLayout(layout)


    def addSimulationConfigPanel(self, panel):
        assert isinstance(panel, SimulationConfigPanel)

        panel.toggleAdvancedOptions(False)
        self.simulation_stack.addWidget(panel)
        self.simulation_widgets[panel.getSimulationModel()] = panel

        panel.simulationConfigurationChanged.connect(self.validationStatusChanged)


    def getActions(self):
        """ @rtype: list of QAction """
        advanced_toggle_action = QAction("Show Advanced Options", self)
        advanced_toggle_action.setObjectName("AdvancedSimulationOptions")
        advanced_toggle_action.setCheckable(True)
        advanced_toggle_action.setChecked(False)
        advanced_toggle_action.toggled.connect(self.toggleAdvanced)

        return [advanced_toggle_action]


    def toggleAdvanced(self, show_advanced):
        for panel in self.simulation_widgets.values():
            panel.toggleAdvancedOptions(show_advanced)


    def getCurrentSimulationMode(self):
        return SimulationModeModel().getCurrentChoice()


    def runSimulation(self):
        run_model = self.getCurrentSimulationMode()

        dialog = RunDialog(run_model)
        dialog.startSimulation()
        dialog.exec_()

        CaseList().externalModificationNotification() # simulations may have added new cases.


    def toggleSimulationMode(self):
        widget = self.simulation_widgets[self.getCurrentSimulationMode()]
        self.simulation_stack.setCurrentWidget(widget)
        self.validationStatusChanged()


    def validationStatusChanged(self):
        widget = self.simulation_widgets[self.getCurrentSimulationMode()]
        self.run_button.setEnabled(widget.isConfigurationValid())
Example #11
0
class SideBar(QWidget):
    """ Sidebar with a widget area which is hidden or shown.
        On by clicking any tab, off by clicking the current tab.
    """

    North = 0
    East = 1
    South = 2
    West = 3

    def __init__(self, orientation=2, parent=None):
        QWidget.__init__(self, parent)

        self.__tabBar = QTabBar()
        self.__tabBar.setDrawBase(True)
        self.__tabBar.setShape(QTabBar.RoundedNorth)
        self.__tabBar.setFocusPolicy(Qt.NoFocus)
        self.__tabBar.setUsesScrollButtons(True)
        self.__tabBar.setElideMode(1)
        self.__stackedWidget = QStackedWidget(self)
        self.__stackedWidget.setContentsMargins(0, 0, 0, 0)
        self.barLayout = QBoxLayout(QBoxLayout.LeftToRight)
        self.barLayout.setMargin(0)
        self.layout = QBoxLayout(QBoxLayout.TopToBottom)
        self.layout.setMargin(0)
        self.layout.setSpacing(0)
        self.barLayout.addWidget(self.__tabBar)
        self.layout.addLayout(self.barLayout)
        self.layout.addWidget(self.__stackedWidget)
        self.setLayout(self.layout)

        self.__minimized = False
        self.__minSize = 0
        self.__maxSize = 0
        self.__bigSize = QSize()

        self.splitter = None

        self.__tabBar.installEventFilter(self)

        self.__orientation = orientation
        self.setOrientation(orientation)

        self.__tabBar.currentChanged.connect(
            self.__stackedWidget.setCurrentIndex)
        return

    def setSplitter(self, splitter):
        """ Set the splitter managing the sidebar """
        self.splitter = splitter
        return

    def __getIndex(self):
        " Provides the widget index in splitters "
        if self.__orientation == SideBar.West:
            return 0
        if self.__orientation == SideBar.East:
            return 2
        if self.__orientation == SideBar.South:
            return 1
        return 0

    def __getWidget(self):
        " Provides a reference to the widget "
        return self.splitter.widget(self.__getIndex())

    def shrink(self):
        """ Shrink the sidebar """
        if self.__minimized:
            return

        self.__minimized = True
        self.__bigSize = self.size()
        if self.__orientation in [SideBar.North, SideBar.South]:
            self.__minSize = self.minimumHeight()
            self.__maxSize = self.maximumHeight()
        else:
            self.__minSize = self.minimumWidth()
            self.__maxSize = self.maximumWidth()

        self.__stackedWidget.hide()

        sizes = self.splitter.sizes()
        selfIndex = self.__getIndex()

        if self.__orientation in [SideBar.North, SideBar.South]:
            newHeight = self.__tabBar.minimumSizeHint().height()
            self.setFixedHeight(newHeight)

            diff = sizes[selfIndex] - newHeight
            sizes[selfIndex] = newHeight
        else:
            newWidth = self.__tabBar.minimumSizeHint().width()
            self.setFixedWidth(newWidth)

            diff = sizes[selfIndex] - newWidth
            sizes[selfIndex] = newWidth

        if selfIndex == 0:
            sizes[1] += diff
        else:
            sizes[selfIndex - 1] += diff

        self.splitter.setSizes(sizes)
        return

    def expand(self):
        """ Expand the sidebar """
        if not self.__minimized:
            return

        self.__minimized = False
        self.__stackedWidget.show()
        self.resize(self.__bigSize)

        sizes = self.splitter.sizes()
        selfIndex = self.__getIndex()

        if self.__orientation in [SideBar.North, SideBar.South]:
            self.setMinimumHeight(self.__minSize)
            self.setMaximumHeight(self.__maxSize)

            diff = self.__bigSize.height() - sizes[selfIndex]
            sizes[selfIndex] = self.__bigSize.height()
        else:
            self.setMinimumWidth(self.__minSize)
            self.setMaximumWidth(self.__maxSize)

            diff = self.__bigSize.width() - sizes[selfIndex]
            sizes[selfIndex] = self.__bigSize.width()

        if selfIndex == 0:
            sizes[1] -= diff
        else:
            sizes[selfIndex - 1] -= diff

        self.splitter.setSizes(sizes)
        return

    def isMinimized(self):
        """ Provides the minimized state """
        return self.__minimized

    def eventFilter(self, obj, evt):
        """ Handle click events for the tabbar """

        if obj == self.__tabBar:
            if evt.type() == QEvent.MouseButtonPress:
                pos = evt.pos()

                index = self.__tabBar.count() - 1
                while index >= 0:
                    if self.__tabBar.tabRect(index).contains(pos):
                        break
                    index -= 1

                if index == self.__tabBar.currentIndex():
                    if self.isMinimized():
                        self.expand()
                    else:
                        self.shrink()
                    return True

                elif self.isMinimized():
                    if self.isTabEnabled(index):
                        self.expand()

        return QWidget.eventFilter(self, obj, evt)

    def addTab(self, widget, iconOrLabel, label=None):
        """ Add a tab to the sidebar """

        if label:
            self.__tabBar.addTab(iconOrLabel, label)
        else:
            self.__tabBar.addTab(iconOrLabel)
        self.__stackedWidget.addWidget(widget)
        return

    def insertTab(self, index, widget, iconOrLabel, label=None):
        """ Insert a tab into the sidebar """

        if label:
            self.__tabBar.insertTab(index, iconOrLabel, label)
        else:
            self.__tabBar.insertTab(index, iconOrLabel)
        self.__stackedWidget.insertWidget(index, widget)
        return

    def removeTab(self, index):
        """ Remove a tab """

        self.__stackedWidget.removeWidget(self.__stackedWidget.widget(index))
        self.__tabBar.removeTab(index)
        return

    def clear(self):
        """ Remove all tabs """

        while self.count() > 0:
            self.removeTab(0)
        return

    def prevTab(self):
        """ Show the previous tab """

        index = self.currentIndex() - 1
        if index < 0:
            index = self.count() - 1

        self.setCurrentIndex(index)
        self.currentWidget().setFocus()
        return

    def nextTab(self):
        """ Show the next tab """

        index = self.currentIndex() + 1
        if index >= self.count():
            index = 0

        self.setCurrentIndex(index)
        self.currentWidget().setFocus()
        return

    def count(self):
        """ Provides the number of tabs """

        return self.__tabBar.count()

    def currentIndex(self):
        """ Provides the index of the current tab """

        return self.__stackedWidget.currentIndex()

    def setCurrentIndex(self, index):
        """ Switch to the certain tab """

        if index >= self.currentIndex():
            return

        self.__tabBar.setCurrentIndex(index)
        self.__stackedWidget.setCurrentIndex(index)
        if self.isMinimized():
            self.expand()
        return

    def currentWidget(self):
        """ Provide a reference to the current widget """

        return self.__stackedWidget.currentWidget()

    def setCurrentWidget(self, widget):
        """ Set the current widget """

        self.__stackedWidget.setCurrentWidget(widget)
        self.__tabBar.setCurrentIndex(self.__stackedWidget.currentIndex())
        if self.isMinimized():
            self.expand()
        return

    def indexOf(self, widget):
        """ Provides the index of the given widget """

        return self.__stackedWidget.indexOf(widget)

    def isTabEnabled(self, index):
        """ Check if the tab is enabled """

        return self.__tabBar.isTabEnabled(index)

    def setTabEnabled(self, index, enabled):
        """ Set the enabled state of the tab """

        self.__tabBar.setTabEnabled(index, enabled)
        return

    def orientation(self):
        """ Provides the orientation of the sidebar """

        return self.__orientation

    def setOrientation(self, orient):
        """ Set the orientation of the sidebar """

        if orient == SideBar.North:
            self.__tabBar.setShape(QTabBar.RoundedNorth)
            self.barLayout.setDirection(QBoxLayout.LeftToRight)
            self.layout.setDirection(QBoxLayout.TopToBottom)
            self.layout.setAlignment(self.barLayout, Qt.AlignLeft)
        elif orient == SideBar.East:
            self.__tabBar.setShape(QTabBar.RoundedEast)
            self.barLayout.setDirection(QBoxLayout.TopToBottom)
            self.layout.setDirection(QBoxLayout.RightToLeft)
            self.layout.setAlignment(self.barLayout, Qt.AlignTop)
        elif orient == SideBar.South:
            self.__tabBar.setShape(QTabBar.RoundedSouth)
            self.barLayout.setDirection(QBoxLayout.LeftToRight)
            self.layout.setDirection(QBoxLayout.BottomToTop)
            self.layout.setAlignment(self.barLayout, Qt.AlignLeft)
        else:
            # default
            orient = SideBar.West
            self.__tabBar.setShape(QTabBar.RoundedWest)
            self.barLayout.setDirection(QBoxLayout.TopToBottom)
            self.layout.setDirection(QBoxLayout.LeftToRight)
            self.layout.setAlignment(self.barLayout, Qt.AlignTop)
        self.__orientation = orient
        return

    def tabIcon(self, index):
        """ Provide the icon of the tab """

        return self.__tabBar.tabIcon(index)

    def setTabIcon(self, index, icon):
        """ Set the icon of the tab """

        self.__tabBar.setTabIcon(index, icon)
        return

    def tabText(self, index):
        """ Provide the text of the tab """

        return self.__tabBar.tabText(index)

    def setTabText(self, index, text):
        """ Set the text of the tab """

        self.__tabBar.setTabText(index, text)
        return

    def tabToolTip(self, index):
        """ Provide the tooltip text of the tab """

        return self.__tabBar.tabToolTip(index)

    def setTabToolTip(self, index, tip):
        """ Set the tooltip text of the tab """

        self.__tabBar.setTabToolTip(index, tip)
        return

    def tabWhatsThis(self, index):
        """ Provide the WhatsThis text of the tab """

        return self.__tabBar.tabWhatsThis(index)

    def setTabWhatsThis(self, index, text):
        """ Set the WhatsThis text for the tab """

        self.__tabBar.setTabWhatsThis(index, text)
        return

    def widget(self, index):
        """ Provides the reference to the widget (QWidget) """

        return self.__stackedWidget.widget(index)
Example #12
0
class SideBar( QWidget ):
    """ Sidebar with a widget area which is hidden or shown.
        On by clicking any tab, off by clicking the current tab.
    """

    North = 0
    East  = 1
    South = 2
    West  = 3

    def __init__( self, orientation = 2, parent = None ):
        QWidget.__init__( self, parent )

        self.__tabBar = QTabBar()
        self.__tabBar.setDrawBase( True )
        self.__tabBar.setShape( QTabBar.RoundedNorth )
        self.__tabBar.setFocusPolicy( Qt.NoFocus )
        self.__tabBar.setUsesScrollButtons( True )
        self.__tabBar.setElideMode( 1 )
        self.__stackedWidget = QStackedWidget( self )
        self.__stackedWidget.setContentsMargins( 0, 0, 0, 0 )
        self.barLayout = QBoxLayout( QBoxLayout.LeftToRight )
        self.barLayout.setMargin( 0 )
        self.layout = QBoxLayout( QBoxLayout.TopToBottom )
        self.layout.setMargin( 0 )
        self.layout.setSpacing( 0 )
        self.barLayout.addWidget( self.__tabBar )
        self.layout.addLayout( self.barLayout )
        self.layout.addWidget( self.__stackedWidget )
        self.setLayout( self.layout )

        self.__minimized = False
        self.__minSize = 0
        self.__maxSize = 0
        self.__bigSize = QSize()

        self.splitter = None

        self.__tabBar.installEventFilter( self )

        self.__orientation = orientation
        self.setOrientation( orientation )

        self.__tabBar.currentChanged.connect(
                                        self.__stackedWidget.setCurrentIndex )
        return

    def setSplitter( self, splitter ):
        """ Set the splitter managing the sidebar """
        self.splitter = splitter
        return

    def __getIndex( self ):
        " Provides the widget index in splitters "
        if self.__orientation == SideBar.West:
            return 0
        if self.__orientation == SideBar.East:
            return 2
        if self.__orientation == SideBar.South:
            return 1
        return 0

    def __getWidget( self ):
        " Provides a reference to the widget "
        return self.splitter.widget( self.__getIndex() )

    def shrink( self ):
        """ Shrink the sidebar """
        if self.__minimized:
            return

        self.__minimized = True
        self.__bigSize = self.size()
        if self.__orientation in [ SideBar.North, SideBar.South ]:
            self.__minSize = self.minimumHeight()
            self.__maxSize = self.maximumHeight()
        else:
            self.__minSize = self.minimumWidth()
            self.__maxSize = self.maximumWidth()

        self.__stackedWidget.hide()

        sizes = self.splitter.sizes()
        selfIndex = self.__getIndex()

        if self.__orientation in [ SideBar.North, SideBar.South ]:
            newHeight = self.__tabBar.minimumSizeHint().height()
            self.setFixedHeight( newHeight )

            diff = sizes[ selfIndex ] - newHeight
            sizes[ selfIndex ] = newHeight
        else:
            newWidth = self.__tabBar.minimumSizeHint().width()
            self.setFixedWidth( newWidth )

            diff = sizes[ selfIndex ] - newWidth
            sizes[ selfIndex ] = newWidth

        if selfIndex == 0:
            sizes[ 1 ] += diff
        else:
            sizes[ selfIndex - 1 ] += diff

        self.splitter.setSizes( sizes )
        return

    def expand( self ):
        """ Expand the sidebar """
        if not self.__minimized:
            return

        self.__minimized = False
        self.__stackedWidget.show()
        self.resize( self.__bigSize )

        sizes = self.splitter.sizes()
        selfIndex = self.__getIndex()

        if self.__orientation in [ SideBar.North, SideBar.South ]:
            self.setMinimumHeight( self.__minSize )
            self.setMaximumHeight( self.__maxSize )

            diff = self.__bigSize.height() - sizes[ selfIndex ]
            sizes[ selfIndex ] = self.__bigSize.height()
        else:
            self.setMinimumWidth( self.__minSize )
            self.setMaximumWidth( self.__maxSize )

            diff = self.__bigSize.width() - sizes[ selfIndex ]
            sizes[ selfIndex ] = self.__bigSize.width()

        if selfIndex == 0:
            sizes[ 1 ] -= diff
        else:
            sizes[ selfIndex - 1 ] -= diff

        self.splitter.setSizes( sizes )
        return

    def isMinimized( self ):
        """ Provides the minimized state """
        return self.__minimized

    def eventFilter( self, obj, evt ):
        """ Handle click events for the tabbar """

        if obj == self.__tabBar:
            if evt.type() == QEvent.MouseButtonPress:
                pos = evt.pos()

                index = self.__tabBar.count() - 1
                while index >= 0:
                    if self.__tabBar.tabRect( index ).contains( pos ):
                        break
                    index -= 1

                if index == self.__tabBar.currentIndex():
                    if self.isMinimized():
                        self.expand()
                    else:
                        self.shrink()
                    return True

                elif self.isMinimized():
                    if self.isTabEnabled( index ):
                        self.expand()

        return QWidget.eventFilter( self, obj, evt )

    def addTab( self, widget, iconOrLabel, label = None ):
        """ Add a tab to the sidebar """

        if label:
            self.__tabBar.addTab( iconOrLabel, label )
        else:
            self.__tabBar.addTab( iconOrLabel )
        self.__stackedWidget.addWidget( widget )
        return

    def insertTab( self, index, widget, iconOrLabel, label = None ):
        """ Insert a tab into the sidebar """

        if label:
            self.__tabBar.insertTab( index, iconOrLabel, label )
        else:
            self.__tabBar.insertTab( index, iconOrLabel )
        self.__stackedWidget.insertWidget( index, widget )
        return

    def removeTab( self, index ):
        """ Remove a tab """

        self.__stackedWidget.removeWidget( self.__stackedWidget.widget( index ) )
        self.__tabBar.removeTab( index )
        return

    def clear( self ):
        """ Remove all tabs """

        while self.count() > 0:
            self.removeTab( 0 )
        return

    def prevTab( self ):
        """ Show the previous tab """

        index = self.currentIndex() - 1
        if index < 0:
            index = self.count() - 1

        self.setCurrentIndex( index )
        self.currentWidget().setFocus()
        return

    def nextTab( self ):
        """ Show the next tab """

        index = self.currentIndex() + 1
        if index >= self.count():
            index = 0

        self.setCurrentIndex( index )
        self.currentWidget().setFocus()
        return

    def count( self ):
        """ Provides the number of tabs """

        return self.__tabBar.count()

    def currentIndex( self ):
        """ Provides the index of the current tab """

        return self.__stackedWidget.currentIndex()

    def setCurrentIndex( self, index ):
        """ Switch to the certain tab """

        if index >= self.currentIndex():
            return

        self.__tabBar.setCurrentIndex( index )
        self.__stackedWidget.setCurrentIndex(index)
        if self.isMinimized():
            self.expand()
        return

    def currentWidget( self ):
        """ Provide a reference to the current widget """

        return self.__stackedWidget.currentWidget()

    def setCurrentWidget( self, widget ):
        """ Set the current widget """

        self.__stackedWidget.setCurrentWidget( widget )
        self.__tabBar.setCurrentIndex( self.__stackedWidget.currentIndex() )
        if self.isMinimized():
            self.expand()
        return

    def indexOf( self, widget ):
        """ Provides the index of the given widget """

        return self.__stackedWidget.indexOf( widget )

    def isTabEnabled( self, index ):
        """ Check if the tab is enabled """

        return self.__tabBar.isTabEnabled( index )

    def setTabEnabled( self, index, enabled ):
        """ Set the enabled state of the tab """

        self.__tabBar.setTabEnabled( index, enabled )
        return

    def orientation( self ):
        """ Provides the orientation of the sidebar """

        return self.__orientation

    def setOrientation( self, orient ):
        """ Set the orientation of the sidebar """

        if orient == SideBar.North:
            self.__tabBar.setShape( QTabBar.RoundedNorth )
            self.barLayout.setDirection( QBoxLayout.LeftToRight )
            self.layout.setDirection( QBoxLayout.TopToBottom )
            self.layout.setAlignment( self.barLayout, Qt.AlignLeft )
        elif orient == SideBar.East:
            self.__tabBar.setShape( QTabBar.RoundedEast )
            self.barLayout.setDirection( QBoxLayout.TopToBottom )
            self.layout.setDirection( QBoxLayout.RightToLeft )
            self.layout.setAlignment( self.barLayout, Qt.AlignTop )
        elif orient == SideBar.South:
            self.__tabBar.setShape( QTabBar.RoundedSouth )
            self.barLayout.setDirection( QBoxLayout.LeftToRight )
            self.layout.setDirection( QBoxLayout.BottomToTop )
            self.layout.setAlignment( self.barLayout, Qt.AlignLeft )
        else:
            # default
            orient = SideBar.West
            self.__tabBar.setShape( QTabBar.RoundedWest )
            self.barLayout.setDirection( QBoxLayout.TopToBottom )
            self.layout.setDirection( QBoxLayout.LeftToRight )
            self.layout.setAlignment( self.barLayout, Qt.AlignTop )
        self.__orientation = orient
        return

    def tabIcon( self, index ):
        """ Provide the icon of the tab """

        return self.__tabBar.tabIcon( index )

    def setTabIcon( self, index, icon ):
        """ Set the icon of the tab """

        self.__tabBar.setTabIcon( index, icon )
        return

    def tabText( self, index ):
        """ Provide the text of the tab """

        return self.__tabBar.tabText( index )

    def setTabText( self, index, text ):
        """ Set the text of the tab """

        self.__tabBar.setTabText( index, text )
        return

    def tabToolTip( self, index ):
        """ Provide the tooltip text of the tab """

        return self.__tabBar.tabToolTip( index )

    def setTabToolTip( self, index, tip ):
        """ Set the tooltip text of the tab """

        self.__tabBar.setTabToolTip( index, tip )
        return

    def tabWhatsThis( self, index ):
        """ Provide the WhatsThis text of the tab """

        return self.__tabBar.tabWhatsThis( index )

    def setTabWhatsThis( self, index, text ):
        """ Set the WhatsThis text for the tab """

        self.__tabBar.setTabWhatsThis( index, text )
        return

    def widget( self, index ):
        """ Provides the reference to the widget (QWidget) """

        return self.__stackedWidget.widget( index )
Example #13
0
class EvtsFilesWidget(QSplitter):
    def __init__(self, manager, root="/"):
        """
     \returns a QSplitter() containing a QLIstWidget() listing all evt files,
     an EVtTableWIdget() and an EvtControlPannel
     """
        QSplitter.__init__(self, Qt.Horizontal)
        self.manager = manager

        self.evtFileListWidget = QListWidget()
        self.evtFileListWidget.itemClicked.connect(self.switchEventTable)
        fileLabel = QLabel('Windows events')

        w = QWidget()
        self.stackedWidget = QStackedWidget()
        vboxLayout = QVBoxLayout(w)
        vboxLayout.addWidget(fileLabel)
        vboxLayout.addWidget(self.evtFileListWidget)
        vboxLayout.setSpacing(2)
        vboxLayout.setContentsMargins(2, 2, 2, 2)

        for evt in self.manager.evts:
            try:
                events = self.manager.evts[evt]
                if events and len(events):
                    fileItem = self.getFileItems(evt, root)
                    if fileItem:
                        self.evtFileListWidget.addItem(fileItem)
                        currentTable = EvtWidget(None, fileItem.text(), events,
                                                 evt)
                        self.stackedWidget.addWidget(currentTable)
                        fileItem.table(currentTable)
            except Exception as e:
                pass

        self.addWidget(w)
        self.addWidget(self.stackedWidget)
        self.currentItem = self.evtFileListWidget.item(0)
        if self.currentItem:
            self.switchEventTable(self.currentItem)
        self.setStretchFactor(1, 2)

    def switchEventTable(self, fileItem):
        table = fileItem.table()
        self.currentItem = fileItem
        if table:
            self.stackedWidget.setCurrentWidget(table)

    def getFileItems(self, evt, root):
        node = VFS.Get().getNodeById(evt)
        if node is None:
            return None
        if node.absolute()[:len(root)] != root:
            return None
        fileItem = FileItem(QIcon(':/toggle_log'), node.name(), node)
        return fileItem

    def report(self):
        reportManager = ReportManager()
        for itemID in range(self.evtFileListWidget.count()):
            fileItem = self.evtFileListWidget.item(itemID)
            fileItem.table().report()
class QgsTextAnnotationDialog(QDialog):
    def __init__(self, item):
        QDialog.__init__(self)
        self.gridLayout = QGridLayout(self)
        self.gridLayout.setObjectName(("gridLayout"))
        self.horizontalLayout = QHBoxLayout()
        self.horizontalLayout.setObjectName(("horizontalLayout"))
        self.mFontComboBox = QFontComboBox(self)
        self.mFontComboBox.setObjectName(("mFontComboBox"))
        self.horizontalLayout.addWidget(self.mFontComboBox)
        spacerItem = QSpacerItem(38, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
        self.horizontalLayout.addItem(spacerItem)
        self.mFontSizeSpinBox = QSpinBox(self)
        self.mFontSizeSpinBox.setObjectName(("mFontSizeSpinBox"))
        self.horizontalLayout.addWidget(self.mFontSizeSpinBox)
        self.mBoldPushButton = QPushButton(self)
        self.mBoldPushButton.setMinimumSize(QSize(50, 0))
        self.mBoldPushButton.setCheckable(True)
        self.mBoldPushButton.setObjectName(("mBoldPushButton"))
        self.horizontalLayout.addWidget(self.mBoldPushButton)
        self.mItalicsPushButton = QPushButton(self)
        self.mItalicsPushButton.setMinimumSize(QSize(50, 0))
        self.mItalicsPushButton.setCheckable(True)
        self.mItalicsPushButton.setObjectName(("mItalicsPushButton"))
        self.horizontalLayout.addWidget(self.mItalicsPushButton)
        self.mFontColorButton = QgsColorButton(self)
        self.mFontColorButton.setText((""))
        self.mFontColorButton.setAutoDefault(False)
        self.mFontColorButton.setObjectName(("mFontColorButton"))
        self.horizontalLayout.addWidget(self.mFontColorButton)
        self.gridLayout.addLayout(self.horizontalLayout, 0, 0, 1, 1)
        self.mButtonBox = QDialogButtonBox(self)
        self.mButtonBox.setOrientation(Qt.Horizontal)
        self.mButtonBox.setStandardButtons(QDialogButtonBox.Cancel|QDialogButtonBox.Ok)
        self.mButtonBox.setObjectName(("mButtonBox"))
        self.gridLayout.addWidget(self.mButtonBox, 3, 0, 1, 1)
        self.mTextEdit = QTextEdit(self)
        self.mTextEdit.setObjectName(("mTextEdit"))
        self.gridLayout.addWidget(self.mTextEdit, 1, 0, 1, 1)
        self.mStackedWidget = QStackedWidget(self)
        self.mStackedWidget.setObjectName(("mStackedWidget"))
        self.page = QWidget()
        self.page.setObjectName(("page"))
        self.mStackedWidget.addWidget(self.page)
        self.page_2 = QWidget()
        self.page_2.setObjectName(("page_2"))
        self.mStackedWidget.addWidget(self.page_2)
        self.gridLayout.addWidget(self.mStackedWidget, 2, 0, 1, 1)
        self.setLayout(self.gridLayout)
        
        self.mStackedWidget.setCurrentIndex(0)
        QObject.connect(self.mButtonBox, SIGNAL(("accepted()")), self.accept)
        QObject.connect(self.mButtonBox, SIGNAL(("rejected()")), self.reject)
        
        self.setTabOrder(self.mFontComboBox, self.mFontSizeSpinBox)
        self.setTabOrder(self.mFontSizeSpinBox, self.mBoldPushButton)
        self.setTabOrder(self.mBoldPushButton, self.mItalicsPushButton)
        self.setTabOrder(self.mItalicsPushButton, self.mFontColorButton)
        self.setTabOrder(self.mFontColorButton, self.mTextEdit)
        self.setTabOrder(self.mTextEdit, self.mButtonBox)
        
        self.setWindowTitle("Annotation text")
        self.mBoldPushButton.setText("B")
        self.mItalicsPushButton.setText("I")
        
        self.mTextDocument = None
        self.mItem = item
        self.mEmbeddedWidget = QgsAnnotationWidget(self, self.mItem )
        self.mEmbeddedWidget.show()
        self.mStackedWidget.addWidget( self.mEmbeddedWidget )
        self.mStackedWidget.setCurrentWidget( self.mEmbeddedWidget )
        if ( self.mItem != None ):
            self.mTextDocument = self.mItem.document()
            self.mTextEdit.setDocument( self.mTextDocument )
        self.mFontColorButton.setColorDialogTitle(  "Select font color"  )
        self.mFontColorButton.setColorDialogOptions( QColorDialog.ShowAlphaChannel )
        self.setCurrentFontPropertiesToGui()
        QObject.connect( self.mButtonBox, SIGNAL("accepted()"), self.applyTextToItem)
#         QObject.connect( self.mFontComboBox, SIGNAL( "currentFontChanged(QFont())"), self.changeCurrentFormat)
        self.mFontComboBox.currentFontChanged.connect(self.changeCurrentFormat)
        QObject.connect( self.mFontSizeSpinBox, SIGNAL( "valueChanged( int )" ), self.changeCurrentFormat ) 
        QObject.connect( self.mBoldPushButton, SIGNAL( "toggled( bool )" ), self.changeCurrentFormat)
        QObject.connect( self.mItalicsPushButton, SIGNAL( "toggled( bool )" ), self.changeCurrentFormat)
        QObject.connect( self.mTextEdit, SIGNAL( "cursorPositionChanged()" ), self.setCurrentFontPropertiesToGui )
        
#         QObject.connect( self.mButtonBox, SIGNAL( "accepted()" ), self.applySettingsToItem)
        deleteButton = QPushButton( "Delete" )
        QObject.connect( deleteButton, SIGNAL( "clicked()" ), self.deleteItem )
        self.mButtonBox.addButton( deleteButton, QDialogButtonBox.RejectRole )
    def applyTextToItem(self):
        if ( self.mItem  != None and self.mTextDocument !=None ):
            if ( self.mEmbeddedWidget != None):
                self.mEmbeddedWidget.apply()
            self.mItem.setDocument( self.mTextDocument )
            self.mItem.update()
    def changeCurrentFormat(self):
        newFont = QFont()
        newFont.setFamily( self.mFontComboBox.currentFont().family() )
        #bold
        if ( self.mBoldPushButton.isChecked() ):
            newFont.setBold( True )
        else:
            newFont.setBold( False )
        #italic
        if ( self.mItalicsPushButton.isChecked() ):
            newFont.setItalic( True )
        else:
            newFont.setItalic( False )
        #size
        newFont.setPointSize( self.mFontSizeSpinBox.value() )
        self.mTextEdit.setCurrentFont( newFont )
        #color
        self.mTextEdit.setTextColor( self.mFontColorButton.color() )
        
    def on_mFontColorButton_colorChanged(self, color ):
        self.changeCurrentFormat()
    def setCurrentFontPropertiesToGui(self):
        self.blockAllSignals( True )
        currentFont = self.mTextEdit.currentFont()
        self.mFontComboBox.setCurrentFont( currentFont )
        self.mFontSizeSpinBox.setValue( currentFont.pointSize() )
        self.mBoldPushButton.setChecked( currentFont.bold() )
        self.mItalicsPushButton.setChecked( currentFont.italic() )
        self.mFontColorButton.setColor( self.mTextEdit.textColor() )
        self.blockAllSignals( False )
        
    def blockAllSignals(self, block ):
        self.mFontComboBox.blockSignals( block )
        self.mFontSizeSpinBox.blockSignals( block )
        self.mBoldPushButton.blockSignals( block )
        self.mItalicsPushButton.blockSignals( block )
        self.mFontColorButton.blockSignals( block )
    
    def deleteItem(self):
        scene = self.mItem.scene()
        if ( scene != None ):
            scene.removeItem( self.mItem )
        self.mItem = None
Example #15
0
class FontsColors(preferences.Page):
    def __init__(self, dialog):
        super(FontsColors, self).__init__(dialog)

        layout = QVBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        self.setLayout(layout)
        
        self.scheme = SchemeSelector(self)
        layout.addWidget(self.scheme)
        
        self.printScheme = QCheckBox()
        layout.addWidget(self.printScheme)
        
        hbox = QHBoxLayout()
        self.tree = QTreeWidget(self)
        self.tree.setHeaderHidden(True)
        self.tree.setAnimated(True)
        self.stack = QStackedWidget(self)
        hbox.addWidget(self.tree)
        hbox.addWidget(self.stack)
        layout.addLayout(hbox)
        
        hbox = QHBoxLayout()
        self.fontLabel = QLabel()
        self.fontChooser = QFontComboBox()
        self.fontSize = QDoubleSpinBox()
        self.fontSize.setRange(6.0, 32.0)
        self.fontSize.setSingleStep(0.5)
        self.fontSize.setDecimals(1)
        hbox.addWidget(self.fontLabel)
        hbox.addWidget(self.fontChooser, 1)
        hbox.addWidget(self.fontSize)
        layout.addLayout(hbox)
        
        # add the items to our list
        self.baseColorsItem = i = QTreeWidgetItem()
        self.tree.addTopLevelItem(i)
        self.defaultStylesItem = i = QTreeWidgetItem()
        self.tree.addTopLevelItem(i)
        
        self.defaultStyles = {}
        for name in textformats.defaultStyles:
            self.defaultStyles[name] = i = QTreeWidgetItem()
            self.defaultStylesItem.addChild(i)
            i.name = name
        self.defaultStylesItem.setExpanded(True)
        
        self.allStyles = {}
        for group, styles in ly.colorize.default_mapping():
            i = QTreeWidgetItem()
            children = {}
            self.allStyles[group] = (i, children)
            self.tree.addTopLevelItem(i)
            i.group = group
            for name, base, clss in styles:
                j = QTreeWidgetItem()
                j.name = name
                j.base = base
                i.addChild(j)
                children[name] = j
        
        self.baseColorsWidget = BaseColors(self)
        self.customAttributesWidget = CustomAttributes(self)
        self.emptyWidget = QWidget(self)
        self.stack.addWidget(self.baseColorsWidget)
        self.stack.addWidget(self.customAttributesWidget)
        self.stack.addWidget(self.emptyWidget)
        
        self.tree.currentItemChanged.connect(self.currentItemChanged)
        self.tree.setCurrentItem(self.baseColorsItem)
        self.scheme.currentChanged.connect(self.currentSchemeChanged)
        self.scheme.changed.connect(self.changed)
        self.baseColorsWidget.changed.connect(self.baseColorsChanged)
        self.customAttributesWidget.changed.connect(self.customAttributesChanged)
        self.fontChooser.currentFontChanged.connect(self.fontChanged)
        self.fontSize.valueChanged.connect(self.fontChanged)
        self.printScheme.clicked.connect(self.printSchemeChanged)
        
        app.translateUI(self)
        
    def translateUI(self):
        self.printScheme.setText(_("Use this scheme for printing"))
        self.fontLabel.setText(_("Font:"))
        self.baseColorsItem.setText(0, _("Base Colors"))
        self.defaultStylesItem.setText(0, _("Default Styles"))
        
        self.defaultStyleNames = defaultStyleNames()
        self.allStyleNames = allStyleNames()
        
        for name in textformats.defaultStyles:
            self.defaultStyles[name].setText(0, self.defaultStyleNames[name])
        for group, styles in ly.colorize.default_mapping():
            self.allStyles[group][0].setText(0, self.allStyleNames[group][0])
            for name, base, clss in styles:
                self.allStyles[group][1][name].setText(0, self.allStyleNames[group][1][name])
            
    def currentItemChanged(self, item, previous):
        if item is self.baseColorsItem:
            self.stack.setCurrentWidget(self.baseColorsWidget)
        elif not item.parent():
            self.stack.setCurrentWidget(self.emptyWidget)
        else:
            data = self.data[self.scheme.currentScheme()]
            w = self.customAttributesWidget
            self.stack.setCurrentWidget(w)
            toptext = None
            if item.parent() is self.defaultStylesItem:
                # default style
                w.setTitle(item.text(0))
                w.setTristate(False)
                w.setTextFormat(data.defaultStyles[item.name])
            else:
                # specific style of specific group
                group, name = item.parent().group, item.name
                w.setTitle("{0}: {1}".format(item.parent().text(0), item.text(0)))
                inherit = item.base
                if inherit:
                    toptext = _("(Inherits: {name})").format(name=self.defaultStyleNames[inherit])
                w.setTristate(bool(inherit))
                w.setTextFormat(data.allStyles[group][name])
            w.setTopText(toptext)
    
    def currentSchemeChanged(self):
        scheme = self.scheme.currentScheme()
        if scheme not in self.data:
            self.data[scheme] = textformats.TextFormatData(scheme)
        self.updateDisplay()
        if self.tree.currentItem():
            self.currentItemChanged(self.tree.currentItem(), None)
        with qutil.signalsBlocked(self.printScheme):
            self.printScheme.setChecked(scheme == self._printScheme)
    
    def fontChanged(self):
        data = self.data[self.scheme.currentScheme()]
        data.font = self.fontChooser.currentFont()
        data.font.setPointSizeF(self.fontSize.value())
        self.updateDisplay()
        self.changed.emit()
    
    def printSchemeChanged(self):
        if self.printScheme.isChecked():
            self._printScheme = self.scheme.currentScheme()
        else:
            self._printScheme = None
        self.changed.emit()
    
    def addSchemeData(self, scheme, tfd):
        self.data[scheme] = tfd
        
    def currentSchemeData(self):
        return self.data[self.scheme.currentScheme()]
        
    def updateDisplay(self):
        data = self.data[self.scheme.currentScheme()]
        
        with qutil.signalsBlocked(self.fontChooser, self.fontSize):
            self.fontChooser.setCurrentFont(data.font)
            self.fontSize.setValue(data.font.pointSizeF())
        
        with qutil.signalsBlocked(self):
            # update base colors
            for name in textformats.baseColors:
                self.baseColorsWidget.color[name].setColor(data.baseColors[name])
        
        # update base colors for whole treewidget
        p = QApplication.palette()
        p.setColor(QPalette.Base, data.baseColors['background'])
        p.setColor(QPalette.Text, data.baseColors['text'])
        p.setColor(QPalette.Highlight, data.baseColors['selectionbackground'])
        p.setColor(QPalette.HighlightedText, data.baseColors['selectiontext'])
        self.tree.setPalette(p)
        
        def setItemTextFormat(item, f):
            font = QFont(data.font)
            if f.hasProperty(QTextFormat.ForegroundBrush):
                item.setForeground(0, f.foreground().color())
            else:
                item.setForeground(0, data.baseColors['text'])
            if f.hasProperty(QTextFormat.BackgroundBrush):
                item.setBackground(0, f.background().color())
            else:
                item.setBackground(0, QBrush())
            font.setWeight(f.fontWeight())
            font.setItalic(f.fontItalic())
            font.setUnderline(f.fontUnderline())
            item.setFont(0, font)
            
        # update looks of default styles
        for name in textformats.defaultStyles:
            setItemTextFormat(self.defaultStyles[name], data.defaultStyles[name])
        
        # update looks of all the specific styles
        for group, styles in ly.colorize.default_mapping():
            children = self.allStyles[group][1]
            for name, inherit, clss in styles:
                f = QTextCharFormat(data.defaultStyles[inherit]) if inherit else QTextCharFormat()
                f.merge(data.allStyles[group][name])
                setItemTextFormat(children[name], f)
        
    def baseColorsChanged(self, name):
        # keep data up to date with base colors
        data = self.data[self.scheme.currentScheme()]
        data.baseColors[name] = self.baseColorsWidget.color[name].color()
        self.updateDisplay()
        self.changed.emit()
    
    def customAttributesChanged(self):
        item = self.tree.currentItem()
        if not item or not item.parent():
            return
        data = self.data[self.scheme.currentScheme()]
        if item.parent() is self.defaultStylesItem:
            # a default style has been changed
            data.defaultStyles[item.name] = self.customAttributesWidget.textFormat()
        else:
            # a specific style has been changed
            group, name = item.parent().group, item.name
            data.allStyles[group][name] = self.customAttributesWidget.textFormat()
        self.updateDisplay()
        self.changed.emit()
        
    def import_(self, filename):
        from . import import_export
        import_export.importTheme(filename, self, self.scheme)
        
    def export(self, name, filename):
        from . import import_export
        try:
            import_export.exportTheme(self, name, filename)
        except (IOError, OSError) as e:
            QMessageBox.critical(self, _("Error"), _(
                "Can't write to destination:\n\n{url}\n\n{error}").format(
                url=filename, error=e.strerror))
    
    def loadSettings(self):
        self.data = {} # holds all data with scheme as key
        self._printScheme = QSettings().value("printer_scheme", "default", type(""))
        self.scheme.loadSettings("editor_scheme", "editor_schemes")
        
    def saveSettings(self):
        self.scheme.saveSettings("editor_scheme", "editor_schemes", "fontscolors")
        for scheme in self.scheme.schemes():
            if scheme in self.data:
                self.data[scheme].save(scheme)
        if self._printScheme:
            QSettings().setValue("printer_scheme", self._printScheme)
        else:
            QSettings().remove("printer_scheme")
Example #16
0
class PlotScalesWidget(QWidget):
    plotScaleChanged = pyqtSignal()

    def __init__(self, type_key, title, select_min_time_value=False):
        QWidget.__init__(self)

        self.__type_key = type_key
        self.__type = None

        self.__double_spinner = self.createDoubleSpinner(minimum=-999999999.0,
                                                         maximum=999999999.0)
        self.__integer_spinner = self.createIntegerSpinner(minimum=0,
                                                           maximum=999999999)

        self.__time_map = ReportStepsModel().getList()
        self.__time_index_map = {}
        for index in range(len(self.__time_map)):
            time = self.__time_map[index]
            self.__time_index_map[time] = index

        self.__time_spinner = self.createTimeSpinner(
            select_minimum_value=select_min_time_value)

        layout = QVBoxLayout()
        self.setLayout(layout)

        self.__label = QLabel(title)
        self.__label.setAlignment(Qt.AlignHCenter)

        self.__stack = QStackedWidget()
        self.__stack.setSizePolicy(QSizePolicy(QSizePolicy.Preferred))
        self.__stack.addWidget(self.__integer_spinner)
        self.__stack.addWidget(self.__double_spinner)
        self.__stack.addWidget(self.__time_spinner)

        layout.addWidget(self.__stack)
        layout.addWidget(self.__label)

        self.setLayout(layout)

    def createDoubleSpinner(self, minimum, maximum):
        spinner = QDoubleSpinBox()
        spinner.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)
        spinner.setMinimumWidth(105)
        spinner.setRange(minimum, maximum)
        spinner.setKeyboardTracking(False)
        spinner.setDecimals(8)

        spinner.editingFinished.connect(self.plotScaleChanged)
        spinner.valueChanged.connect(self.plotScaleChanged)

        return spinner

    def createIntegerSpinner(self, minimum, maximum):
        spinner = QSpinBox()
        spinner.setMinimumWidth(75)
        spinner.setRange(minimum, maximum)
        spinner.setKeyboardTracking(False)

        spinner.editingFinished.connect(self.plotScaleChanged)
        spinner.valueChanged.connect(self.plotScaleChanged)

        return spinner

    def createTimeSpinner(self, select_minimum_value):
        def converter(item):
            return "%s" % (str(item.date()))

        spinner = ListSpinBox(self.__time_map)
        spinner.setMinimumWidth(75)

        if select_minimum_value:
            spinner.setValue(0)

        spinner.valueChanged[int].connect(self.plotScaleChanged)
        spinner.editingFinished.connect(self.plotScaleChanged)
        spinner.setStringConverter(converter)

        return spinner

    def getValue(self):
        if self.__type is int:
            return self.__integer_spinner.value()
        elif self.__type is float:
            return self.__double_spinner.value()
        elif self.__type is CTime:
            index = self.__time_spinner.value()
            return self.__time_map[index]
        else:
            raise TypeError("Unsupported spinner type: %s" % self.__type)

    def setValue(self, value):
        if value is not None:
            if self.__type is int:
                self.__integer_spinner.setValue(int(value))
            elif self.__type is float:
                self.__double_spinner.setValue(value)
            elif self.__type is CTime:
                index = self.__time_index_map[value]
                self.__time_spinner.setValue(index)
            else:
                raise TypeError("Unsupported spinner type: %s" % self.__type)

    def setFontSize(self, size):
        font = self.__double_spinner.font()
        font.setPointSize(size)
        self.__double_spinner.setFont(font)

        font = self.__integer_spinner.font()
        font.setPointSize(size)
        self.__integer_spinner.setFont(font)

        font = self.__time_spinner.font()
        font.setPointSize(size)
        self.__time_spinner.setFont(font)

        font = self.__label.font()
        font.setPointSize(size)
        self.__label.setFont(font)

    def setType(self, spinner_type):
        self.__type = spinner_type
        if spinner_type is int:
            self.__stack.setCurrentWidget(self.__integer_spinner)
        elif spinner_type is float:
            self.__stack.setCurrentWidget(self.__double_spinner)
        elif spinner_type is CTime:
            self.__stack.setCurrentWidget(self.__time_spinner)
        else:
            raise TypeError("Unsupported spinner type: %s" % spinner_type)

    def getType(self):
        return self.__type
Example #17
0
class ViewSpace(QWidget):
    """A ViewSpace manages a stack of views, one of them is visible.
    
    The ViewSpace also has a statusbar, accessible in the status attribute.
    The viewChanged(View) signal is emitted when the current view for this ViewSpace changes.
    
    Also, when a ViewSpace is created (e.g. when a window is created or split), the
    app.viewSpaceCreated(space) signal is emitted.
    
    You can use the app.viewSpaceCreated() and the ViewSpace.viewChanged() signals to implement
    things on a per ViewSpace basis, e.g. in the statusbar of a ViewSpace.
    
    """
    viewChanged = pyqtSignal(view_.View)
    
    def __init__(self, manager, parent=None):
        super(ViewSpace, self).__init__(parent)
        self.manager = weakref.ref(manager)
        self.views = []
        
        layout = QVBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)
        self.setLayout(layout)
        self.stack = QStackedWidget(self)
        layout.addWidget(self.stack)
        self.status = ViewStatusBar(self)
        self.status.setEnabled(False)
        layout.addWidget(self.status)
        app.languageChanged.connect(self.updateStatusBar)
        app.viewSpaceCreated(self)
        
    def activeView(self):
        if self.views:
            return self.views[-1]

    def document(self):
        """Returns the currently active document in this space.
        
        If there are no views, returns None.
        
        """
        if self.views:
            return self.views[-1].document()
    
    def showDocument(self, doc):
        """Shows the document, creating a View if necessary."""
        if doc is self.document():
            return
        cur = self.activeView()
        for view in self.views[:-1]:
            if doc is view.document():
                self.views.remove(view)
                break
        else:
            view = view_.View(doc)
            self.stack.addWidget(view)
        self.views.append(view)
        if cur:
            self.disconnectView(cur)
        self.connectView(view)
        self.stack.setCurrentWidget(view)
        self.updateStatusBar()
        
    def removeDocument(self, doc):
        active = doc is self.document()
        if active:
            self.disconnectView(self.activeView())
        for view in self.views:
            if doc is view.document():
                self.views.remove(view)
                view.deleteLater()
                break
        else:
            return
        if active and self.views:
            self.connectView(self.views[-1])
            self.stack.setCurrentWidget(self.views[-1])
            self.updateStatusBar()
    
    def connectView(self, view):
        view.installEventFilter(self)
        view.cursorPositionChanged.connect(self.updateCursorPosition)
        view.modificationChanged.connect(self.updateModificationState)
        view.document().urlChanged.connect(self.updateDocumentName)
        self.viewChanged.emit(view)

    def disconnectView(self, view):
        view.removeEventFilter(self)
        view.cursorPositionChanged.disconnect(self.updateCursorPosition)
        view.modificationChanged.disconnect(self.updateModificationState)
        view.document().urlChanged.disconnect(self.updateDocumentName)
    
    def eventFilter(self, view, ev):
        if ev.type() == QEvent.FocusIn:
            self.setActiveViewSpace()
        return False

    def setActiveViewSpace(self):
        self.manager().setActiveViewSpace(self)
        
    def updateStatusBar(self):
        """Update all info in the statusbar, e.g. on document change."""
        if self.views:
            self.updateCursorPosition()
            self.updateModificationState()
            self.updateDocumentName()
        
    def updateCursorPosition(self):
        cur = self.activeView().textCursor()
        line = cur.blockNumber() + 1
        try:
            column = cur.positionInBlock()
        except AttributeError: # only in very recent PyQt4
            column = cur.position() - cur.block().position()
        self.status.pos.setText(_("Line: {line}, Col: {column}").format(
            line = line, column = column))
    
    def updateModificationState(self):
        modified = self.document().isModified()
        pixmap = icons.get('document-save').pixmap(16) if modified else QPixmap()
        self.status.state.setPixmap(pixmap)
    
    def updateDocumentName(self):
        self.status.info.setText(self.document().documentName())
Example #18
0
class DataExportGui(QWidget):
    """
    Manages all GUI elements in the data selection applet.
    This class itself is the central widget and also owns/manages the applet drawer widgets.
    """

    ###########################################
    ### AppletGuiInterface Concrete Methods ###
    ###########################################

    def centralWidget(self):
        return self

    def appletDrawer(self):
        return self.drawer

    def menus(self):
        return []

    def viewerControlWidget(self):
        return self._viewerControlWidgetStack

    def setImageIndex(self, index):
        pass

    def stopAndCleanUp(self):
        for editor in self.layerViewerGuis.values():
            self.viewerStack.removeWidget(editor)
            editor.stopAndCleanUp()
        self.layerViewerGuis.clear()

    def imageLaneAdded(self, laneIndex):
        pass

    def imageLaneRemoved(self, laneIndex, finalLength):
        pass

    def allowLaneSelectionChange(self):
        return False

    ###########################################
    ###########################################

    def __init__(self, parentApplet, topLevelOperator):
        super(DataExportGui, self).__init__()

        self.drawer = None
        self.topLevelOperator = topLevelOperator

        self.threadRouter = ThreadRouter(self)
        self._thunkEventHandler = ThunkEventHandler(self)

        self._initAppletDrawerUic()
        self.initCentralUic()
        self.initViewerControls()

        self.parentApplet = parentApplet
        self.progressSignal = parentApplet.progressSignal

        self.overwrite = False

        @threadRoutedWithRouter(self.threadRouter)
        def handleNewDataset(multislot, index):
            # Make room in the GUI table
            self.batchOutputTableWidget.insertRow(index)

            # Update the table row data when this slot has new data
            # We can't bind in the row here because the row may change in the meantime.
            multislot[index].notifyReady(bind(self.updateTableForSlot))
            if multislot[index].ready():
                self.updateTableForSlot(multislot[index])

            multislot[index].notifyUnready(self._updateExportButtons)
            multislot[index].notifyReady(self._updateExportButtons)

        self.topLevelOperator.ExportPath.notifyInserted(bind(handleNewDataset))

        # For each dataset that already exists, update the GUI
        for i, subslot in enumerate(self.topLevelOperator.ExportPath):
            handleNewDataset(self.topLevelOperator.ExportPath, i)
            if subslot.ready():
                self.updateTableForSlot(subslot)

        @threadRoutedWithRouter(self.threadRouter)
        def handleLaneRemoved(multislot, index, finalLength):
            if self.batchOutputTableWidget.rowCount() <= finalLength:
                return

            # Remove the row we don't need any more
            self.batchOutputTableWidget.removeRow(index)

            # Remove the viewer for this dataset
            imageMultiSlot = self.topLevelOperator.Inputs[index]
            if imageMultiSlot in self.layerViewerGuis.keys():
                layerViewerGui = self.layerViewerGuis[imageMultiSlot]
                self.viewerStack.removeWidget(layerViewerGui)
                self._viewerControlWidgetStack.removeWidget(
                    layerViewerGui.viewerControlWidget())
                layerViewerGui.stopAndCleanUp()

        self.topLevelOperator.Inputs.notifyRemove(bind(handleLaneRemoved))

    def _initAppletDrawerUic(self, drawerPath=None):
        """
        Load the ui file for the applet drawer, which we own.
        """
        if drawerPath is None:
            localDir = os.path.split(__file__)[0]
            drawerPath = os.path.join(localDir, "dataExportDrawer.ui")
        self.drawer = uic.loadUi(drawerPath)

        self.drawer.settingsButton.clicked.connect(self._chooseSettings)
        self.drawer.exportAllButton.clicked.connect(self.exportAllResults)
        self.drawer.exportAllButton.setIcon(QIcon(ilastikIcons.Save))
        self.drawer.deleteAllButton.clicked.connect(self.deleteAllResults)
        self.drawer.deleteAllButton.setIcon(QIcon(ilastikIcons.Clear))

        @threadRoutedWithRouter(self.threadRouter)
        def _handleNewSelectionNames(*args):
            input_names = self.topLevelOperator.SelectionNames.value
            self.drawer.inputSelectionCombo.addItems(input_names)

        self.topLevelOperator.SelectionNames.notifyDirty(
            _handleNewSelectionNames)
        _handleNewSelectionNames()

        self.drawer.inputSelectionCombo.currentIndexChanged.connect(
            self._handleInputComboSelectionChanged)

    def _handleInputComboSelectionChanged(self, index):
        assert index < len(self.topLevelOperator.SelectionNames.value)
        if self.drawer.inputSelectionCombo.currentText(
        ) == self.topLevelOperator.TableOnlyName.value:
            self.topLevelOperator.TableOnly.setValue(True)
        else:
            self.topLevelOperator.TableOnly.setValue(False)
            self.topLevelOperator.InputSelection.setValue(index)

    def initCentralUic(self):
        """
        Load the GUI from the ui file into this class and connect it with event handlers.
        """
        # Load the ui file into this class (find it in our own directory)
        localDir = os.path.split(__file__)[0]
        uic.loadUi(localDir + "/dataExport.ui", self)

        self.batchOutputTableWidget.resizeRowsToContents()
        self.batchOutputTableWidget.resizeColumnsToContents()
        self.batchOutputTableWidget.setAlternatingRowColors(True)
        self.batchOutputTableWidget.setShowGrid(False)
        self.batchOutputTableWidget.horizontalHeader().setResizeMode(
            0, QHeaderView.Interactive)

        self.batchOutputTableWidget.horizontalHeader().resizeSection(
            Column.Dataset, 200)
        self.batchOutputTableWidget.horizontalHeader().resizeSection(
            Column.ExportLocation, 250)
        self.batchOutputTableWidget.horizontalHeader().resizeSection(
            Column.Action, 100)

        self.batchOutputTableWidget.verticalHeader().hide()

        # Set up handlers
        self.batchOutputTableWidget.itemSelectionChanged.connect(
            self.handleTableSelectionChange)

        # Set up the viewer area
        self.initViewerStack()
        self.splitter.setSizes([150, 850])

    def initViewerStack(self):
        self.layerViewerGuis = {}
        self.viewerStack.addWidget(QWidget())

    def initViewerControls(self):
        self._viewerControlWidgetStack = QStackedWidget(parent=self)

    def showEvent(self, event):
        super(DataExportGui, self).showEvent(event)
        self.showSelectedDataset()

    def hideEvent(self, event):
        super(DataExportGui, self).hideEvent(event)

        # Make sure all 'on disk' layers are discarded so we aren't using those files any more.
        for opLaneView in self.topLevelOperator:
            opLaneView.cleanupOnDiskView()

    def _chooseSettings(self):
        opExportModelOp, opSubRegion = get_model_op(self.topLevelOperator)
        if opExportModelOp is None:
            QMessageBox.information(
                self, "Image not ready for export",
                "Export isn't possible yet: No images are ready for export.  "
                "Please configure upstream pipeline with valid settings, "
                "check that images were specified in the (batch) input applet and try again."
            )
            return

        settingsDlg = DataExportOptionsDlg(self, opExportModelOp)
        if settingsDlg.exec_() == DataExportOptionsDlg.Accepted:
            # Copy the settings from our 'model op' into the real op
            setting_slots = [
                opExportModelOp.RegionStart, opExportModelOp.RegionStop,
                opExportModelOp.InputMin, opExportModelOp.InputMax,
                opExportModelOp.ExportMin, opExportModelOp.ExportMax,
                opExportModelOp.ExportDtype, opExportModelOp.OutputAxisOrder,
                opExportModelOp.OutputFilenameFormat,
                opExportModelOp.OutputInternalPath,
                opExportModelOp.OutputFormat
            ]

            # Disconnect the special 'transaction' slot to prevent these
            #  settings from triggering many calls to setupOutputs.
            self.topLevelOperator.TransactionSlot.disconnect()

            for model_slot in setting_slots:
                real_inslot = getattr(self.topLevelOperator, model_slot.name)
                if model_slot.ready():
                    real_inslot.setValue(model_slot.value)
                else:
                    real_inslot.disconnect()

            # Re-connect the 'transaction' slot to apply all settings at once.
            self.topLevelOperator.TransactionSlot.setValue(True)

            # Discard the temporary model op
            opExportModelOp.cleanUp()
            opSubRegion.cleanUp()

            # Update the gui with the new export paths
            for index, slot in enumerate(self.topLevelOperator.ExportPath):
                self.updateTableForSlot(slot)

    def getSlotIndex(self, multislot, subslot):
        # Which index is this slot?
        for index, slot in enumerate(multislot):
            if slot == subslot:
                return index
        return -1

    @threadRouted
    def updateTableForSlot(self, slot):
        """
        Update the table row that corresponds to the given slot of the top-level operator (could be either input slot)
        """
        row = self.getSlotIndex(self.topLevelOperator.ExportPath, slot)
        assert row != -1, "Unknown input slot!"

        if not self.topLevelOperator.ExportPath[row].ready() or\
           not self.topLevelOperator.RawDatasetInfo[row].ready():
            return

        try:
            nickname = self.topLevelOperator.RawDatasetInfo[row].value.nickname
            exportPath = self.topLevelOperator.ExportPath[row].value
        except Slot.SlotNotReadyError:
            # Sadly, it is possible to get here even though we checked for .ready() immediately beforehand.
            # That's because the graph has a diamond-shaped DAG of connections, but the graph has no transaction mechanism
            # (It's therefore possible for RawDatasetInfo[row] to be ready() even though it's upstream partner is NOT ready.
            return

        self.batchOutputTableWidget.setItem(
            row, Column.Dataset,
            QTableWidgetItem(decode_to_qstring(nickname, 'utf-8')))
        self.batchOutputTableWidget.setItem(
            row, Column.ExportLocation,
            QTableWidgetItem(decode_to_qstring(exportPath)))

        exportNowButton = QPushButton("Export")
        exportNowButton.setToolTip("Generate individual batch output dataset.")
        exportNowButton.clicked.connect(
            bind(self.exportResultsForSlot, self.topLevelOperator[row]))
        self.batchOutputTableWidget.setCellWidget(row, Column.Action,
                                                  exportNowButton)

        # Select a row if there isn't one already selected.
        selectedRanges = self.batchOutputTableWidget.selectedRanges()
        if len(selectedRanges) == 0:
            self.batchOutputTableWidget.selectRow(0)

    def setEnabledIfAlive(self, widget, enable):
        if not sip.isdeleted(widget):
            widget.setEnabled(enable)

    def _updateExportButtons(self, *args):
        """Called when at least one dataset became 'unready', so we have to disable the export button."""
        all_ready = True
        # Enable/disable the appropriate export buttons in the table.
        # Use ThunkEvents to ensure that this happens in the Gui thread.
        for row, slot in enumerate(self.topLevelOperator.ImageToExport):
            all_ready &= slot.ready()
            export_button = self.batchOutputTableWidget.cellWidget(
                row, Column.Action)
            if export_button is not None:
                executable_event = ThunkEvent(
                    partial(self.setEnabledIfAlive, export_button,
                            slot.ready()))
                QApplication.instance().postEvent(self, executable_event)

        # Disable the "Export all" button unless all slots are ready.
        executable_event = ThunkEvent(
            partial(self.setEnabledIfAlive, self.drawer.exportAllButton,
                    all_ready))
        QApplication.instance().postEvent(self, executable_event)

    def handleTableSelectionChange(self):
        """
        Any time the user selects a new item, select the whole row.
        """
        self.selectEntireRow()
        self.showSelectedDataset()

    def selectEntireRow(self):
        # FIXME: There is a better way to do this...
        # Figure out which row is selected
        selectedItemRows = set()
        selectedRanges = self.batchOutputTableWidget.selectedRanges()
        for rng in selectedRanges:
            for row in range(rng.topRow(), rng.bottomRow() + 1):
                selectedItemRows.add(row)

        # Disconnect from selection change notifications while we do this
        self.batchOutputTableWidget.itemSelectionChanged.disconnect(
            self.handleTableSelectionChange)
        for row in selectedItemRows:
            self.batchOutputTableWidget.selectRow(row)

        # Reconnect now that we're finished
        self.batchOutputTableWidget.itemSelectionChanged.connect(
            self.handleTableSelectionChange)

    def exportSlots(self, laneViewList):
        try:
            # Set the busy flag so the workflow knows not to allow
            #  upstream changes or shell changes while we're exporting
            self.parentApplet.busy = True
            self.parentApplet.appletStateUpdateRequested.emit()

            # Disable our own gui
            QApplication.instance().postEvent(
                self,
                ThunkEvent(partial(self.setEnabledIfAlive, self.drawer,
                                   False)))
            QApplication.instance().postEvent(
                self, ThunkEvent(partial(self.setEnabledIfAlive, self, False)))

            # Start with 1% so the progress bar shows up
            self.progressSignal.emit(0)
            self.progressSignal.emit(1)

            def signalFileProgress(slotIndex, percent):
                self.progressSignal.emit(
                    (100 * slotIndex + percent) / len(laneViewList))

            # Client hook
            self.parentApplet.prepare_for_entire_export()

            for i, opLaneView in enumerate(laneViewList):
                lane_index = self.topLevelOperator.innerOperators.index(
                    opLaneView)
                logger.debug("Exporting result {}".format(i))

                # If the operator provides a progress signal, use it.
                slotProgressSignal = opLaneView.progressSignal
                slotProgressSignal.subscribe(partial(signalFileProgress, i))

                try:
                    # Client hook
                    self.parentApplet.prepare_lane_for_export(lane_index)

                    # Export the image
                    opLaneView.run_export()

                    # Client hook
                    if self.parentApplet.postprocessCanCheckForExistingFiles():
                        exportSuccessful = self.parentApplet.post_process_lane_export(
                            lane_index, checkOverwriteFiles=True)
                        if not exportSuccessful:
                            userSelection = [None]
                            self.showOverwriteQuestion(userSelection)
                            if userSelection[0]:
                                self.parentApplet.post_process_lane_export(
                                    lane_index, checkOverwriteFiles=False)
                    else:
                        self.parentApplet.post_process_lane_export(lane_index)

                except Exception as ex:
                    if opLaneView.ExportPath.ready():
                        msg = "Failed to generate export file: \n"
                        msg += opLaneView.ExportPath.value
                        msg += "\n{}".format(ex)
                    else:
                        msg = "Failed to generate export file."
                        msg += "\n{}".format(ex)
                    log_exception(logger, msg)
                    self.showExportError(msg)

                # We're finished with this file.
                self.progressSignal.emit(100 * (i + 1) /
                                         float(len(laneViewList)))

            # Client hook
            self.parentApplet.post_process_entire_export()

            # Ensure the shell knows we're really done.
            self.progressSignal.emit(100)
        except:
            # Cancel our progress.
            self.progressSignal.emit(0, True)
            raise
        finally:
            # We're not busy any more.  Tell the workflow.
            self.parentApplet.busy = False
            self.parentApplet.appletStateUpdateRequested.emit()

            # Re-enable our own gui
            QApplication.instance().postEvent(
                self,
                ThunkEvent(partial(self.setEnabledIfAlive, self.drawer, True)))
            QApplication.instance().postEvent(
                self, ThunkEvent(partial(self.setEnabledIfAlive, self, True)))

    def postProcessLane(self, lane_index):
        """
        Called immediately after the result for each lane is exported.
        Can be overridden by subclasses for post-processing purposes.
        """
        pass

    @threadRouted
    def showExportError(self, msg):
        QMessageBox.critical(self, "Failed to export", msg)

    @threadRouted
    def showOverwriteQuestion(self, userSelection):
        assert isinstance(userSelection, list)
        reply = QMessageBox.question(
            self, 'Warning!',
            'This filename already exists. Are you sure you want to overwrite?',
            QMessageBox.Yes, QMessageBox.No)
        if reply == QMessageBox.Yes:
            userSelection[0] = True
        else:
            userSelection[0] = False

    def exportResultsForSlot(self, opLane):
        # Make sure all 'on disk' layers are discarded so we aren't using those files any more.
        for opLaneView in self.topLevelOperator:
            opLaneView.cleanupOnDiskView()

        # Do this in a separate thread so the UI remains responsive
        exportThread = threading.Thread(target=bind(self.exportSlots,
                                                    [opLane]),
                                        name="DataExportThread")
        exportThread.start()

    def exportAllResults(self):
        # Make sure all 'on disk' layers are discarded so we aren't using those files any more.
        for opLaneView in self.topLevelOperator:
            opLaneView.cleanupOnDiskView()

        # Do this in a separate thread so the UI remains responsive
        exportThread = threading.Thread(target=bind(self.exportSlots,
                                                    self.topLevelOperator),
                                        name="DataExportThread")
        exportThread.start()

    def deleteAllResults(self):
        for innerOp in self.topLevelOperator:
            operatorView = innerOp
            operatorView.cleanupOnDiskView()
            pathComp = PathComponents(operatorView.ExportPath.value,
                                      operatorView.WorkingDirectory.value)
            if os.path.exists(pathComp.externalPath):
                os.remove(pathComp.externalPath)
            operatorView.setupOnDiskView()
            # we need to toggle the dirts state in order to enforce a frech dirty signal
            operatorView.Dirty.setValue(False)
            operatorView.Dirty.setValue(True)

    def showSelectedDataset(self):
        """
        Show the exported file in the viewer
        """
        # Get the selected row and corresponding slot value
        selectedRanges = self.batchOutputTableWidget.selectedRanges()
        if len(selectedRanges) == 0:
            return
        row = selectedRanges[0].topRow()

        # Hide all layers that come from the disk.
        for opLaneView in self.topLevelOperator:
            opLaneView.cleanupOnDiskView()

        # Activate the 'on disk' layers for this lane (if possible)
        opLane = self.topLevelOperator.getLane(row)
        opLane.setupOnDiskView()

        # Create if necessary
        imageMultiSlot = self.topLevelOperator.Inputs[row]
        if imageMultiSlot not in self.layerViewerGuis.keys():
            layerViewer = self.createLayerViewer(opLane)

            # Maximize the x-y view by default.
            layerViewer.volumeEditorWidget.quadview.ensureMaximized(2)

            self.layerViewerGuis[imageMultiSlot] = layerViewer
            self.viewerStack.addWidget(layerViewer)
            self._viewerControlWidgetStack.addWidget(
                layerViewer.viewerControlWidget())

        # Show the right one
        layerViewer = self.layerViewerGuis[imageMultiSlot]
        self.viewerStack.setCurrentWidget(layerViewer)
        self._viewerControlWidgetStack.setCurrentWidget(
            layerViewer.viewerControlWidget())

    def createLayerViewer(self, opLane):
        """
        This method provides an instance of LayerViewerGui for the given data lane.
        If this GUI class is subclassed, this method can be reimplemented to provide 
        custom layer types for the exported layers.
        """
        return DataExportLayerViewerGui(self.parentApplet, opLane)
class E4SideBar(QWidget):
    """
    Class implementing a sidebar with a widget area, that is hidden or shown, if the
    current tab is clicked again.
    """
    Version = 1
    
    North = 0
    East  = 1
    South = 2
    West  = 3
    
    def __init__(self, orientation = None, delay = 200, parent = None):
        """
        Constructor
        
        @param orientation orientation of the sidebar widget (North, East, South, West)
        @param delay value for the expand/shrink delay in milliseconds (integer)
        @param parent parent widget (QWidget)
        """
        QWidget.__init__(self, parent)
        
        self.__tabBar = QTabBar()
        self.__tabBar.setDrawBase(True)
        self.__tabBar.setShape(QTabBar.RoundedNorth)
        self.__tabBar.setUsesScrollButtons(True)
        self.__tabBar.setDrawBase(False)
        self.__stackedWidget = QStackedWidget(self)
        self.__stackedWidget.setContentsMargins(0, 0, 0, 0)
        self.__autoHideButton = QToolButton()
        self.__autoHideButton.setCheckable(True)
        self.__autoHideButton.setIcon(UI.PixmapCache.getIcon("autoHideOff.png"))
        self.__autoHideButton.setChecked(True)
        self.__autoHideButton.setToolTip(
            self.trUtf8("Deselect to activate automatic collapsing"))
        self.barLayout = QBoxLayout(QBoxLayout.LeftToRight)
        self.barLayout.setMargin(0)
        self.layout = QBoxLayout(QBoxLayout.TopToBottom)
        self.layout.setMargin(0)
        self.layout.setSpacing(0)
        self.barLayout.addWidget(self.__autoHideButton)
        self.barLayout.addWidget(self.__tabBar)
        self.layout.addLayout(self.barLayout)
        self.layout.addWidget(self.__stackedWidget)
        self.setLayout(self.layout)
        
        # initialize the delay timer
        self.__actionMethod = None
        self.__delayTimer = QTimer(self)
        self.__delayTimer.setSingleShot(True)
        self.__delayTimer.setInterval(delay)
        self.connect(self.__delayTimer, SIGNAL("timeout()"), self.__delayedAction)
        
        self.__minimized = False
        self.__minSize = 0
        self.__maxSize = 0
        self.__bigSize = QSize()
        
        self.splitter = None
        self.splitterSizes = []
        
        self.__hasFocus = False # flag storing if this widget or any child has the focus
        self.__autoHide = False
        
        self.__tabBar.installEventFilter(self)
        
        self.__orientation = E4SideBar.North
        if orientation is None:
            orientation = E4SideBar.North
        self.setOrientation(orientation)
        
        self.connect(self.__tabBar, SIGNAL("currentChanged(int)"), 
                     self.__stackedWidget, SLOT("setCurrentIndex(int)"))
        self.connect(e4App(), SIGNAL("focusChanged(QWidget*, QWidget*)"), 
                     self.__appFocusChanged)
        self.connect(self.__autoHideButton, SIGNAL("toggled(bool)"), 
                     self.__autoHideToggled)
    
    def setSplitter(self, splitter):
        """
        Public method to set the splitter managing the sidebar.
        
        @param splitter reference to the splitter (QSplitter)
        """
        self.splitter = splitter
        self.connect(self.splitter, SIGNAL("splitterMoved(int, int)"), 
                     self.__splitterMoved)
        self.splitter.setChildrenCollapsible(False)
        index = self.splitter.indexOf(self)
        self.splitter.setCollapsible(index, False)
    
    def __splitterMoved(self, pos, index):
        """
        Private slot to react on splitter moves.
        
        @param pos new position of the splitter handle (integer)
        @param index index of the splitter handle (integer)
        """
        if self.splitter:
            self.splitterSizes = self.splitter.sizes()
    
    def __delayedAction(self):
        """
        Private slot to handle the firing of the delay timer.
        """
        if self.__actionMethod is not None:
            self.__actionMethod()
    
    def setDelay(self, delay):
        """
        Public method to set the delay value for the expand/shrink delay in milliseconds.
        
        @param delay value for the expand/shrink delay in milliseconds (integer)
        """
        self.__delayTimer.setInterval(delay)
    
    def delay(self):
        """
        Public method to get the delay value for the expand/shrink delay in milliseconds.
        
        @return value for the expand/shrink delay in milliseconds (integer)
        """
        return self.__delayTimer.interval()
    
    def __cancelDelayTimer(self):
        """
        Private method to cancel the current delay timer.
        """
        self.__delayTimer.stop()
        self.__actionMethod = None
    
    def shrink(self):
        """
        Public method to record a shrink request.
        """
        self.__delayTimer.stop()
        self.__actionMethod = self.__shrinkIt
        self.__delayTimer.start()
   
    def __shrinkIt(self):
        """
        Private method to shrink the sidebar.
        """
        self.__minimized = True
        self.__bigSize = self.size()
        if self.__orientation in [E4SideBar.North, E4SideBar.South]:
            self.__minSize = self.minimumSizeHint().height()
            self.__maxSize = self.maximumHeight()
        else:
            self.__minSize = self.minimumSizeHint().width()
            self.__maxSize = self.maximumWidth()
        if self.splitter:
            self.splitterSizes = self.splitter.sizes()
        
        self.__stackedWidget.hide()
        
        if self.__orientation in [E4SideBar.North, E4SideBar.South]:
            self.setFixedHeight(self.__tabBar.minimumSizeHint().height())
        else:
            self.setFixedWidth(self.__tabBar.minimumSizeHint().width())
        
        self.__actionMethod = None
    
    def expand(self):
        """
        Private method to record a expand request.
        """
        self.__delayTimer.stop()
        self.__actionMethod = self.__expandIt
        self.__delayTimer.start()
    
    def __expandIt(self):
        """
        Public method to expand the sidebar.
        """
        self.__minimized = False
        self.__stackedWidget.show()
        self.resize(self.__bigSize)
        if self.__orientation in [E4SideBar.North, E4SideBar.South]:
            minSize = max(self.__minSize, self.minimumSizeHint().height())
            self.setMinimumHeight(minSize)
            self.setMaximumHeight(self.__maxSize)
        else:
            minSize = max(self.__minSize, self.minimumSizeHint().width())
            self.setMinimumWidth(minSize)
            self.setMaximumWidth(self.__maxSize)
        if self.splitter:
            self.splitter.setSizes(self.splitterSizes)
        
        self.__actionMethod = None
    
    def isMinimized(self):
        """
        Public method to check the minimized state.
        
        @return flag indicating the minimized state (boolean)
        """
        return self.__minimized
    
    def isAutoHiding(self):
        """
        Public method to check, if the auto hide function is active.
        
        @return flag indicating the state of auto hiding (boolean)
        """
        return self.__autoHide
    
    def eventFilter(self, obj, evt):
        """
        Protected method to handle some events for the tabbar.
        
        @param obj reference to the object (QObject)
        @param evt reference to the event object (QEvent)
        @return flag indicating, if the event was handled (boolean)
        """
        if obj == self.__tabBar:
            if evt.type() == QEvent.MouseButtonPress:
                pos = evt.pos()
                for i in range(self.__tabBar.count()):
                    if self.__tabBar.tabRect(i).contains(pos):
                        break
                
                if i == self.__tabBar.currentIndex():
                    if self.isMinimized():
                        self.expand()
                    else:
                        self.shrink()
                    return True
                elif self.isMinimized():
                    self.expand()
            elif evt.type() == QEvent.Wheel and not self.__stackedWidget.isHidden():
                if evt.delta() > 0:
                    self.prevTab()
                else:
                    self.nextTab()
                return True
        
        return QWidget.eventFilter(self, obj, evt)
    
    def addTab(self, widget, iconOrLabel, label = None):
        """
        Public method to add a tab to the sidebar.
        
        @param widget reference to the widget to add (QWidget)
        @param iconOrLabel reference to the icon or the labeltext of the tab
            (QIcon, string or QString)
        @param label the labeltext of the tab (string or QString) (only to be
            used, if the second parameter is a QIcon)
        """
        if label:
            index = self.__tabBar.addTab(iconOrLabel, label)
            self.__tabBar.setTabToolTip(index, label)
        else:
            index = self.__tabBar.addTab(iconOrLabel)
            self.__tabBar.setTabToolTip(index, iconOrLabel)
        self.__stackedWidget.addWidget(widget)
        if self.__orientation in [E4SideBar.North, E4SideBar.South]:
            self.__minSize = self.minimumSizeHint().height()
        else:
            self.__minSize = self.minimumSizeHint().width()
    
    def insertTab(self, index, widget, iconOrLabel, label = None):
        """
        Public method to insert a tab into the sidebar.
        
        @param index the index to insert the tab at (integer)
        @param widget reference to the widget to insert (QWidget)
        @param iconOrLabel reference to the icon or the labeltext of the tab
            (QIcon, string or QString)
        @param label the labeltext of the tab (string or QString) (only to be
            used, if the second parameter is a QIcon)
        """
        if label:
            index = self.__tabBar.insertTab(index, iconOrLabel, label)
            self.__tabBar.setTabToolTip(index, label)
        else:
            index = self.__tabBar.insertTab(index, iconOrLabel)
            self.__tabBar.setTabToolTip(index, iconOrLabel)
        self.__stackedWidget.insertWidget(index, widget)
        if self.__orientation in [E4SideBar.North, E4SideBar.South]:
            self.__minSize = self.minimumSizeHint().height()
        else:
            self.__minSize = self.minimumSizeHint().width()
    
    def removeTab(self, index):
        """
        Public method to remove a tab.
        
        @param index the index of the tab to remove (integer)
        """
        self.__stackedWidget.removeWidget(self.__stackedWidget.widget(index))
        self.__tabBar.removeTab(index)
        if self.__orientation in [E4SideBar.North, E4SideBar.South]:
            self.__minSize = self.minimumSizeHint().height()
        else:
            self.__minSize = self.minimumSizeHint().width()
    
    def clear(self):
        """
        Public method to remove all tabs.
        """
        while self.count() > 0:
            self.removeTab(0)
    
    def prevTab(self):
        """
        Public slot used to show the previous tab.
        """
        ind = self.currentIndex() - 1
        if ind == -1:
            ind = self.count() - 1
            
        self.setCurrentIndex(ind)
        self.currentWidget().setFocus()
    
    def nextTab(self):
        """
        Public slot used to show the next tab.
        """
        ind = self.currentIndex() + 1
        if ind == self.count():
            ind = 0
            
        self.setCurrentIndex(ind)
        self.currentWidget().setFocus()
    
    def count(self):
        """
        Public method to get the number of tabs.
        
        @return number of tabs in the sidebar (integer)
        """
        return self.__tabBar.count()
    
    def currentIndex(self):
        """
        Public method to get the index of the current tab.
        
        @return index of the current tab (integer)
        """
        return self.__stackedWidget.currentIndex()
    
    def setCurrentIndex(self, index):
        """
        Public slot to set the current index.
        
        @param index the index to set as the current index (integer)
        """
        self.__tabBar.setCurrentIndex(index)
        self.__stackedWidget.setCurrentIndex(index)
        if self.isMinimized():
            self.expand()
    
    def currentWidget(self):
        """
        Public method to get a reference to the current widget.
        
        @return reference to the current widget (QWidget)
        """
        return self.__stackedWidget.currentWidget()
    
    def setCurrentWidget(self, widget):
        """
        Public slot to set the current widget.
        
        @param widget reference to the widget to become the current widget (QWidget)
        """
        self.__stackedWidget.setCurrentWidget(widget)
        self.__tabBar.setCurrentIndex(self.__stackedWidget.currentIndex())
        if self.isMinimized():
            self.expand()
    
    def indexOf(self, widget):
        """
        Public method to get the index of the given widget.
        
        @param widget reference to the widget to get the index of (QWidget)
        @return index of the given widget (integer)
        """
        return self.__stackedWidget.indexOf(widget)
    
    def isTabEnabled(self, index):
        """
        Public method to check, if a tab is enabled.
        
        @param index index of the tab to check (integer)
        @return flag indicating the enabled state (boolean)
        """
        return self.__tabBar.isTabEnabled(index)
    
    def setTabEnabled(self, index, enabled):
        """
        Public method to set the enabled state of a tab.
        
        @param index index of the tab to set (integer)
        @param enabled enabled state to set (boolean)
        """
        self.__tabBar.setTabEnabled(index, enabled)
    
    def orientation(self):
        """
        Public method to get the orientation of the sidebar.
        
        @return orientation of the sidebar (North, East, South, West)
        """
        return self.__orientation
    
    def setOrientation(self, orient):
        """
        Public method to set the orientation of the sidebar.
        @param orient orientation of the sidebar (North, East, South, West)
        """
        if orient == E4SideBar.North:
            self.__tabBar.setShape(QTabBar.RoundedNorth)
            self.__tabBar.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
            self.barLayout.setDirection(QBoxLayout.LeftToRight)
            self.layout.setDirection(QBoxLayout.TopToBottom)
            self.layout.setAlignment(self.barLayout, Qt.AlignLeft)
        elif orient == E4SideBar.East:
            self.__tabBar.setShape(QTabBar.RoundedEast)
            self.__tabBar.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding)
            self.barLayout.setDirection(QBoxLayout.TopToBottom)
            self.layout.setDirection(QBoxLayout.RightToLeft)
            self.layout.setAlignment(self.barLayout, Qt.AlignTop)
        elif orient == E4SideBar.South:
            self.__tabBar.setShape(QTabBar.RoundedSouth)
            self.__tabBar.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
            self.barLayout.setDirection(QBoxLayout.LeftToRight)
            self.layout.setDirection(QBoxLayout.BottomToTop)
            self.layout.setAlignment(self.barLayout, Qt.AlignLeft)
        elif orient == E4SideBar.West:
            self.__tabBar.setShape(QTabBar.RoundedWest)
            self.__tabBar.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding)
            self.barLayout.setDirection(QBoxLayout.TopToBottom)
            self.layout.setDirection(QBoxLayout.LeftToRight)
            self.layout.setAlignment(self.barLayout, Qt.AlignTop)
        self.__orientation = orient
    
    def tabIcon(self, index):
        """
        Public method to get the icon of a tab.
        
        @param index index of the tab (integer)
        @return icon of the tab (QIcon)
        """
        return self.__tabBar.tabIcon(index)
    
    def setTabIcon(self, index, icon):
        """
        Public method to set the icon of a tab.
        
        @param index index of the tab (integer)
        @param icon icon to be set (QIcon)
        """
        self.__tabBar.setTabIcon(index, icon)
    
    def tabText(self, index):
        """
        Public method to get the text of a tab.
        
        @param index index of the tab (integer)
        @return text of the tab (QString)
        """
        return self.__tabBar.tabText(index)
    
    def setTabText(self, index, text):
        """
        Public method to set the text of a tab.
        
        @param index index of the tab (integer)
        @param text text to set (QString)
        """
        self.__tabBar.setTabText(index, text)
    
    def tabToolTip(self, index):
        """
        Public method to get the tooltip text of a tab.
        
        @param index index of the tab (integer)
        @return tooltip text of the tab (QString)
        """
        return self.__tabBar.tabToolTip(index)
    
    def setTabToolTip(self, index, tip):
        """
        Public method to set the tooltip text of a tab.
        
        @param index index of the tab (integer)
        @param tooltip text text to set (QString)
        """
        self.__tabBar.setTabToolTip(index, tip)
    
    def tabWhatsThis(self, index):
        """
        Public method to get the WhatsThis text of a tab.
        
        @param index index of the tab (integer)
        @return WhatsThis text of the tab (QString)
        """
        return self.__tabBar.tabWhatsThis(index)
    
    def setTabWhatsThis(self, index, text):
        """
        Public method to set the WhatsThis text of a tab.
        
        @param index index of the tab (integer)
        @param WhatsThis text text to set (QString)
        """
        self.__tabBar.setTabWhatsThis(index, text)
    
    def widget(self, index):
        """
        Public method to get a reference to the widget associated with a tab.
        
        @param index index of the tab (integer)
        @return reference to the widget (QWidget)
        """
        return self.__stackedWidget.widget(index)
    
    def saveState(self):
        """
        Public method to save the state of the sidebar.
        
        @return saved state as a byte array (QByteArray)
        """
        if len(self.splitterSizes) == 0:
            if self.splitter:
                self.splitterSizes = self.splitter.sizes()
            self.__bigSize = self.size()
            if self.__orientation in [E4SideBar.North, E4SideBar.South]:
                self.__minSize = self.minimumSizeHint().height()
                self.__maxSize = self.maximumHeight()
            else:
                self.__minSize = self.minimumSizeHint().width()
                self.__maxSize = self.maximumWidth()
        
        data = QByteArray()
        stream = QDataStream(data, QIODevice.WriteOnly)
        
        stream.writeUInt16(self.Version)
        stream.writeBool(self.__minimized)
        stream << self.__bigSize
        stream.writeUInt16(self.__minSize)
        stream.writeUInt16(self.__maxSize)
        stream.writeUInt16(len(self.splitterSizes))
        for size in self.splitterSizes:
            stream.writeUInt16(size)
        stream.writeBool(self.__autoHide)
        
        return data
    
    def restoreState(self, state):
        """
        Public method to restore the state of the sidebar.
        
        @param state byte array containing the saved state (QByteArray)
        @return flag indicating success (boolean)
        """
        if state.isEmpty():
            return False
        
        if self.__orientation in [E4SideBar.North, E4SideBar.South]:
            minSize = self.layout.minimumSize().height()
            maxSize = self.maximumHeight()
        else:
            minSize = self.layout.minimumSize().width()
            maxSize = self.maximumWidth()
        
        data = QByteArray(state)
        stream = QDataStream(data, QIODevice.ReadOnly)
        stream.readUInt16()  # version
        minimized = stream.readBool()
        
        if minimized:
            self.shrink()
        
        stream >> self.__bigSize
        self.__minSize = max(stream.readUInt16(), minSize)
        self.__maxSize = max(stream.readUInt16(), maxSize)
        count = stream.readUInt16()
        self.splitterSizes = []
        for i in range(count):
            self.splitterSizes.append(stream.readUInt16())
        
        self.__autoHide = stream.readBool()
        self.__autoHideButton.setChecked(not self.__autoHide)
        
        if not minimized:
            self.expand()
        
        return True
    
    #######################################################################
    ## methods below implement the autohide functionality
    #######################################################################
    
    def __autoHideToggled(self, checked):
        """
        Private slot to handle the toggling of the autohide button.
        
        @param checked flag indicating the checked state of the button (boolean)
        """
        self.__autoHide = not checked
        if self.__autoHide:
            self.__autoHideButton.setIcon(UI.PixmapCache.getIcon("autoHideOn.png"))
        else:
            self.__autoHideButton.setIcon(UI.PixmapCache.getIcon("autoHideOff.png"))
    
    def __appFocusChanged(self, old, now):
        """
        Private slot to handle a change of the focus.
        
        @param old reference to the widget, that lost focus (QWidget or None)
        @param now reference to the widget having the focus (QWidget or None)
        """
        self.__hasFocus = self.isAncestorOf(now)
        if self.__autoHide and not self.__hasFocus and not self.isMinimized():
            self.shrink()
        elif self.__autoHide and self.__hasFocus and self.isMinimized():
            self.expand()
    
    def enterEvent(self, event):
        """
        Protected method to handle the mouse entering this widget.
        
        @param event reference to the event (QEvent)
        """
        if self.__autoHide and self.isMinimized():
            self.expand()
        else:
            self.__cancelDelayTimer()
    
    def leaveEvent(self, event):
        """
        Protected method to handle the mouse leaving this widget.
        
        @param event reference to the event (QEvent)
        """
        if self.__autoHide and not self.__hasFocus and not self.isMinimized():
            self.shrink()
        else:
            self.__cancelDelayTimer()
    
    def shutdown(self):
        """
        Public method to shut down the object.
        
        This method does some preparations so the object can be deleted properly.
        It disconnects from the focusChanged signal in order to avoid trouble later
        on.
        """
        self.disconnect(e4App(), SIGNAL("focusChanged(QWidget*, QWidget*)"), 
                        self.__appFocusChanged)
Example #20
0
class SimulationPanel(QWidget):

    def __init__(self):
        QWidget.__init__(self)

        layout = QVBoxLayout()

        self._simulation_mode_combo = QComboBox()
        addHelpToWidget(self._simulation_mode_combo, "run/simulation_mode")

        self._simulation_mode_combo.currentIndexChanged.connect(self.toggleSimulationMode)

        simulation_mode_layout = QHBoxLayout()
        simulation_mode_layout.addSpacing(10)
        simulation_mode_layout.addWidget(QLabel("Simulation mode:"), 0, Qt.AlignVCenter)
        simulation_mode_layout.addWidget(self._simulation_mode_combo, 0, Qt.AlignVCenter)

        simulation_mode_layout.addSpacing(20)

        self.run_button = QToolButton()
        self.run_button.setIconSize(QSize(32, 32))
        self.run_button.setText("Start Simulation")
        self.run_button.setIcon(resourceIcon("ide/gear_in_play"))
        self.run_button.clicked.connect(self.runSimulation)
        self.run_button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
        addHelpToWidget(self.run_button, "run/start_simulation")

        simulation_mode_layout.addWidget(self.run_button)
        simulation_mode_layout.addStretch(1)

        layout.addSpacing(5)
        layout.addLayout(simulation_mode_layout)
        layout.addSpacing(10)

        self._simulation_stack = QStackedWidget()
        self._simulation_stack.setLineWidth(1)
        self._simulation_stack.setFrameStyle(QFrame.StyledPanel)

        layout.addWidget(self._simulation_stack)

        self._simulation_widgets = OrderedDict()
        """ :type: OrderedDict[BaseRunModel,SimulationConfigPanel]"""

        self.addSimulationConfigPanel(EnsembleExperimentPanel())
        self.addSimulationConfigPanel(EnsembleSmootherPanel())
        self.addSimulationConfigPanel(IteratedEnsembleSmootherPanel(advanced_option=True))
        self.addSimulationConfigPanel(MultipleDataAssimilationPanel())

        self.setLayout(layout)


    def addSimulationConfigPanel(self, panel):
        assert isinstance(panel, SimulationConfigPanel)

        panel.toggleAdvancedOptions(False)
        self._simulation_stack.addWidget(panel)

        simulation_model = panel.getSimulationModel()

        self._simulation_widgets[simulation_model] = panel

        if not panel.is_advanced_option:
            self._simulation_mode_combo.addItem(str(simulation_model), simulation_model)

        panel.simulationConfigurationChanged.connect(self.validationStatusChanged)


    def getActions(self):
        return []

    def toggleAdvancedOptions(self, show_advanced):
        current_model = self.getCurrentSimulationModel()

        self._simulation_mode_combo.clear()

        for model, panel in self._simulation_widgets.iteritems():
            if show_advanced or not panel.is_advanced_option:
                self._simulation_mode_combo.addItem(str(model), model)

        old_index = self._simulation_mode_combo.findText(str(current_model))
        self._simulation_mode_combo.setCurrentIndex(old_index if old_index > -1 else 0)

    def toggleAdvancedMode(self, show_advanced):
        for panel in self._simulation_widgets.values():
            panel.toggleAdvancedOptions(show_advanced)

        self.toggleAdvancedOptions(show_advanced)

    def getCurrentSimulationModel(self):
        data = self._simulation_mode_combo.itemData(self._simulation_mode_combo.currentIndex(), Qt.UserRole)
        return data.toPyObject()

    def getSimulationArguments(self):
        """ @rtype: dict[str,object]"""
        simulation_widget = self._simulation_widgets[self.getCurrentSimulationModel()]
        return simulation_widget.getSimulationArguments()


    def runSimulation(self):
        case_name = getCurrentCaseName()
        message = "Are you sure you want to use case '%s' for initialization of the initial ensemble when running the simulations?" % case_name
        start_simulations = QMessageBox.question(self, "Start simulations?", message, QMessageBox.Yes | QMessageBox.No )

        if start_simulations == QMessageBox.Yes:
            run_model = self.getCurrentSimulationModel()
            arguments = self.getSimulationArguments()
            dialog = RunDialog(run_model, arguments, self)
            dialog.startSimulation()
            dialog.exec_()

            ERT.emitErtChange() # simulations may have added new cases.


    def toggleSimulationMode(self):
        current_model = self.getCurrentSimulationModel()
        if current_model is not None:
            widget = self._simulation_widgets[self.getCurrentSimulationModel()]
            self._simulation_stack.setCurrentWidget(widget)
            self.validationStatusChanged()


    def validationStatusChanged(self):
        widget = self._simulation_widgets[self.getCurrentSimulationModel()]
        self.run_button.setEnabled(widget.isConfigurationValid())
Example #21
0
class Dialog(KDialog):
    def __init__(self, mainwin):
        KDialog.__init__(self, mainwin)
        self.jobs = []
        self.mainwin = mainwin
        self.setButtons(KDialog.ButtonCode(
            KDialog.Try | KDialog.Help |
            KDialog.Details | KDialog.Reset |
            KDialog.Ok | KDialog.Cancel))
        self.setButtonIcon(KDialog.Try, KIcon("run-lilypond"))
        self.setCaption(i18n("Create blank staff paper"))
        self.setHelp("blankpaper")
        self.setDefaultButton(KDialog.Ok)

        layout = QGridLayout(self.mainWidget())
        self.typeChooser = QComboBox()
        self.stack = QStackedWidget()
        StackFader(self.stack)
        paperSettings = QWidget(self)
        paperSettings.setLayout(QHBoxLayout())
        self.actionChooser = QComboBox(self)
        layout.addWidget(self.typeChooser, 0, 1)
        layout.addWidget(self.stack, 1, 0, 1, 3)
        layout.addWidget(self.actionChooser, 2, 1)
        l = QLabel(i18n("Type:"))
        l.setBuddy(self.typeChooser)
        layout.addWidget(l, 0, 0, Qt.AlignRight)
        l = QLabel(i18n("Action:"))
        l.setBuddy(self.actionChooser)
        layout.addWidget(l, 2, 0, Qt.AlignRight)
        
        # tool tips
        self.typeChooser.setToolTip(i18n(
            "Choose what kind of empty staves you want to create."))
        self.actionChooser.setToolTip(i18n(
            "Choose which action happens when clicking \"Ok\"."))
        self.setButtonToolTip(KDialog.Try, i18n(
            "Preview the empty staff paper."))
        self.setButtonToolTip(KDialog.Details, i18n(
            "Click to see more settings."))
        
        # paper stuff
        paper = QGroupBox(i18n("Paper"))
        paperSettings.layout().addWidget(paper)
        settings = QGroupBox(i18n("Settings"))
        paperSettings.layout().addWidget(settings)
        
        paper.setLayout(QGridLayout())
        
        self.paperSize = QComboBox()
        l = QLabel(i18n("Paper size:"))
        l.setBuddy(self.paperSize)
        paper.layout().addWidget(l, 0, 0, Qt.AlignRight)
        paper.layout().addWidget(self.paperSize, 0, 1)
        self.paperSize.addItem(i18n("Default"))
        self.paperSize.addItems(ly.paperSizes)

        self.staffSize = QSpinBox()
        l = QLabel(i18n("Staff Size:"))
        l.setBuddy(self.staffSize)
        paper.layout().addWidget(l, 1, 0, Qt.AlignRight)
        paper.layout().addWidget(self.staffSize, 1, 1)
        self.staffSize.setRange(8, 40)
        
        self.pageCount = QSpinBox()
        l = QLabel(i18n("Page count:"))
        l.setBuddy(self.pageCount)
        paper.layout().addWidget(l, 2, 0, Qt.AlignRight)
        paper.layout().addWidget(self.pageCount, 2, 1)
        self.pageCount.setRange(1, 1000)
        
        self.removeTagline = QCheckBox(i18n("Remove default tagline"))
        paper.layout().addWidget(self.removeTagline, 3, 0, 1, 2)
        
        settings.setLayout(QGridLayout())
        
        self.barLines = QCheckBox(i18n("Print Bar Lines"))
        self.barsPerLine = QSpinBox()
        l = QLabel(i18n("Bars per line:"))
        l.setBuddy(self.barsPerLine)
        settings.layout().addWidget(self.barLines, 0, 0, 1, 2)
        settings.layout().addWidget(l, 1, 0, Qt.AlignRight)
        settings.layout().addWidget(self.barsPerLine, 1, 1)
        self.barsPerLine.setRange(1, 20)
        
        self.pageNumbers = QCheckBox(i18n("Print Page Numbers"))
        self.pageNumStart = QSpinBox()
        l = QLabel(i18n("Start with:"))
        l.setBuddy(self.pageNumStart)
        settings.layout().addWidget(self.pageNumbers, 2, 0, 1, 2)
        settings.layout().addWidget(l, 3, 0, Qt.AlignRight)
        settings.layout().addWidget(self.pageNumStart, 3, 1)
        self.barLines.toggled.connect(self.barsPerLine.setEnabled)
        self.pageNumbers.toggled.connect(self.pageNumStart.setEnabled)
        
        # types
        self.typeWidgets = [
            SingleStaff(self),
            PianoStaff(self),
            OrganStaff(self),
            ChoirStaff(self),
            CustomStaff(self),
            ]
        for widget in self.typeWidgets:
            self.stack.addWidget(widget)
            self.typeChooser.addItem(widget.name())
        self.typeChooser.currentIndexChanged.connect(lambda index:
            self.stack.setCurrentWidget(self.typeWidgets[index]))

        self.actors = [
            PrintPDF,
            SavePDF,
            OpenPDF,
            CopyToEditor,
            ]
        for actor in self.actors:
            self.actionChooser.addItem(actor.name())
        
        self.setDetailsWidget(paperSettings)
        # cleanup on exit
        mainwin.aboutToClose.connect(self.cleanup)
        # buttons
        self.resetClicked.connect(self.default)
        self.tryClicked.connect(self.showPreview)
        self.setInitialSize(QSize(400, 240))
        self.default()
        self.loadSettings()
    
    def done(self, r):
        self.saveSettings()
        KDialog.done(self, r)
        if r:
            self.actors[self.actionChooser.currentIndex()](self)

    def default(self):
        """ Set everything to default """
        self.paperSize.setCurrentIndex(0)
        self.staffSize.setValue(22)
        self.pageCount.setValue(1)
        self.removeTagline.setChecked(False)
        self.barLines.setChecked(False)
        self.barsPerLine.setValue(4)
        self.barsPerLine.setEnabled(False)
        self.pageNumbers.setChecked(False)
        self.pageNumStart.setValue(1)
        self.pageNumStart.setEnabled(False)
        self.typeChooser.setCurrentIndex(0)
        self.actionChooser.setCurrentIndex(0)
        for widget in self.typeWidgets:
            widget.default()

    def loadSettings(self):
        conf = config()
        self.removeTagline.setChecked(conf.readEntry("remove tagline", False))
        action = conf.readEntry("action", "PrintPDF")
        for index, actor in enumerate(self.actors):
            if actor.__name__ == action:
                self.actionChooser.setCurrentIndex(index)
    
    def saveSettings(self):
        conf = config()
        conf.writeEntry("remove tagline", self.removeTagline.isChecked())
        action = self.actors[self.actionChooser.currentIndex()].__name__
        conf.writeEntry("action", action)
    
    def showPreview(self):
        self.previewDialog().showPreview(self.ly())

    @cacheresult
    def previewDialog(self):
        return PreviewDialog(self)

    def ly(self):
        """
        Return the LilyPond document to print the empty staff paper.
        """
        staff = self.stack.currentWidget()
        output = []
        version = lilyPondVersion()
        if version:
            output.append('\\version "{0}"\n'.format(version))
        output.append('#(set-global-staff-size {0})\n'.format(self.staffSize.value()))
        # paper section
        output.append('\\paper {')
        if self.paperSize.currentIndex() > 0:
            output.append('#(set-paper-size "{0}")'.format(ly.paperSizes[self.paperSize.currentIndex()-1]))
        if self.pageNumbers.isChecked():
            output.append('top-margin = 10\\mm')
            output.append('first-page-number = #{0}'.format(self.pageNumStart.value()))
            output.append('oddHeaderMarkup = \\markup \\fill-line {')
            output.append('\\strut')
            output.append("\\fromproperty #'page:page-number-string")
            output.append('}')
        else:
            output.append('top-margin = 16\\mm')
            output.append('oddHeaderMarkup = ##f')
        output.append('evenHeaderMarkup = ##f')
        if self.removeTagline.isChecked():
            output.append('bottom-margin = 16\\mm')
            output.append('oddFooterMarkup = ##f')
        else:
            output.append('bottom-margin = 10\\mm')
            output.append('oddFooterMarkup = \\markup \\abs-fontsize #6 \\fill-line {')
            tagline = config().readEntry("tagline",
                '\\with-url #"http://www.frescobaldi.org/" FRESCOBALDI.ORG')
            output.append('\\sans {{ {0} }}'.format(tagline))
            output.append('\\strut')
            output.append('}')
        output.append('evenFooterMarkup = ##f')
        output.append('ragged-last-bottom = ##f')
        output.append('ragged-right = ##f')
        output.append('}\n')
        # music expression
        output.append('music = \\repeat unfold {0} {{ % pages'.format(self.pageCount.value()))
        output.append('\\repeat unfold {0} {{ % systems'.format(staff.systemCount()))
        output.append('\\repeat unfold {0} {{ % bars'.format(
            self.barLines.isChecked() and self.barsPerLine.value() or 1))
        output.extend(('r1', '\\noBreak', '}', '\\break', '\\noPageBreak', '}', '\\pageBreak', '}\n'))

        # get the layout
        layout = LayoutContexts()
        layout.add("Score", '\\remove "Bar_number_engraver"')
        layout.add("Voice", "\\override Rest #'stencil = ##f")
        music = staff.music(layout)
        layout.addToStaffContexts('\\remove "Time_signature_engraver"')
        if not self.barLines.isChecked():
            layout.disableBarLines()
        # write it out
        output.append('\\layout {\nindent = #0')
        output.extend(layout.ly())
        output.append('}\n')
        
        # score
        output.append('\\score {')
        output.extend(music)
        output.append('}\n')
        return ly.indent.indent('\n'.join(output))

    def cleanup(self):
        for job in self.jobs[:]: # copy
            job.cleanup()
Example #22
0
class DataSelectionGui(QWidget):
    """
    Manages all GUI elements in the data selection applet.
    This class itself is the central widget and also owns/manages the applet drawer widgets.
    """

    ###########################################
    ### AppletGuiInterface Concrete Methods ###
    ###########################################

    def centralWidget(self):
        return self

    def appletDrawer(self):
        return self._drawer

    def menus(self):
        return []

    def viewerControlWidget(self):
        return self._viewerControlWidgetStack

    def setImageIndex(self, imageIndex):
        if imageIndex is not None:
            self.laneSummaryTableView.selectRow(imageIndex)
            for detailWidget in self._detailViewerWidgets:
                detailWidget.selectRow(imageIndex)

    def stopAndCleanUp(self):
        self._cleaning_up = True
        for editor in self.volumeEditors.values():
            self.viewerStack.removeWidget(editor)
            self._viewerControlWidgetStack.removeWidget(
                editor.viewerControlWidget())
            editor.stopAndCleanUp()
        self.volumeEditors.clear()

    def imageLaneAdded(self, laneIndex):
        if len(self.laneSummaryTableView.selectedIndexes()) == 0:
            self.laneSummaryTableView.selectRow(laneIndex)

        # We don't have any real work to do because this gui initiated the lane addition in the first place
        if self.guiMode != GuiMode.Batch:
            if (len(self.topLevelOperator.DatasetGroup) != laneIndex + 1):
                import warnings
                warnings.warn(
                    "DataSelectionGui.imageLaneAdded(): length of dataset multislot out of sync with laneindex [%s != %s + 1]"
                    % (len(self.topLevelOperator.DatasetGroup), laneIndex))

    def imageLaneRemoved(self, laneIndex, finalLength):
        # There's nothing to do here because the GUI already
        #  handles operator resizes via slot callbacks.
        pass

    def allowLaneSelectionChange(self):
        return False

    ###########################################
    ###########################################

    class UserCancelledError(Exception):
        # This exception type is raised when the user cancels the
        #  addition of dataset files in the middle of the process somewhere.
        # It isn't an error -- it's used for control flow.
        pass

    def __init__(self,
                 parentApplet,
                 dataSelectionOperator,
                 serializer,
                 instructionText,
                 guiMode=GuiMode.Normal,
                 max_lanes=None,
                 show_axis_details=False):
        """
        Constructor.
        
        :param dataSelectionOperator: The top-level operator.  Must be of type :py:class:`OpMultiLaneDataSelectionGroup`.
        :param serializer: The applet's serializer.  Must be of type :py:class:`DataSelectionSerializer`
        :param instructionText: A string to display in the applet drawer.
        :param guiMode: Either ``GuiMode.Normal`` or ``GuiMode.Batch``.  Currently, there is no difference between normal and batch mode.
        :param max_lanes: The maximum number of lanes that the user is permitted to add to this workflow.  If ``None``, there is no maximum.
        """
        super(DataSelectionGui, self).__init__()
        self._cleaning_up = False
        self.parentApplet = parentApplet
        self._max_lanes = max_lanes
        self._default_h5_volumes = {}
        self.show_axis_details = show_axis_details

        self._viewerControls = QWidget()
        self.topLevelOperator = dataSelectionOperator
        self.guiMode = guiMode
        self.serializer = serializer
        self.threadRouter = ThreadRouter(self)

        self._initCentralUic()
        self._initAppletDrawerUic(instructionText)

        self._viewerControlWidgetStack = QStackedWidget(self)

        def handleImageRemove(multislot, index, finalLength):
            # Remove the viewer for this dataset
            datasetSlot = self.topLevelOperator.DatasetGroup[index]
            if datasetSlot in self.volumeEditors.keys():
                editor = self.volumeEditors[datasetSlot]
                self.viewerStack.removeWidget(editor)
                self._viewerControlWidgetStack.removeWidget(
                    editor.viewerControlWidget())
                editor.stopAndCleanUp()

        self.topLevelOperator.DatasetGroup.notifyRemove(
            bind(handleImageRemove))

        opWorkflow = self.topLevelOperator.parent
        assert hasattr(opWorkflow.shell, 'onSaveProjectActionTriggered'), \
            "This class uses the IlastikShell.onSaveProjectActionTriggered function.  Did you rename it?"

    def _initCentralUic(self):
        """
        Load the GUI from the ui file into this class and connect it with event handlers.
        """
        # Load the ui file into this class (find it in our own directory)
        localDir = os.path.split(__file__)[0] + '/'
        uic.loadUi(localDir + "/dataSelection.ui", self)

        self._initTableViews()
        self._initViewerStack()
        self.splitter.setSizes([150, 850])

    def _initAppletDrawerUic(self, instructionText):
        """
        Load the ui file for the applet drawer, which we own.
        """
        localDir = os.path.split(__file__)[0] + '/'
        self._drawer = uic.loadUi(localDir + "/dataSelectionDrawer.ui")
        self._drawer.instructionLabel.setText(instructionText)

    def _initTableViews(self):
        self.fileInfoTabWidget.setTabText(0, "Summary")
        self.laneSummaryTableView.setModel(
            DataLaneSummaryTableModel(self, self.topLevelOperator))
        self.laneSummaryTableView.dataLaneSelected.connect(self.showDataset)
        self.laneSummaryTableView.addFilesRequested.connect(self.addFiles)
        self.laneSummaryTableView.addStackRequested.connect(self.addStack)
        self.laneSummaryTableView.removeLanesRequested.connect(
            self.handleRemoveLaneButtonClicked)

        # These two helper functions enable/disable an 'add files' button for a given role
        #  based on the the max lane index for that role and the overall permitted max_lanes
        def _update_button_status(viewer, role_index):
            if self._max_lanes:
                viewer.setEnabled(
                    self._findFirstEmptyLane(role_index) < self._max_lanes)

        def _handle_lane_added(button, role_index, lane_slot, lane_index):
            def _handle_role_slot_added(role_slot, added_slot_index, *args):
                if added_slot_index == role_index:
                    role_slot.notifyReady(
                        bind(_update_button_status, button, role_index))
                    role_slot.notifyUnready(
                        bind(_update_button_status, button, role_index))

            lane_slot[lane_index].notifyInserted(_handle_role_slot_added)

        self._retained = []  # Retain menus so they don't get deleted
        self._detailViewerWidgets = []
        for roleIndex, role in enumerate(
                self.topLevelOperator.DatasetRoles.value):
            detailViewer = DatasetDetailedInfoTableView(self)
            detailViewer.setModel(
                DatasetDetailedInfoTableModel(self, self.topLevelOperator,
                                              roleIndex))
            self._detailViewerWidgets.append(detailViewer)

            # Button
            detailViewer.addFilesRequested.connect(
                partial(self.addFiles, roleIndex))
            detailViewer.addStackRequested.connect(
                partial(self.addStack, roleIndex))
            detailViewer.addRemoteVolumeRequested.connect(
                partial(self.addDvidVolume, roleIndex))

            # Monitor changes to each lane so we can enable/disable the 'add lanes' button for each tab
            self.topLevelOperator.DatasetGroup.notifyInserted(
                bind(_handle_lane_added, detailViewer, roleIndex))
            self.topLevelOperator.DatasetGroup.notifyRemoved(
                bind(_update_button_status, detailViewer, roleIndex))

            # While we're at it, do the same for the buttons in the summary table, too
            self.topLevelOperator.DatasetGroup.notifyInserted(
                bind(_handle_lane_added,
                     self.laneSummaryTableView.addFilesButtons[roleIndex],
                     roleIndex))
            self.topLevelOperator.DatasetGroup.notifyRemoved(
                bind(_update_button_status,
                     self.laneSummaryTableView.addFilesButtons[roleIndex],
                     roleIndex))

            # Context menu
            detailViewer.replaceWithFileRequested.connect(
                partial(self.handleReplaceFile, roleIndex))
            detailViewer.replaceWithStackRequested.connect(
                partial(self.addStack, roleIndex))
            detailViewer.editRequested.connect(
                partial(self.editDatasetInfo, roleIndex))
            detailViewer.resetRequested.connect(
                partial(self.handleClearDatasets, roleIndex))

            # Drag-and-drop
            detailViewer.addFilesRequestedDrop.connect(
                partial(self.addFileNames, roleIndex=roleIndex))

            # Selection handling
            def showFirstSelectedDataset(_roleIndex, lanes):
                if lanes:
                    self.showDataset(lanes[0], _roleIndex)

            detailViewer.dataLaneSelected.connect(
                partial(showFirstSelectedDataset, roleIndex))

            self.fileInfoTabWidget.insertTab(roleIndex, detailViewer, role)

        self.fileInfoTabWidget.currentChanged.connect(self.handleSwitchTabs)
        self.fileInfoTabWidget.setCurrentIndex(0)

    def handleSwitchTabs(self, tabIndex):
        if tabIndex < len(self._detailViewerWidgets):
            roleIndex = tabIndex  # If summary tab is moved to the front, change this line.
            detailViewer = self._detailViewerWidgets[roleIndex]
            selectedLanes = detailViewer.selectedLanes
            if selectedLanes:
                self.showDataset(selectedLanes[0], roleIndex)

    def _initViewerStack(self):
        self.volumeEditors = {}
        self.viewerStack.addWidget(QWidget())

    def handleRemoveLaneButtonClicked(self):
        """
        The user clicked the "Remove" button.
        Remove the currently selected row(s) from both the GUI and the top-level operator.
        """
        # Figure out which lanes to remove
        selectedIndexes = self.laneSummaryTableView.selectedIndexes()
        rows = set()
        for modelIndex in selectedIndexes:
            rows.add(modelIndex.row())

        # Don't remove the last row, which is just buttons.
        rows.discard(self.laneSummaryTableView.model().rowCount() - 1)

        # Remove in reverse order so row numbers remain consistent
        for row in reversed(sorted(rows)):
            # Remove lanes from the operator.
            # The table model will notice the changes and update the rows accordingly.
            finalSize = len(self.topLevelOperator.DatasetGroup) - 1
            self.topLevelOperator.DatasetGroup.removeSlot(row, finalSize)

    @threadRouted
    def showDataset(self, laneIndex, roleIndex=None):
        if self._cleaning_up:
            return
        if laneIndex == -1:
            self.viewerStack.setCurrentIndex(0)
            return

        assert threading.current_thread().name == "MainThread"

        if laneIndex >= len(self.topLevelOperator.DatasetGroup):
            return
        datasetSlot = self.topLevelOperator.DatasetGroup[laneIndex]

        # Create if necessary
        if datasetSlot not in self.volumeEditors.keys():

            class DatasetViewer(LayerViewerGui):
                def moveToTop(self, roleIndex):
                    opLaneView = self.topLevelOperatorView
                    if not opLaneView.DatasetRoles.ready():
                        return
                    datasetRoles = opLaneView.DatasetRoles.value
                    if roleIndex >= len(datasetRoles):
                        return
                    roleName = datasetRoles[roleIndex]
                    try:
                        layerIndex = [l.name
                                      for l in self.layerstack].index(roleName)
                    except ValueError:
                        return
                    else:
                        self.layerstack.selectRow(layerIndex)
                        self.layerstack.moveSelectedToTop()

                def setupLayers(self):
                    opLaneView = self.topLevelOperatorView
                    if not opLaneView.DatasetRoles.ready():
                        return []
                    layers = []
                    datasetRoles = opLaneView.DatasetRoles.value
                    for roleIndex, slot in enumerate(opLaneView.ImageGroup):
                        if slot.ready():
                            roleName = datasetRoles[roleIndex]
                            layer = self.createStandardLayerFromSlot(slot)
                            layer.name = roleName
                            layers.append(layer)
                    return layers

            opLaneView = self.topLevelOperator.getLane(laneIndex)
            layerViewer = DatasetViewer(self.parentApplet,
                                        opLaneView,
                                        crosshair=False)

            # Maximize the x-y view by default.
            layerViewer.volumeEditorWidget.quadview.ensureMaximized(2)

            self.volumeEditors[datasetSlot] = layerViewer
            self.viewerStack.addWidget(layerViewer)
            self._viewerControlWidgetStack.addWidget(
                layerViewer.viewerControlWidget())

        # Show the right one
        viewer = self.volumeEditors[datasetSlot]
        displayedRole = self.fileInfoTabWidget.currentIndex()
        viewer.moveToTop(displayedRole)
        self.viewerStack.setCurrentWidget(viewer)
        self._viewerControlWidgetStack.setCurrentWidget(
            viewer.viewerControlWidget())

    def handleReplaceFile(self, roleIndex, startingLane):
        self.addFiles(roleIndex, startingLane)

    def addFiles(self, roleIndex, startingLane=None):
        """
        The user clicked the "Add File" button.
        Ask him to choose a file (or several) and add them to both
          the GUI table and the top-level operator inputs.
        """
        # Find the directory of the most recently opened image file
        mostRecentImageFile = PreferencesManager().get('DataSelection',
                                                       'recent image')
        if mostRecentImageFile is not None:
            defaultDirectory = os.path.split(mostRecentImageFile)[0]
        else:
            defaultDirectory = os.path.expanduser('~')

        # Launch the "Open File" dialog
        fileNames = self.getImageFileNamesToOpen(self, defaultDirectory)

        # If the user didn't cancel
        if len(fileNames) > 0:
            PreferencesManager().set('DataSelection', 'recent image',
                                     fileNames[0])
            try:
                self.addFileNames(fileNames, roleIndex, startingLane)
            except Exception as ex:
                log_exception(logger)
                QMessageBox.critical(self, "Error loading file", str(ex))

    @classmethod
    def getImageFileNamesToOpen(cls, parent_window, defaultDirectory):
        """
        Launch an "Open File" dialog to ask the user for one or more image files.
        """
        extensions = OpDataSelection.SupportedExtensions
        filter_strs = ["*." + x for x in extensions]
        filters = ["{filt} ({filt})".format(filt=x) for x in filter_strs]
        filt_all_str = "Image files (" + ' '.join(filter_strs) + ')'

        fileNames = []

        if ilastik_config.getboolean("ilastik", "debug"):
            # use Qt dialog in debug mode (more portable?)
            file_dialog = QFileDialog(parent_window, "Select Images")
            file_dialog.setOption(QFileDialog.DontUseNativeDialog, True)
            # do not display file types associated with a filter
            # the line for "Image files" is too long otherwise
            file_dialog.setFilters([filt_all_str] + filters)
            file_dialog.setNameFilterDetailsVisible(False)
            # select multiple files
            file_dialog.setFileMode(QFileDialog.ExistingFiles)
            file_dialog.setDirectory(defaultDirectory)

            if file_dialog.exec_():
                fileNames = file_dialog.selectedFiles()
        else:
            # otherwise, use native dialog of the present platform
            fileNames = QFileDialog.getOpenFileNames(parent_window,
                                                     "Select Images",
                                                     defaultDirectory,
                                                     filt_all_str)
        # Convert from QtString to python str
        fileNames = map(encode_from_qstring, fileNames)
        return fileNames

    def _findFirstEmptyLane(self, roleIndex):
        opTop = self.topLevelOperator

        # Determine the number of files this role already has
        # Search for the last valid value.
        firstNewLane = 0
        for laneIndex, slot in reversed(
                zip(range(len(opTop.DatasetGroup)), opTop.DatasetGroup)):
            if slot[roleIndex].ready():
                firstNewLane = laneIndex + 1
                break
        return firstNewLane

    def addFileNames(self, fileNames, roleIndex, startingLane=None, rois=None):
        """
        Add the given filenames to both the GUI table and the top-level operator inputs.
        If startingLane is None, the filenames will be *appended* to the role's list of files.
        
        If rois is provided, it must be a list of (start,stop) tuples (one for each fileName)
        """
        # What lanes will we touch?
        startingLane, endingLane = self._determineLaneRange(
            fileNames, roleIndex, startingLane)
        if startingLane is None:
            # Something went wrong.
            return

        # If we're only adding new lanes, NOT modifying existing lanes...
        adding_only = startingLane == len(self.topLevelOperator)

        # Create a list of DatasetInfos
        try:
            infos = self._createDatasetInfos(roleIndex, fileNames, rois)
        except DataSelectionGui.UserCancelledError:
            return

        # If no exception was thrown so far, set up the operator now
        loaded_all = self._configureOpWithInfos(roleIndex, startingLane,
                                                endingLane, infos)

        if loaded_all:
            # Now check the resulting slots.
            # If they should be copied to the project file, say so.
            self._reconfigureDatasetLocations(roleIndex, startingLane,
                                              endingLane)

            self._checkDataFormatWarnings(roleIndex, startingLane, endingLane)

            # If we succeeded in adding all images, show the first one.
            self.showDataset(startingLane, roleIndex)

        # Notify the workflow that we just added some new lanes.
        if adding_only:
            workflow = self.parentApplet.topLevelOperator.parent
            workflow.handleNewLanesAdded()

        # Notify the workflow that something that could affect applet readyness has occurred.
        self.parentApplet.appletStateUpdateRequested.emit()

        self.updateInternalPathVisiblity()

    def _determineLaneRange(self, fileNames, roleIndex, startingLane=None):
        """
        Determine which lanes should be configured if the user wants to add the 
            given fileNames to the specified role, starting at startingLane.
        If startingLane is None, assume the user wants to APPEND the 
            files to the role's slots.
        """
        if startingLane is None or startingLane == -1:
            startingLane = len(self.topLevelOperator.DatasetGroup)
            endingLane = startingLane + len(fileNames) - 1
        else:
            assert startingLane < len(self.topLevelOperator.DatasetGroup)
            max_files = len(self.topLevelOperator.DatasetGroup) - \
                    startingLane
            if len(fileNames) > max_files:
                msg = "You selected {num_selected} files for {num_slots} "\
                      "slots. To add new files use the 'Add new...' option "\
                      "in the context menu or the button in the last row."\
                              .format(num_selected=len(fileNames),
                                      num_slots=max_files)
                QMessageBox.critical(self, "Too many files", msg)
                return (None, None)
            endingLane = min(startingLane + len(fileNames) - 1,
                             len(self.topLevelOperator.DatasetGroup))

        if self._max_lanes and endingLane >= self._max_lanes:
            msg = "You may not add more than {} file(s) to this workflow.  Please try again.".format(
                self._max_lanes)
            QMessageBox.critical(self, "Too many files", msg)
            return (None, None)

        return (startingLane, endingLane)

    def _createDatasetInfos(self, roleIndex, filePaths, rois):
        """
        Create a list of DatasetInfos for the given filePaths and rois
        rois may be None, in which case it is ignored.
        """
        if rois is None:
            rois = [None] * len(filePaths)
        assert len(rois) == len(filePaths)

        infos = []
        for filePath, roi in zip(filePaths, rois):
            info = self._createDatasetInfo(roleIndex, filePath, roi)
            infos.append(info)
        return infos

    def _createDatasetInfo(self, roleIndex, filePath, roi):
        """
        Create a DatasetInfo object for the given filePath and roi.
        roi may be None, in which case it is ignored.
        """
        cwd = self.topLevelOperator.WorkingDirectory.value
        datasetInfo = DatasetInfo(filePath, cwd=cwd)
        datasetInfo.subvolume_roi = roi  # (might be None)

        absPath, relPath = getPathVariants(filePath, cwd)

        # If the file is in a totally different tree from the cwd,
        # then leave the path as absolute.  Otherwise, override with the relative path.
        if relPath is not None and len(os.path.commonprefix([cwd, absPath
                                                             ])) > 1:
            datasetInfo.filePath = relPath

        h5Exts = ['.ilp', '.h5', '.hdf5']
        if os.path.splitext(datasetInfo.filePath)[1] in h5Exts:
            datasetNames = self.getPossibleInternalPaths(absPath)
            if len(datasetNames) == 0:
                raise RuntimeError("HDF5 file %s has no image datasets" %
                                   datasetInfo.filePath)
            elif len(datasetNames) == 1:
                datasetInfo.filePath += str(datasetNames[0])
            else:
                # If exactly one of the file's datasets matches a user's previous choice, use it.
                if roleIndex not in self._default_h5_volumes:
                    self._default_h5_volumes[roleIndex] = set()
                previous_selections = self._default_h5_volumes[roleIndex]
                possible_auto_selections = previous_selections.intersection(
                    datasetNames)
                if len(possible_auto_selections) == 1:
                    datasetInfo.filePath += str(
                        list(possible_auto_selections)[0])
                else:
                    # Ask the user which dataset to choose
                    dlg = H5VolumeSelectionDlg(datasetNames, self)
                    if dlg.exec_() == QDialog.Accepted:
                        selected_index = dlg.combo.currentIndex()
                        selected_dataset = str(datasetNames[selected_index])
                        datasetInfo.filePath += selected_dataset
                        self._default_h5_volumes[roleIndex].add(
                            selected_dataset)
                    else:
                        raise DataSelectionGui.UserCancelledError()

        # Allow labels by default if this gui isn't being used for batch data.
        datasetInfo.allowLabels = (self.guiMode == GuiMode.Normal)
        return datasetInfo

    def _configureOpWithInfos(self, roleIndex, startingLane, endingLane,
                              infos):
        """
        Attempt to configure the specified role and lanes of the 
        top-level operator with the given DatasetInfos.
        
        Returns True if all lanes were configured successfully, or False if something went wrong.
        """
        opTop = self.topLevelOperator
        originalSize = len(opTop.DatasetGroup)

        # Resize the slot if necessary
        if len(opTop.DatasetGroup) < endingLane + 1:
            opTop.DatasetGroup.resize(endingLane + 1)

        # Configure each subslot
        for laneIndex, info in zip(range(startingLane, endingLane + 1), infos):
            try:
                self.topLevelOperator.DatasetGroup[laneIndex][
                    roleIndex].setValue(info)
            except DatasetConstraintError as ex:
                return_val = [False]
                # Give the user a chance to fix the problem
                self.handleDatasetConstraintError(info, info.filePath, ex,
                                                  roleIndex, laneIndex,
                                                  return_val)
                if not return_val[0]:
                    # Not successfully repaired.  Roll back the changes and give up.
                    opTop.DatasetGroup.resize(originalSize)
                    return False
            except OpDataSelection.InvalidDimensionalityError as ex:
                opTop.DatasetGroup.resize(originalSize)
                QMessageBox.critical(self,
                                     "Dataset has different dimensionality",
                                     ex.message)
                return False
            except Exception as ex:
                msg = "Wasn't able to load your dataset into the workflow.  See error log for details."
                log_exception(logger, msg)
                QMessageBox.critical(self, "Dataset Load Error", msg)
                opTop.DatasetGroup.resize(originalSize)
                return False

        return True

    def _reconfigureDatasetLocations(self, roleIndex, startingLane,
                                     endingLane):
        """
        Check the metadata for the given slots.  
        If the data is stored a format that is poorly optimized for 3D access, 
        then configure it to be copied to the project file.
        Finally, save the project if we changed something. 
        """
        save_needed = False
        opTop = self.topLevelOperator
        for lane_index in range(startingLane, endingLane + 1):
            output_slot = opTop.ImageGroup[lane_index][roleIndex]
            if output_slot.meta.prefer_2d and 'z' in output_slot.meta.axistags:
                shape = numpy.array(output_slot.meta.shape)
                total_volume = numpy.prod(shape)

                # Only copy to the project file if the total volume is reasonably small
                if total_volume < 0.5e9:
                    info_slot = opTop.DatasetGroup[lane_index][roleIndex]
                    info = info_slot.value
                    info.location = DatasetInfo.Location.ProjectInternal
                    info_slot.setValue(info, check_changed=False)
                    save_needed = True

        if save_needed:
            logger.info(
                "Some of your data cannot be accessed efficiently in 3D in its current format."
                "  It will now be copied to the project file.")
            opWorkflow = self.topLevelOperator.parent
            opWorkflow.shell.onSaveProjectActionTriggered()

    def _checkDataFormatWarnings(self, roleIndex, startingLane, endingLane):
        warn_needed = False
        opTop = self.topLevelOperator
        for lane_index in range(startingLane, endingLane + 1):
            output_slot = opTop.ImageGroup[lane_index][roleIndex]
            if output_slot.meta.inefficient_format:
                warn_needed = True

        if warn_needed:
            QMessageBox.warning(
                self, "Inefficient Data Format",
                "Your data cannot be accessed efficiently in its current format.  "
                "Check the console output for details.\n"
                "(For HDF5 files, be sure to enable chunking on your dataset.)"
            )

    @threadRouted
    def handleDatasetConstraintError(self,
                                     info,
                                     filename,
                                     ex,
                                     roleIndex,
                                     laneIndex,
                                     return_val=[False]):
        msg = "Can't use default properties for dataset:\n\n" + \
              filename + "\n\n" + \
              "because it violates a constraint of the {} applet.\n\n".format( ex.appletName ) + \
              ex.message + "\n\n" + \
              "Please enter valid dataset properties to continue."
        QMessageBox.warning(self, "Dataset Needs Correction", msg)

        # The success of this is 'returned' via our special out-param
        # (We can't return a value from this func because it is @threadRouted.
        successfully_repaired = self.repairDatasetInfo(info, roleIndex,
                                                       laneIndex)
        return_val[0] = successfully_repaired

    def repairDatasetInfo(self, info, roleIndex, laneIndex):
        """Open the dataset properties editor and return True if the new properties are acceptable."""
        defaultInfos = {}
        defaultInfos[laneIndex] = info
        editorDlg = DatasetInfoEditorWidget(
            self,
            self.topLevelOperator,
            roleIndex, [laneIndex],
            defaultInfos,
            show_axis_details=self.show_axis_details)
        dlg_state = editorDlg.exec_()
        return (dlg_state == QDialog.Accepted)

    @classmethod
    def getPossibleInternalPaths(cls, absPath, min_ndim=3, max_ndim=5):
        datasetNames = []
        # Open the file as a read-only so we can get a list of the internal paths
        with h5py.File(absPath, 'r') as f:
            # Define a closure to collect all of the dataset names in the file.
            def accumulateDatasetPaths(name, val):
                if type(val) == h5py._hl.dataset.Dataset and min_ndim <= len(
                        val.shape) <= max_ndim:
                    datasetNames.append('/' + name)

            # Visit every group/dataset in the file
            f.visititems(accumulateDatasetPaths)
        return datasetNames

    def addStack(self, roleIndex, laneIndex):
        """
        The user clicked the "Import Stack Files" button.
        """
        stackDlg = StackFileSelectionWidget(self)
        stackDlg.exec_()
        if stackDlg.result() != QDialog.Accepted:
            return
        files = stackDlg.selectedFiles
        sequence_axis = stackDlg.sequence_axis
        if len(files) == 0:
            return

        cwd = self.topLevelOperator.WorkingDirectory.value
        info = DatasetInfo(os.path.pathsep.join(files), cwd=cwd)

        originalNumLanes = len(self.topLevelOperator.DatasetGroup)

        if laneIndex is None or laneIndex == -1:
            laneIndex = len(self.topLevelOperator.DatasetGroup)
        if len(self.topLevelOperator.DatasetGroup) < laneIndex + 1:
            self.topLevelOperator.DatasetGroup.resize(laneIndex + 1)

        def importStack():
            self.parentApplet.busy = True
            self.parentApplet.appletStateUpdateRequested.emit()

            # Serializer will update the operator for us, which will propagate to the GUI.
            try:
                self.serializer.importStackAsLocalDataset(info, sequence_axis)
                try:
                    self.topLevelOperator.DatasetGroup[laneIndex][
                        roleIndex].setValue(info)
                except DatasetConstraintError as ex:
                    # Give the user a chance to repair the problem.
                    filename = files[0] + "\n...\n" + files[-1]
                    return_val = [False]
                    self.handleDatasetConstraintError(info, filename, ex,
                                                      roleIndex, laneIndex,
                                                      return_val)
                    if not return_val[0]:
                        # Not successfully repaired.  Roll back the changes and give up.
                        self.topLevelOperator.DatasetGroup.resize(
                            originalNumLanes)
            finally:
                self.parentApplet.busy = False
                self.parentApplet.appletStateUpdateRequested.emit()

        req = Request(importStack)
        req.notify_finished(
            lambda result: self.showDataset(laneIndex, roleIndex))
        req.notify_failed(
            partial(self.handleFailedStackLoad, files, originalNumLanes))
        req.submit()

    @threadRouted
    def handleFailedStackLoad(self, files, originalNumLanes, exc, exc_info):
        msg = "Failed to load stack due to the following error:\n{}".format(
            exc)
        msg += "\nAttempted stack files were:\n"
        msg += "\n".join(files)
        log_exception(logger, msg, exc_info)
        QMessageBox.critical(self, "Failed to load image stack", msg)
        self.topLevelOperator.DatasetGroup.resize(originalNumLanes)

    def handleClearDatasets(self, roleIndex, selectedRows):
        for row in selectedRows:
            self.topLevelOperator.DatasetGroup[row][roleIndex].disconnect()

        # Remove all operators that no longer have any connected slots
        laneIndexes = range(len(self.topLevelOperator.DatasetGroup))
        for laneIndex, multislot in reversed(
                zip(laneIndexes, self.topLevelOperator.DatasetGroup)):
            any_ready = False
            for slot in multislot:
                any_ready |= slot.ready()
            if not any_ready:
                self.topLevelOperator.DatasetGroup.removeSlot(
                    laneIndex,
                    len(self.topLevelOperator.DatasetGroup) - 1)

        # Notify the workflow that something that could affect applet readyness has occurred.
        self.parentApplet.appletStateUpdateRequested.emit()

    def editDatasetInfo(self, roleIndex, laneIndexes):
        editorDlg = DatasetInfoEditorWidget(
            self,
            self.topLevelOperator,
            roleIndex,
            laneIndexes,
            show_axis_details=self.show_axis_details)
        editorDlg.exec_()
        self.parentApplet.appletStateUpdateRequested.emit()

    def updateInternalPathVisiblity(self):
        for view in self._detailViewerWidgets:
            model = view.model()
            view.setColumnHidden(DatasetDetailedInfoColumn.InternalID,
                                 not model.hasInternalPaths())

    def addDvidVolume(self, roleIndex, laneIndex):
        # TODO: Provide list of recently used dvid hosts, loaded from user preferences
        recent_hosts_pref = PreferencesManager.Setting("DataSelection",
                                                       "Recent DVID Hosts")
        recent_hosts = recent_hosts_pref.get()
        if not recent_hosts:
            recent_hosts = ["localhost:8000"]
        recent_hosts = filter(lambda h: h, recent_hosts)

        from dvidDataSelectionBrowser import DvidDataSelectionBrowser
        browser = DvidDataSelectionBrowser(recent_hosts, parent=self)
        if browser.exec_() == DvidDataSelectionBrowser.Rejected:
            return

        if None in browser.get_selection():
            QMessageBox.critical("Couldn't use your selection.")
            return

        rois = None
        hostname, dset_uuid, volume_name, uuid = browser.get_selection()
        dvid_url = 'http://{hostname}/api/node/{uuid}/{volume_name}'.format(
            **locals())
        subvolume_roi = browser.get_subvolume_roi()

        # Relocate host to top of 'recent' list, and limit list to 10 items.
        try:
            i = recent_hosts.index(hostname)
            del recent_hosts[i]
        except ValueError:
            pass
        finally:
            recent_hosts.insert(0, hostname)
            recent_hosts = recent_hosts[:10]

        # Save pref
        recent_hosts_pref.set(recent_hosts)

        if subvolume_roi is None:
            self.addFileNames([dvid_url], roleIndex, laneIndex)
        else:
            # In ilastik, we display the dvid volume axes in C-order, despite the dvid convention of F-order
            # Transpose the subvolume roi to match
            # (see implementation of OpDvidVolume)
            start, stop = subvolume_roi
            start = tuple(reversed(start))
            stop = tuple(reversed(stop))
            self.addFileNames([dvid_url], roleIndex, laneIndex,
                              [(start, stop)])
Example #23
0
class CustomStaff(SymbolManager, QWidget):
    def __init__(self, dialog):
        QWidget.__init__(self, dialog)
        SymbolManager.__init__(self)
        self.setDefaultSymbolSize(40)
        self.dialog = dialog
        self.setLayout(QGridLayout())
        self.tree = QTreeWidget()
        self.stack = QStackedWidget()
        StackFader(self.stack)
        self.systems = QSpinBox()
        newStaves = QVBoxLayout()
        operations = QHBoxLayout()
        self.layout().addLayout(newStaves, 0, 0)
        self.layout().addWidget(self.tree, 0, 1)
        self.layout().addWidget(self.stack, 0, 2, 1, 2)
        self.layout().addWidget(self.systems, 1, 3)
        self.systems.setRange(1, 64)
        l = QLabel(i18n("Systems per page:"))
        l.setBuddy(self.systems)
        self.layout().addWidget(l, 1, 2, Qt.AlignRight)
        self.layout().addLayout(operations, 1, 1)
        
        operations.setSpacing(0)
        operations.setContentsMargins(0, 0, 0, 0)
        removeButton = KPushButton(KStandardGuiItem.remove())
        removeButton.clicked.connect(self.removeSelectedItems)
        operations.addWidget(removeButton)
        upButton = QToolButton()
        upButton.clicked.connect(self.moveSelectedItemsUp)
        upButton.setIcon(KIcon("go-up"))
        operations.addWidget(upButton)
        downButton = QToolButton()
        downButton.clicked.connect(self.moveSelectedItemsDown)
        downButton.setIcon(KIcon("go-down"))
        operations.addWidget(downButton)
        newStaves.setSpacing(0)
        newStaves.setContentsMargins(0, 0, 0, 0)
        
        self.tree.setIconSize(QSize(32, 32))
        self.tree.setDragDropMode(QTreeWidget.InternalMove)
        self.tree.headerItem().setHidden(True)
        self.tree.itemSelectionChanged.connect(self.slotSelectionChanged)
        
        for staffType in (
            BracketItem,
            BraceItem,
            StaffItem,
            ):
            b = QPushButton(staffType.name())
            b.clicked.connect((lambda t: lambda: self.createItem(t))(staffType))
            b.setIconSize(QSize(40, 40))
            self.addSymbol(b, staffType.symbol())
            newStaves.addWidget(b)
        
    def createItem(self, staffType):
        empty = self.tree.topLevelItemCount() == 0
        items = self.tree.selectedItems()
        if len(items) == 1 and items[0].flags() & Qt.ItemIsDropEnabled:
            parent = items[0]
        else:
            parent = self.tree
        item = staffType(self, parent)
        if empty:
            item.setSelected(True)
        
    def slotSelectionChanged(self):
        items = self.tree.selectedItems()
        if items:
            self.stack.setCurrentWidget(items[0].widget)
        
    def moveSelectedItemsUp(self):
        items = self.tree.selectedItems()
        if items:
            item = items[0] # currently we support only one selected item
            parent = item.parent() or self.tree.invisibleRootItem()
            index = parent.indexOfChild(item)
            if index > 0:
                parent.takeChild(index)
                parent.insertChild(index - 1, item)
                self.tree.clearSelection()
                item.setSelected(True)
                item.setExpanded(True)
    
    def moveSelectedItemsDown(self):
        items = self.tree.selectedItems()
        if items:
            item = items[0] # currently we support only one selected item
            parent = item.parent() or self.tree.invisibleRootItem()
            index = parent.indexOfChild(item)
            if index < parent.childCount() - 1:
                parent.takeChild(index)
                parent.insertChild(index + 1, item)
                self.tree.clearSelection()
                item.setSelected(True)
                item.setExpanded(True)
    
    def removeSelectedItems(self):
        for item in self.tree.selectedItems():
            item.remove()

    def systemCount(self):
        return self.systems.value()
    
    def name(self):
        return i18n("Custom Staff")
        
    def default(self):
        while self.tree.topLevelItemCount():
            self.tree.topLevelItem(0).remove()
        self.systems.setValue(4)
        
    def items(self):
        for i in range(self.tree.topLevelItemCount()):
            yield self.tree.topLevelItem(i)
    
    def music(self, layout):
        music = sum((i.music(layout) for i in self.items()), ['<<'])
        music.append('>>')
        return music
        
    def staffCount(self):
        return sum(i.staffCount() for i in self.items())
Example #24
0
class SimulationPanel(QWidget):

    def __init__(self):
        QWidget.__init__(self)

        layout = QVBoxLayout()

        simulation_mode_layout = QHBoxLayout()
        simulation_mode_layout.addSpacing(10)
        simulation_mode_model = SimulationModeModel()
        simulation_mode_model.observable().attach(SimulationModeModel.CURRENT_CHOICE_CHANGED_EVENT, self.toggleSimulationMode)
        simulation_mode_combo = ComboChoice(simulation_mode_model, "Simulation mode", "run/simulation_mode")
        simulation_mode_layout.addWidget(QLabel(simulation_mode_combo.getLabel()), 0, Qt.AlignVCenter)
        simulation_mode_layout.addWidget(simulation_mode_combo, 0, Qt.AlignVCenter)

        # simulation_mode_layout.addStretch()
        simulation_mode_layout.addSpacing(20)

        self.run_button = QToolButton()
        self.run_button.setIconSize(QSize(32, 32))
        self.run_button.setText("Start Simulation")
        self.run_button.setIcon(util.resourceIcon("ide/gear_in_play"))
        self.run_button.clicked.connect(self.runSimulation)
        self.run_button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
        HelpedWidget.addHelpToWidget(self.run_button, "run/start_simulation")

        simulation_mode_layout.addWidget(self.run_button)
        simulation_mode_layout.addStretch(1)

        layout.addSpacing(5)
        layout.addLayout(simulation_mode_layout)
        layout.addSpacing(10)

        self.simulation_stack = QStackedWidget()
        self.simulation_stack.setLineWidth(1)
        self.simulation_stack.setFrameStyle(QFrame.StyledPanel)

        layout.addWidget(self.simulation_stack)

        self.simulation_widgets = {}

        self.addSimulationConfigPanel(EnsembleExperimentPanel())
        self.addSimulationConfigPanel(EnsembleSmootherPanel())
        self.addSimulationConfigPanel(MultipleDataAssimilationPanel())
        self.addSimulationConfigPanel(IteratedEnsembleSmootherPanel())

        self.setLayout(layout)


    def addSimulationConfigPanel(self, panel):
        assert isinstance(panel, SimulationConfigPanel)

        panel.toggleAdvancedOptions(False)
        self.simulation_stack.addWidget(panel)
        self.simulation_widgets[panel.getSimulationModel()] = panel

        panel.simulationConfigurationChanged.connect(self.validationStatusChanged)


    def getActions(self):
        return []


    def toggleAdvancedMode(self, show_advanced):
        for panel in self.simulation_widgets.values():
            panel.toggleAdvancedOptions(show_advanced)


    def getCurrentSimulationMode(self):
        return SimulationModeModel().getCurrentChoice()


    def runSimulation(self):
        case_name = CaseSelectorModel().getCurrentChoice()
        message = "Are you sure you want to use case '%s' for initialization of the initial ensemble when running the simulations?" % case_name
        start_simulations = QMessageBox.question(self, "Start simulations?", message, QMessageBox.Yes | QMessageBox.No )

        if start_simulations == QMessageBox.Yes:
            run_model = self.getCurrentSimulationMode()

            dialog = RunDialog(run_model, self)
            dialog.startSimulation()
            dialog.exec_()

            CaseList().externalModificationNotification() # simulations may have added new cases.


    def toggleSimulationMode(self):
        widget = self.simulation_widgets[self.getCurrentSimulationMode()]
        self.simulation_stack.setCurrentWidget(widget)
        self.validationStatusChanged()


    def validationStatusChanged(self):
        widget = self.simulation_widgets[self.getCurrentSimulationMode()]
        self.run_button.setEnabled(widget.isConfigurationValid())
Example #25
0
class PlotScalesWidget(QWidget):
    plotScaleChanged = pyqtSignal()

    def __init__(self, type_key, title, select_min_time_value=False):
        QWidget.__init__(self)

        self.__type_key = type_key
        self.__type = None

        self.__double_spinner = self.createDoubleSpinner(minimum=-999999999.0, maximum=999999999.0)
        self.__integer_spinner = self.createIntegerSpinner(minimum=0, maximum=999999999)

        self.__time_map = ReportStepsModel().getList()
        self.__time_index_map = {}
        for index in range(len(self.__time_map)):
            time = self.__time_map[index]
            self.__time_index_map[time] = index

        self.__time_spinner = self.createTimeSpinner(select_minimum_value=select_min_time_value)

        layout = QVBoxLayout()
        self.setLayout(layout)

        self.__label = QLabel(title)
        self.__label.setAlignment(Qt.AlignHCenter)

        self.__stack = QStackedWidget()
        self.__stack.setSizePolicy(QSizePolicy(QSizePolicy.Preferred))
        self.__stack.addWidget(self.__integer_spinner)
        self.__stack.addWidget(self.__double_spinner)
        self.__stack.addWidget(self.__time_spinner)

        layout.addWidget(self.__stack)
        layout.addWidget(self.__label)

        self.setLayout(layout)

    def createDoubleSpinner(self, minimum, maximum):
        spinner = QDoubleSpinBox()
        spinner.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)
        spinner.setMinimumWidth(105)
        spinner.setRange(minimum, maximum)
        spinner.setKeyboardTracking(False)
        spinner.setDecimals(8)

        spinner.editingFinished.connect(self.plotScaleChanged)
        spinner.valueChanged.connect(self.plotScaleChanged)

        return spinner

    def createIntegerSpinner(self, minimum, maximum):
        spinner = QSpinBox()
        spinner.setMinimumWidth(75)
        spinner.setRange(minimum, maximum)
        spinner.setKeyboardTracking(False)

        spinner.editingFinished.connect(self.plotScaleChanged)
        spinner.valueChanged.connect(self.plotScaleChanged)

        return spinner

    def createTimeSpinner(self, select_minimum_value):
        def converter(item):
            return "%s" % (str(item.date()))

        spinner = ListSpinBox(self.__time_map)
        spinner.setMinimumWidth(75)

        if select_minimum_value:
            spinner.setValue(0)

        spinner.valueChanged[int].connect(self.plotScaleChanged)
        spinner.editingFinished.connect(self.plotScaleChanged)
        spinner.setStringConverter(converter)

        return spinner


    def getValue(self):
        if self.__type is int:
            return self.__integer_spinner.value()
        elif self.__type is float:
            return self.__double_spinner.value()
        elif self.__type is CTime:
            index = self.__time_spinner.value()
            return self.__time_map[index]
        else:
            raise TypeError("Unsupported spinner type: %s" % self.__type)


    def setValue(self, value):
        if value is not None:
            if self.__type is int:
                self.__integer_spinner.setValue(int(value))
            elif self.__type is float:
                self.__double_spinner.setValue(value)
            elif self.__type is CTime:
                index = self.__time_index_map[value]
                self.__time_spinner.setValue(index)
            else:
                raise TypeError("Unsupported spinner type: %s" % self.__type)


    def setFontSize(self, size):
        font = self.__double_spinner.font()
        font.setPointSize(size)
        self.__double_spinner.setFont(font)

        font = self.__integer_spinner.font()
        font.setPointSize(size)
        self.__integer_spinner.setFont(font)

        font = self.__time_spinner.font()
        font.setPointSize(size)
        self.__time_spinner.setFont(font)

        font = self.__label.font()
        font.setPointSize(size)
        self.__label.setFont(font)


    def setType(self, spinner_type):
        self.__type = spinner_type
        if spinner_type is int:
            self.__stack.setCurrentWidget(self.__integer_spinner)
        elif spinner_type is float:
            self.__stack.setCurrentWidget(self.__double_spinner)
        elif spinner_type is CTime:
            self.__stack.setCurrentWidget(self.__time_spinner)
        else:
            raise TypeError("Unsupported spinner type: %s" % spinner_type)


    def getType(self):
        return self.__type
class DataSelectionGui(QWidget):
    """
    Manages all GUI elements in the data selection applet.
    This class itself is the central widget and also owns/manages the applet drawer widgets.
    """

    ###########################################
    ### AppletGuiInterface Concrete Methods ###
    ###########################################

    def centralWidget( self ):
        return self

    def appletDrawer( self ):
        return self._drawer

    def menus( self ):
        return []

    def viewerControlWidget(self):
        return self._viewerControlWidgetStack

    def setImageIndex(self, imageIndex):
        if imageIndex is not None:
            self.laneSummaryTableView.selectRow(imageIndex)
            for detailWidget in self._detailViewerWidgets:
                detailWidget.selectRow(imageIndex)

    def stopAndCleanUp(self):
        for editor in self.volumeEditors.values():
            self.viewerStack.removeWidget( editor )
            self._viewerControlWidgetStack.removeWidget( editor.viewerControlWidget() )
            editor.stopAndCleanUp()
        self.volumeEditors.clear()

    def imageLaneAdded(self, laneIndex):
        if len(self.laneSummaryTableView.selectedIndexes()) == 0:
            self.laneSummaryTableView.selectRow(laneIndex)
        
        # We don't have any real work to do because this gui initiated the lane addition in the first place
        if self.guiMode != GuiMode.Batch:
            if(len(self.topLevelOperator.DatasetGroup) != laneIndex+1):
                import warnings
                warnings.warn("DataSelectionGui.imageLaneAdded(): length of dataset multislot out of sync with laneindex [%s != %s + 1]" % (len(self.topLevelOperator.DatasetGroup), laneIndex))

    def imageLaneRemoved(self, laneIndex, finalLength):
        # We assume that there's nothing to do here because THIS GUI initiated the lane removal
        if self.guiMode != GuiMode.Batch:
            assert len(self.topLevelOperator.DatasetGroup) == finalLength

    ###########################################
    ###########################################

    def __init__(self, parentApplet, dataSelectionOperator, serializer, instructionText, guiMode=GuiMode.Normal, max_lanes=None):
        """
        Constructor.
        
        :param dataSelectionOperator: The top-level operator.  Must be of type :py:class:`OpMultiLaneDataSelectionGroup`.
        :param serializer: The applet's serializer.  Must be of type :py:class:`DataSelectionSerializer`
        :param instructionText: A string to display in the applet drawer.
        :param guiMode: Either ``GuiMode.Normal`` or ``GuiMode.Batch``.  Currently, there is no difference between normal and batch mode.
        :param max_lanes: The maximum number of lanes that the user is permitted to add to this workflow.  If ``None``, there is no maximum.
        """
        super(DataSelectionGui, self).__init__()

        self.parentApplet = parentApplet
        self._max_lanes = max_lanes

        self._viewerControls = QWidget()
        self.topLevelOperator = dataSelectionOperator
        self.guiMode = guiMode
        self.serializer = serializer
        self.threadRouter = ThreadRouter(self)

        self._initCentralUic()
        self._initAppletDrawerUic(instructionText)
        
        self._viewerControlWidgetStack = QStackedWidget(self)

        def handleImageRemoved(multislot, index, finalLength):
            # Remove the viewer for this dataset
            imageSlot = self.topLevelOperator.Image[index]
            if imageSlot in self.volumeEditors.keys():
                editor = self.volumeEditors[imageSlot]
                self.viewerStack.removeWidget( editor )
                self._viewerControlWidgetStack.removeWidget( editor.viewerControlWidget() )
                editor.stopAndCleanUp()

        self.topLevelOperator.Image.notifyRemove( bind( handleImageRemoved ) )

    def _initCentralUic(self):
        """
        Load the GUI from the ui file into this class and connect it with event handlers.
        """
        # Load the ui file into this class (find it in our own directory)
        localDir = os.path.split(__file__)[0]+'/'
        uic.loadUi(localDir+"/dataSelection.ui", self)

        self._initTableViews()
        self._initViewerStack()
        self.splitter.setSizes( [150, 850] )

    def _initAppletDrawerUic(self, instructionText):
        """
        Load the ui file for the applet drawer, which we own.
        """
        localDir = os.path.split(__file__)[0]+'/'
        self._drawer = uic.loadUi(localDir+"/dataSelectionDrawer.ui")
        self._drawer.instructionLabel.setText( instructionText )

    def _initTableViews(self):
        self.fileInfoTabWidget.setTabText( 0, "Summary" )
        self.laneSummaryTableView.setModel( DataLaneSummaryTableModel(self, self.topLevelOperator) )
        self.laneSummaryTableView.dataLaneSelected.connect( self.showDataset )
        self.laneSummaryTableView.addFilesRequested.connect( self.addFiles )
        self.laneSummaryTableView.addStackRequested.connect( self.addStack )
        self.laneSummaryTableView.removeLanesRequested.connect( self.handleRemoveLaneButtonClicked )

        # These two helper functions enable/disable an 'add files' button for a given role  
        #  based on the the max lane index for that role and the overall permitted max_lanes
        def _update_button_status(viewer, role_index):
            if self._max_lanes:
                viewer.setEnabled( self._findFirstEmptyLane(role_index) < self._max_lanes )

        def _handle_lane_added( button, role_index, slot, lane_index ):
            slot[lane_index][role_index].notifyReady( bind(_update_button_status, button, role_index) )
            slot[lane_index][role_index].notifyUnready( bind(_update_button_status, button, role_index) )

        self._retained = [] # Retain menus so they don't get deleted
        self._detailViewerWidgets = []
        for roleIndex, role in enumerate(self.topLevelOperator.DatasetRoles.value):
            detailViewer = DatasetDetailedInfoTableView(self)
            detailViewer.setModel(DatasetDetailedInfoTableModel(self,
                self.topLevelOperator, roleIndex))
            self._detailViewerWidgets.append( detailViewer )

            # Button
            detailViewer.addFilesRequested.connect(
                    partial(self.addFiles, roleIndex))
            detailViewer.addStackRequested.connect(
                    partial(self.addStack, roleIndex))
            detailViewer.addRemoteVolumeRequested.connect(
                    partial(self.addDvidVolume, roleIndex))

            # Monitor changes to each lane so we can enable/disable the 'add lanes' button for each tab
            self.topLevelOperator.DatasetGroup.notifyInserted( bind( _handle_lane_added, detailViewer, roleIndex ) )
            self.topLevelOperator.DatasetGroup.notifyRemoved( bind( _update_button_status, detailViewer, roleIndex ) )
            
            # While we're at it, do the same for the buttons in the summary table, too
            self.topLevelOperator.DatasetGroup.notifyInserted( bind( _handle_lane_added, self.laneSummaryTableView.addFilesButtons[roleIndex], roleIndex ) )
            self.topLevelOperator.DatasetGroup.notifyRemoved( bind( _update_button_status, self.laneSummaryTableView.addFilesButtons[roleIndex], roleIndex ) )
            
            # Context menu
            detailViewer.replaceWithFileRequested.connect(
                    partial(self.handleReplaceFile, roleIndex) )
            detailViewer.replaceWithStackRequested.connect(
                    partial(self.addStack, roleIndex) )
            detailViewer.editRequested.connect(
                    partial(self.editDatasetInfo, roleIndex) )
            detailViewer.resetRequested.connect(
                    partial(self.handleClearDatasets, roleIndex) )

            # Drag-and-drop
            detailViewer.addFilesRequestedDrop.connect( partial( self.addFileNames, roleIndex=roleIndex ) )

            # Selection handling
            def showFirstSelectedDataset( _roleIndex, lanes ):
                if lanes:
                    self.showDataset( lanes[0], _roleIndex )
            detailViewer.dataLaneSelected.connect( partial(showFirstSelectedDataset, roleIndex) )

            self.fileInfoTabWidget.insertTab(roleIndex, detailViewer, role)
                
        self.fileInfoTabWidget.currentChanged.connect( self.handleSwitchTabs )
        self.fileInfoTabWidget.setCurrentIndex(0)

    def handleSwitchTabs(self, tabIndex ):
        if tabIndex < len(self._detailViewerWidgets):
            roleIndex = tabIndex # If summary tab is moved to the front, change this line.
            detailViewer = self._detailViewerWidgets[roleIndex]
            selectedLanes = detailViewer.selectedLanes
            if selectedLanes:
                self.showDataset( selectedLanes[0], roleIndex )

    def _initViewerStack(self):
        self.volumeEditors = {}
        self.viewerStack.addWidget( QWidget() )

    def handleRemoveLaneButtonClicked(self):
        """
        The user clicked the "Remove" button.
        Remove the currently selected row(s) from both the GUI and the top-level operator.
        """
        # Figure out which lanes to remove
        selectedIndexes = self.laneSummaryTableView.selectedIndexes()
        rows = set()
        for modelIndex in selectedIndexes:
            rows.add( modelIndex.row() )

        # Don't remove the last row, which is just buttons.
        rows.discard( self.laneSummaryTableView.model().rowCount()-1 )

        # Remove in reverse order so row numbers remain consistent
        for row in reversed(sorted(rows)):
            # Remove from the GUI
            self.laneSummaryTableView.model().removeRow(row)
            # Remove from the operator
            finalSize = len(self.topLevelOperator.DatasetGroup) - 1
            self.topLevelOperator.DatasetGroup.removeSlot(row, finalSize)
    
            # The gui and the operator should be in sync (model has one extra row for the button row)
            assert self.laneSummaryTableView.model().rowCount() == len(self.topLevelOperator.DatasetGroup)+1

    @threadRouted
    def showDataset(self, laneIndex, roleIndex=None):
        if laneIndex == -1:
            self.viewerStack.setCurrentIndex(0)
            return
        
        assert threading.current_thread().name == "MainThread"
        imageSlot = self.topLevelOperator.Image[laneIndex]

        # Create if necessary
        if imageSlot not in self.volumeEditors.keys():
            
            class DatasetViewer(LayerViewerGui):
                def moveToTop(self, roleIndex):
                    opLaneView = self.topLevelOperatorView
                    if not opLaneView.DatasetRoles.ready():
                        return
                    datasetRoles = opLaneView.DatasetRoles.value
                    if roleIndex >= len(datasetRoles):
                        return
                    roleName = datasetRoles[roleIndex]
                    try:
                        layerIndex = [l.name for l in self.layerstack].index(roleName)
                    except ValueError:
                        return
                    else:
                        self.layerstack.selectRow(layerIndex)
                        self.layerstack.moveSelectedToTop()

                def setupLayers(self):
                    opLaneView = self.topLevelOperatorView
                    if not opLaneView.DatasetRoles.ready():
                        return []
                    layers = []
                    datasetRoles = opLaneView.DatasetRoles.value
                    for roleIndex, slot in enumerate(opLaneView.ImageGroup):
                        if slot.ready():
                            roleName = datasetRoles[roleIndex]
                            layer = self.createStandardLayerFromSlot(slot)
                            layer.name = roleName
                            layers.append(layer)
                    return layers

            opLaneView = self.topLevelOperator.getLane(laneIndex)
            layerViewer = DatasetViewer(self.parentApplet, opLaneView, crosshair=False)
            
            # Maximize the x-y view by default.
            layerViewer.volumeEditorWidget.quadview.ensureMaximized(2)

            self.volumeEditors[imageSlot] = layerViewer
            self.viewerStack.addWidget( layerViewer )
            self._viewerControlWidgetStack.addWidget( layerViewer.viewerControlWidget() )

        # Show the right one
        viewer = self.volumeEditors[imageSlot]
        displayedRole = self.fileInfoTabWidget.currentIndex()
        viewer.moveToTop(displayedRole)
        self.viewerStack.setCurrentWidget( viewer )
        self._viewerControlWidgetStack.setCurrentWidget( viewer.viewerControlWidget() )


    def handleReplaceFile(self, roleIndex, startingLane):
        self.addFiles(roleIndex, startingLane)

    def addFiles(self, roleIndex, startingLane=None):
        """
        The user clicked the "Add File" button.
        Ask him to choose a file (or several) and add them to both
          the GUI table and the top-level operator inputs.
        """
        # Find the directory of the most recently opened image file
        mostRecentImageFile = PreferencesManager().get( 'DataSelection', 'recent image' )
        if mostRecentImageFile is not None:
            defaultDirectory = os.path.split(mostRecentImageFile)[0]
        else:
            defaultDirectory = os.path.expanduser('~')

        # Launch the "Open File" dialog
        fileNames = self.getImageFileNamesToOpen(defaultDirectory)

        # If the user didn't cancel
        if len(fileNames) > 0:
            PreferencesManager().set('DataSelection', 'recent image', fileNames[0])
            try:
                self.addFileNames(fileNames, roleIndex, startingLane)
            except RuntimeError as e:
                QMessageBox.critical(self, "Error loading file", str(e))

    def getImageFileNamesToOpen(self, defaultDirectory):
        """
        Launch an "Open File" dialog to ask the user for one or more image files.
        """
        file_dialog = QFileDialog(self, "Select Images")

        extensions = OpDataSelection.SupportedExtensions
        filter_strs = ["*." + x for x in extensions]
        filters = ["{filt} ({filt})".format(filt=x) for x in filter_strs]
        filt_all_str = "Image files (" + ' '.join(filter_strs) + ')'
        file_dialog.setFilters([filt_all_str] + filters)

        # do not display file types associated with a filter
        # the line for "Image files" is too long otherwise
        file_dialog.setNameFilterDetailsVisible(False)
        # select multiple files
        file_dialog.setFileMode(QFileDialog.ExistingFiles)
        if ilastik_config.getboolean("ilastik", "debug"):
            file_dialog.setOption(QFileDialog.DontUseNativeDialog, True)

        if file_dialog.exec_():
            fileNames = file_dialog.selectedFiles()
            # Convert from QtString to python str
            fileNames = map(encode_from_qstring, fileNames)
            return fileNames

        return []

    def _findFirstEmptyLane(self, roleIndex):
        opTop = self.topLevelOperator
        
        # Determine the number of files this role already has
        # Search for the last valid value.
        firstNewLane = 0
        for laneIndex, slot in reversed(zip(range(len(opTop.DatasetGroup)), opTop.DatasetGroup)):
            if slot[roleIndex].ready():
                firstNewLane = laneIndex+1
                break
        return firstNewLane

    def addFileNames(self, fileNames, roleIndex, startingLane=None):
        """
        Add the given filenames to both the GUI table and the top-level operator inputs.
        If startingLane is None, the filenames will be *appended* to the role's list of files.
        """
        infos = []

        if startingLane is None or startingLane == -1:
            startingLane = len(self.topLevelOperator.DatasetGroup)
            endingLane = startingLane+len(fileNames)-1
        else:
            assert startingLane < len(self.topLevelOperator.DatasetGroup)
            max_files = len(self.topLevelOperator.DatasetGroup) - \
                    startingLane
            if len(fileNames) > max_files:
                msg = "You selected {num_selected} files for {num_slots} "\
                      "slots. To add new files use the 'Add new...' option "\
                      "in the context menu or the button in the last row."\
                              .format(num_selected=len(fileNames),
                                      num_slots=max_files)
                QMessageBox.critical( self, "Too many files", msg )
                return
            endingLane = min(startingLane+len(fileNames)-1,
                    len(self.topLevelOperator.DatasetGroup))
            
        if self._max_lanes and endingLane >= self._max_lanes:
            msg = "You may not add more than {} file(s) to this workflow.  Please try again.".format( self._max_lanes )
            QMessageBox.critical( self, "Too many files", msg )
            return

        # Assign values to the new inputs we just allocated.
        # The GUI will be updated by callbacks that are listening to slot changes
        for i, filePath in enumerate(fileNames):
            datasetInfo = DatasetInfo()
            cwd = self.topLevelOperator.WorkingDirectory.value
            
            absPath, relPath = getPathVariants(filePath, cwd)
            
            # Relative by default, unless the file is in a totally different tree from the working directory.
            if relPath is not None and len(os.path.commonprefix([cwd, absPath])) > 1:
                datasetInfo.filePath = relPath
            else:
                datasetInfo.filePath = absPath
                
            datasetInfo.nickname = PathComponents(absPath).filenameBase

            h5Exts = ['.ilp', '.h5', '.hdf5']
            if os.path.splitext(datasetInfo.filePath)[1] in h5Exts:
                datasetNames = self.getPossibleInternalPaths( absPath )
                if len(datasetNames) > 0:
                    datasetInfo.filePath += str(datasetNames[0])
                else:
                    raise RuntimeError("HDF5 file %s has no image datasets" % datasetInfo.filePath)

            # Allow labels by default if this gui isn't being used for batch data.
            datasetInfo.allowLabels = ( self.guiMode == GuiMode.Normal )
            infos.append(datasetInfo)

        # if no exception was thrown, set up the operator now
        opTop = self.topLevelOperator
        originalSize = len(opTop.DatasetGroup)
            
        if len( opTop.DatasetGroup ) < endingLane+1:
            opTop.DatasetGroup.resize( endingLane+1 )
        for laneIndex, info in zip(range(startingLane, endingLane+1), infos):
            try:
                self.topLevelOperator.DatasetGroup[laneIndex][roleIndex].setValue( info )
            except DatasetConstraintError as ex:
                return_val = [False]
                # Give the user a chance to fix the problem
                self.handleDatasetConstraintError(info, info.filePath, ex, roleIndex, laneIndex, return_val)
                if not return_val[0]:
                    # Not successfully repaired.  Roll back the changes and give up.
                    opTop.DatasetGroup.resize( originalSize )
                    break
            except OpDataSelection.InvalidDimensionalityError as ex:
                    opTop.DatasetGroup.resize( originalSize )
                    QMessageBox.critical( self, "Dataset has different dimensionality", ex.message )
                    break
            except:
                QMessageBox.critical( self, "Dataset Load Error", "Wasn't able to load your dataset into the workflow.  See console for details." )
                opTop.DatasetGroup.resize( originalSize )
                raise

        # If we succeeded in adding all images, show the first one.
        if laneIndex == endingLane:
            self.showDataset(startingLane, roleIndex)

        # Notify the workflow that something that could affect applet readyness has occurred.
        self.parentApplet.appletStateUpdateRequested.emit()

        self.updateInternalPathVisiblity()

    @threadRouted
    def handleDatasetConstraintError(self, info, filename, ex, roleIndex, laneIndex, return_val=[False]):
        msg = "Can't use default properties for dataset:\n\n" + \
              filename + "\n\n" + \
              "because it violates a constraint of the {} applet.\n\n".format( ex.appletName ) + \
              ex.message + "\n\n" + \
              "Please enter valid dataset properties to continue."
        QMessageBox.warning( self, "Dataset Needs Correction", msg )
        
        # The success of this is 'returned' via our special out-param
        # (We can't return a value from this func because it is @threadRouted.
        successfully_repaired = self.repairDatasetInfo( info, roleIndex, laneIndex )
        return_val[0] = successfully_repaired

    def repairDatasetInfo(self, info, roleIndex, laneIndex):
        """Open the dataset properties editor and return True if the new properties are acceptable."""
        defaultInfos = {}
        defaultInfos[laneIndex] = info
        editorDlg = DatasetInfoEditorWidget(self, self.topLevelOperator, roleIndex, [laneIndex], defaultInfos)
        dlg_state = editorDlg.exec_()
        return ( dlg_state == QDialog.Accepted )

    def getPossibleInternalPaths(self, absPath):
        datasetNames = []
        # Open the file as a read-only so we can get a list of the internal paths
        with h5py.File(absPath, 'r') as f:
            # Define a closure to collect all of the dataset names in the file.
            def accumulateDatasetPaths(name, val):
                if type(val) == h5py._hl.dataset.Dataset and 3 <= len(val.shape) <= 5:
                    datasetNames.append( '/' + name )
            # Visit every group/dataset in the file
            f.visititems(accumulateDatasetPaths)
        return datasetNames

    def addStack(self, roleIndex, laneIndex):
        """
        The user clicked the "Import Stack Files" button.
        """
        stackDlg = StackFileSelectionWidget(self)
        stackDlg.exec_()
        if stackDlg.result() != QDialog.Accepted :
            return
        files = stackDlg.selectedFiles
        if len(files) == 0:
            return

        info = DatasetInfo()
        info.filePath = "//".join( files )
        prefix = os.path.commonprefix(files)
        info.nickname = PathComponents(prefix).filenameBase
        # Add an underscore for each wildcard digit
        num_wildcards = len(files[-1]) - len(prefix) - len( os.path.splitext(files[-1])[1] )
        info.nickname += "_"*num_wildcards

        # Allow labels by default if this gui isn't being used for batch data.
        info.allowLabels = ( self.guiMode == GuiMode.Normal )
        info.fromstack = True

        originalNumLanes = len(self.topLevelOperator.DatasetGroup)

        if laneIndex is None or laneIndex == -1:
            laneIndex = len(self.topLevelOperator.DatasetGroup)
        if len(self.topLevelOperator.DatasetGroup) < laneIndex+1:
            self.topLevelOperator.DatasetGroup.resize(laneIndex+1)

        def importStack():
            self.parentApplet.busy = True
            self.parentApplet.appletStateUpdateRequested.emit()

            # Serializer will update the operator for us, which will propagate to the GUI.
            try:
                self.serializer.importStackAsLocalDataset( info )
                try:
                    self.topLevelOperator.DatasetGroup[laneIndex][roleIndex].setValue(info)
                except DatasetConstraintError as ex:
                    # Give the user a chance to repair the problem.
                    filename = files[0] + "\n...\n" + files[-1]
                    return_val = [False]
                    self.handleDatasetConstraintError( info, filename, ex, roleIndex, laneIndex, return_val )
                    if not return_val[0]:
                        # Not successfully repaired.  Roll back the changes and give up.
                        self.topLevelOperator.DatasetGroup.resize(originalNumLanes)
            finally:
                self.parentApplet.busy = False
                self.parentApplet.appletStateUpdateRequested.emit()

        req = Request( importStack )
        req.notify_finished( lambda result: self.showDataset(laneIndex, roleIndex) )
        req.notify_failed( partial(self.handleFailedStackLoad, files, originalNumLanes ) )
        req.submit()

    @threadRouted
    def handleFailedStackLoad(self, files, originalNumLanes, exc, exc_info):
        import traceback
        traceback.print_tb(exc_info[2])
        msg = "Failed to load stack due to the following error:\n{}".format( exc )
        msg += "Attempted stack files were:"
        for f in files:
            msg += f + "\n"
        QMessageBox.critical(self, "Failed to load image stack", msg)
        self.topLevelOperator.DatasetGroup.resize(originalNumLanes)

    def handleClearDatasets(self, roleIndex, selectedRows):
        for row in selectedRows:
            self.topLevelOperator.DatasetGroup[row][roleIndex].disconnect()

        # Remove all operators that no longer have any connected slots        
        last_valid = -1
        laneIndexes = range( len(self.topLevelOperator.DatasetGroup) )
        for laneIndex, multislot in reversed(zip(laneIndexes, self.topLevelOperator.DatasetGroup)):
            any_ready = False
            for slot in multislot:
                any_ready |= slot.ready()
            if not any_ready:
                self.topLevelOperator.DatasetGroup.removeSlot( laneIndex, len(self.topLevelOperator.DatasetGroup)-1 )

        # Notify the workflow that something that could affect applet readyness has occurred.
        self.parentApplet.appletStateUpdateRequested.emit()

    def editDatasetInfo(self, roleIndex, laneIndexes):
        editorDlg = DatasetInfoEditorWidget(self, self.topLevelOperator, roleIndex, laneIndexes)
        editorDlg.exec_()

    def updateInternalPathVisiblity(self):
        for view in self._detailViewerWidgets:
            model = view.model()
            view.setColumnHidden(DatasetDetailedInfoColumn.InternalID,
                                 not model.hasInternalPaths())
    
    def addDvidVolume(self, roleIndex, laneIndex):
        # TODO: Provide list of recently used dvid hosts, loaded from user preferences
        from dvidclient.gui.contents_browser import ContentsBrowser
        browser = ContentsBrowser(["localhost:8000"], parent=self)
        if browser.exec_() == ContentsBrowser.Rejected:
            return

        hostname, dset_index, volume_name, uuid = browser.get_selection()
        dvid_url = 'http://{hostname}/api/node/{uuid}/{volume_name}'.format( **locals() )        
        self.addFileNames([dvid_url], roleIndex, laneIndex)
Example #27
0
class QMPCApp(QObject):
    __images__ = {
        'background':           "images/background.png",
    }
    
    __icons__  = {
        'homeButton':           "general_backspace",
        'volumeButton':         "general_speaker",
        'settingsButton':       "keyboard_menu",
        'prevButton':           "/etc/hildon/theme/mediaplayer/Back.png",
        'prevButtonPressed':    "/etc/hildon/theme/mediaplayer/BackPressed.png",
        'playButton':           "/etc/hildon/theme/mediaplayer/Play.png",
        'pauseButton':          "/etc/hildon/theme/mediaplayer/Pause.png",
        'stopButton':           "/etc/hildon/theme/mediaplayer/Stop.png",
        'stopButtonPressed':    "/etc/hildon/theme/mediaplayer/StopPressed.png",
        'nextButton':           "/etc/hildon/theme/mediaplayer/Forward.png",
        'nextButtonPressed':    "/etc/hildon/theme/mediaplayer/ForwardPressed.png",
        'repeatButton':         "/etc/hildon/theme/mediaplayer/Repeat.png",
        'repeatButtonPressed':  "/etc/hildon/theme/mediaplayer/RepeatPressed.png",
        'shuffleButton':        "/etc/hildon/theme/mediaplayer/Shuffle.png",
        'shuffleButtonPressed': "/etc/hildon/theme/mediaplayer/ShufflePressed.png",
    }

    def __init__(self):
        super(QMPCApp,self).__init__()
        self.mw = None
        self.initData()
        self.initMPD()
        self.initActions()
        self.initGUI()
        QTimer.singleShot(100,self.deferredStart)

    def deferredStart(self):
        if self.data.autoconnect: self.connectMPD()

    def initData(self):
        self.selectedServerName = None
        self.data = DataModel()
        self.data.loadSettings()
        self.imagehelper = ImageHelper(images=self.__images__,
                                       icons=self.__icons__)
        QApplication.instance().aboutToQuit.connect(self.data.saveSettings)

    def initActions(self):
        self.actionPlayer = QAction("Player", self)
        self.actionPlayer.triggered.connect(
            lambda: self.showWidget(self.player))
        self.actionPlaylist = QAction("Playlist",self)
        self.actionPlaylist.triggered.connect(
            lambda: self.showWidget(self.playlist))
        self.actionBrowser = QAction("Browser",self)
        self.actionBrowser.triggered.connect(
            lambda: self.showWidget(self.browser))

        self.actionOutputs = QAction("Outputs",self)
        self.actionOutputs.triggered.connect(self.showOutputs)
        self.actionStats = QAction("Statistics",self)
        self.actionStats.triggered.connect(self.showStats)
        self.actionPrefs = QAction("Preferences",self)
        self.actionPrefs.triggered.connect(self.showPrefs)
        self.actionConnect = QAction("Connect",self)
        self.actionConnect.triggered.connect(self.connectActivated)

    def initGUI(self):
        self.initStartScreen()
        self.initWidgets()
        
        if not have_maemo:
            self.mw     = QMainWindow()
            menu        = self.mw.menuBar()
            menufile    = menu.addMenu("&File")
            menuwindows = menu.addMenu("&Windows")
            self.mw.statusBar()
            menuwindows.addAction(self.actionPlayer)
            menuwindows.addAction(self.actionPlaylist)
            menuwindows.addAction(self.actionBrowser)
            menuwindows.addAction(self.actionOutputs)
            menuwindows.addAction(self.actionStats)
            menufile.addAction(self.actionConnect)
            menufile.addAction(self.actionPrefs)
            menufile.addSeparator()
            menufile.addAction("&Quit", QApplication.quit)
       
        self.setConnectionState(False)

    def initStartScreen(self):
        self.startscreen = StartScreen(self)
        self.startscreen.clicked.connect(self.connectActivated)
        if have_maemo:
            menu = QMenuBar(self.startscreen)
            menu.addAction(self.actionConnect)
            menu.addAction(self.actionPrefs)
            
    def initWidgets(self):
        # create subwidgets
        self.player   = Player(self)
        self.playlist = Playlist(self)
        self.browser  = Browser(self)
        if have_maemo:
            # build Maemo stack hierarchy
            self.playlist.setParent(self.player)
            self.browser.setParent(self.player)
            for w in [ self.player, self.playlist, self.browser ]:
                w.setAttribute( Qt.WA_Maemo5StackedWindow)
                w.setWindowFlags( w.windowFlags() | Qt.Window)

            # add menu bar
            menu = QMenuBar(self.player)
            menu.addAction(self.actionPlaylist)
            menu.addAction(self.actionBrowser)
            menu.addAction(self.actionStats)
            menu.addAction(self.actionOutputs)
            menu.addAction(self.actionPrefs)
            menu.addAction(self.actionConnect)
        else:
            self.stack = QStackedWidget()
            for w in [ self.player, self.playlist, self.browser ]:
                w.setParent(self.stack)
                self.stack.addWidget(w)

    def switchView(self, connected):
        if have_maemo:
            if connected:
                self.player.show()
                self.startscreen.hide()
            else:
                self.startscreen.show()
        else:
            cw = self.mw.centralWidget()
            if cw:
                cw.setParent(None)
                cw.hide()
            if connected:
                self.mw.setCentralWidget(self.stack)
                self.stack.show()
                self.showWidget(self.player)
            else:
                self.mw.setCentralWidget(self.startscreen)
                self.startscreen.show()
            self.mw.show()

    def showWidget(self,widget):
        if not have_maemo:
            self.stack.setCurrentWidget(widget)
        widget.show()

    def connectActivated(self):
        if self.actionConnect.text() == "Connect":
            self.connectMPD()
        else:
            self.disconnectMPD()

    def initMPD(self):
        self.mpd = MPDWrapper()
        self.mpdtimer = None
    
    def connectMPD(self,reconnect=False):
        selected = self.data.selectedServer()
        if not len(selected):
            InformationBox.information( self.mw, "Select server to connect")
            self.showPrefs()

        selected = self.data.selectedServer()
        if len(selected):
            name, address, port = selected
            try:
                if not reconnect:
                    InformationBox.information(
                        self.mw, "Connecting to <b>%s</b>" % name)
                    QApplication.processEvents()
                self.mpd.timeout = 10
                self.mpd.connect( str(address), int(port))
                if not reconnect:
                    InformationBox.information(
                        self.mw, "Connected to <b>%s</b>" % name)
                    QApplication.processEvents()
                    self.setConnectionState(True)
                    self.selectedServerName = name
                self.mpdtimer = self.startTimer(5000)
            except socket.timeout, e:
                self.setConnectionState(False)
                InformationBox.information( self.mw, "%s: %s" %(name,e))
                QApplication.processEvents()
            except socket.gaierror, e:
                self.setConnectionState(False)
                InformationBox.information( self.mw, "%s: %s" %(name,e[1]))
                QApplication.processEvents()
            except socket.error, e:
                self.setConnectionState(False)
                InformationBox.information( self.mw, "%s: %s" %(name,e[1]))
                QApplication.processEvents()