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()
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())
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)
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)])
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)
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()
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())
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())
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())
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)
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 )
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
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")
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 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())
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)
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())
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()
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)])
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())
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())
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)
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()