class TableWidget(QWidget): def __init__(self): super(TableWidget, self).__init__() vbox = QVBoxLayout(self) vbox.setContentsMargins(0, 0, 0, 0) self.relations = {} ## Splitter #self._splitter = QSplitter() ## List of names of tables/relations #self._list_tables = QListWidget() #self._splitter.addWidget(self._list_tables) ## Stack self.stacked = QStackedWidget() vbox.addWidget(self.stacked) #vbox.addWidget(self._splitter) #self.connect(self._list_tables, SIGNAL("currentRowChanged(int)"), #self.__change_table) #def __change_table(self, index): #self.stacked.setCurrentIndex(index) def count(self): return self.stacked.count() def add_table(self, rows, columns, name, data): table = QTableWidget() table.setRowCount(rows) table.setColumnCount(columns) for k, v in list(data.items()): item = QTableWidgetItem() item.setText(v) if k[0] == 0: table.setHorizontalHeaderItem(k[1], item) else: table.setItem(k[0] - 1, k[1], item) #ntuples = " [ " + str(rows) + " ]" #item_list = QListWidgetItem(name + ntuples) #item_list.setTextAlignment(Qt.AlignHCenter) #self._list_tables.addItem(item_list) self.stacked.addWidget(table) self.stacked.setCurrentIndex(self.stacked.count() - 1) def add_new_table(self, rel, name): import itertools table = QTableWidget() table.setRowCount(0) table.setColumnCount(0) data = itertools.chain([rel.fields], rel.content) for row_data in data: row = table.rowCount() table.setColumnCount(len(row_data)) for col, text in enumerate(row_data): item = QTableWidgetItem() item.setText(text) if row == 0: table.setHorizontalHeaderItem(col, item) else: table.setItem(row - 1, col, item) table.insertRow(row) table.removeRow(table.rowCount() - 1) self.stacked.addWidget(table) self.stacked.setCurrentIndex(self.stacked.count() - 1) lateral = Pireal.get_service("lateral") lateral.add_item_list([name]) def remove_table(self, index): table = self.stacked.widget(index) self.stacked.removeWidget(table) def add_table_from_file(self): pass
class ComboTabWidget(QWidget): def __init__(self, parent): super(ComboTabWidget, self).__init__(parent) layout = QVBoxLayout(self) layout.setSpacing(0) self.switchCombo = QComboBox(self) layout.addWidget(self.switchCombo, 0, Qt.AlignCenter) groupBox = QGroupBox(self) groupBoxLayout = QVBoxLayout(groupBox) groupBoxLayout.setSpacing(0) self.pageArea = QStackedWidget(groupBox) groupBoxLayout.addWidget(self.pageArea) layout.addWidget(groupBox, 1) self.switchCombo.currentIndexChanged.connect(self.pageArea.setCurrentIndex) def setTabPosition(self, tabPos): pass def addTab(self, w, tabText): self.pageArea.addWidget(w) self.switchCombo.addItem(tabText) def insertTab(self, pos, w, tabText): self.pageArea.insertWidget(pos, w) self.switchCombo.insertItem(pos, tabText) def removeTab(self, index=-1): if index < 0: index = self.currentIndex() w = self.pageArea.widget(index) self.pageArea.removeWidget(w) self.switchCombo.removeItem(index) def updateTab(self, w, tabText, index=-1): if index < 0: index = self.switchCombo.currentIndex() self.removeTab(index) self.insertTab(index, w, tabText) self.setCurrentIndex(index) def setCurrentIndex(self, index): self.switchCombo.setCurrentIndex(index) def widget(self, index): return self.pageArea.widget(index) def currentIndex(self): return self.switchCombo.currentIndex() def count(self): return self.switchCombo.count()
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 Container(QSplitter): def __init__(self, orientation=Qt.Vertical): super(Container, self).__init__(orientation) self.__last_open_folder = None self.__filename = "" self.__created = False self.__modified = False vbox = QVBoxLayout(self) vbox.setContentsMargins(0, 0, 0, 0) # Stacked self.stacked = QStackedWidget() vbox.addWidget(self.stacked) # Table self.table_widget = table_widget.TableWidget() Pireal.load_service("container", self) def create_data_base(self, filename=''): """ This function opens or creates a database :param filename: Database filename """ if self.__created: QMessageBox.critical(self, self.tr("Error"), self.tr("Solo puede tener una base de datos " "abierta a la vez.")) return if not filename: db_name, ok = QInputDialog.getText(self, self.tr("Nueva DB"), self.tr("Nombre:")) if not ok: return else: # From file try: db_name, data = file_manager.open_database(filename) except Exception as reason: QMessageBox.critical(self, self.tr("Error!"), reason.__str__()) return self.table_widget.add_data_base(data) # Remove Start Page widget if isinstance(self.stacked.widget(0), start_page.StartPage): self.stacked.removeWidget(self.stacked.widget(0)) self.stacked.addWidget(self.table_widget) # Title pireal = Pireal.get_service("pireal") pireal.change_title(db_name) # Enable QAction's pireal.enable_disable_db_actions() self.__created = True def create_new_relation(self): dialog = new_relation_dialog.NewRelationDialog(self) dialog.show() def remove_relation(self): lateral = Pireal.get_service("lateral") rname = lateral.get_relation_name() if not rname: QMessageBox.critical(self, self.tr("Error"), self.tr("No se ha seleccionado ninguna " "relación.")) return r = QMessageBox.question(self, self.tr("Confirmación"), self.tr("Seguro que quieres eliminar la " "relación <b>{}</b>").format(rname), QMessageBox.Yes | QMessageBox.No) if r == QMessageBox.No: return index = lateral.current_index() # Remove table self.table_widget.remove_table(index) # Remove item from list widget lateral.remove_item(index) def new_query(self, filename=''): query_widget = Pireal.get_service("query_widget") self.addWidget(query_widget) if not query_widget.isVisible(): query_widget.show() pireal = Pireal.get_service("pireal") pireal.enable_disable_query_actions() query_widget.new_query(filename) self.connect(query_widget, SIGNAL("currentEditorSaved(QPlainTextEdit)"), self.save_query) @property def modified(self): return self.__modified def show_start_page(self): sp = start_page.StartPage() self.stacked.addWidget(sp) def close_db(self): """ Close data base """ widget = self.stacked.currentWidget() if isinstance(widget, table_widget.TableWidget): # Clear list of relations lateral = Pireal.get_service("lateral") lateral.clear_items() lateral.hide() # Close table widget self.stacked.removeWidget(widget) # Add start page self.show_start_page() self.__created = False def save_query(self, weditor=None): if weditor is None: query_widget = Pireal.get_service("query_widget") # Editor instance weditor = query_widget.get_active_editor() if weditor.rfile.is_new: return self.save_query_as(weditor) content = weditor.toPlainText() weditor.rfile.write(content) weditor.document().setModified(False) self.emit(SIGNAL("currentFileSaved(QString)"), self.tr("Archivo guardado: {}").format(weditor.filename)) def open_file(self): if self.__last_open_folder is None: directory = os.path.expanduser("~") else: directory = self.__last_open_folder filename = QFileDialog.getOpenFileName(self, self.tr("Abrir Archivo"), directory, settings.DBFILE, QFileDialog.DontUseNativeDialog) if not filename: return # Save folder self.__last_open_folder = file_manager.get_path(filename) ext = file_manager.get_extension(filename) if ext == '.pqf': # Query file self.new_query(filename) elif ext == '.rdb': self.load_rdb_database(file_manager.read_rdb_file(filename)) else: self.create_data_base(filename) def load_rdb_database(self, content): csv_content = "" for line in content.splitlines(): if line.startswith('@'): csv_content += '@' portion = line.split('(') name = portion[0][1:] csv_content += name + ':' for i in portion[1].split(','): if not i.startswith(' '): field = i.split('/')[0].strip() csv_content += field + ',' else: if not line: continue csv_content += line csv_content += '\n' self.table_widget.add_table_from_rdb_content(csv_content) def save_query_as(self, editor=None): if editor is None: query_widget = Pireal.get_service("query_widget") editor = query_widget.get_active_editor() directory = os.path.expanduser("~") filename = QFileDialog.getSaveFileName(self, self.tr("Guardar Archivo"), directory) if not filename: return content = editor.toPlainText() editor.rfile.write(content, filename) editor.document().setModified(False) def load_relation(self, filenames=[]): """ Load relation from file """ if not filenames: native_dialog = QFileDialog.DontUseNativeDialog if self.__last_open_folder is None: directory = os.path.expanduser("~") else: directory = self.__last_open_folder ffilter = settings.RFILES.split(';;')[-1] filenames = QFileDialog.getOpenFileNames(self, self.tr("Abrir Archivo"), directory, ffilter, native_dialog) if not filenames: return # Save folder self.__last_open_folder = file_manager.get_path(filenames[0]) self.__modified = True # Load tables self.table_widget.load_relation(filenames) def execute_queries(self): query_widget = Pireal.get_service("query_widget") query_widget.execute_queries() def undo_action(self): query_widget = Pireal.get_service("query_widget") query_widget.undo() def redo_action(self): query_widget = Pireal.get_service("query_widget") query_widget.redo() def cut_action(self): query_widget = Pireal.get_service("query_widget") query_widget.cut() def copy_action(self): query_widget = Pireal.get_service("query_widget") query_widget.copy() def paste_action(self): query_widget = Pireal.get_service("query_widget") query_widget.paste() def check_opened_query_files(self): query_widget = Pireal.get_service("query_widget") return query_widget.opened_files()
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 Container(QSplitter): def __init__(self, orientation=Qt.Vertical): super(Container, self).__init__(orientation) self._data_bases = [] self.__filename = "" vbox = QVBoxLayout(self) vbox.setContentsMargins(0, 0, 0, 0) self.__created = False # Stacked self.stacked = QStackedWidget() vbox.addWidget(self.stacked) # Table self.table_widget = table_widget.TableWidget() Pireal.load_service("container", self) def create_data_base(self, filename=''): """ This function opens or creates a database :param filename: Database filename """ if self.__created: QMessageBox.critical(self, self.tr("Error"), self.tr("Solo puede tener una base de datos " "abierta a la vez.")) return from_file = False if not filename: db_name, ok = QInputDialog.getText(self, self.tr("Nueva DB"), self.tr("Nombre:")) if not ok: return else: from_file = True # Read database from file try: data = file_manager.open_database(filename) except Exception as reason: QMessageBox.critical(self, self.tr("Error!"), reason.__str__()) return # This is intended to give support multiple database for name, files in data.items(): db_name = name # Remove the start page self.stacked.removeWidget(self.stacked.widget(0)) self.stacked.addWidget(self.table_widget) pireal = Pireal.get_service("pireal") # Title pireal.change_title(db_name) if from_file: self.load_relation(files) # Enable QAction's pireal.enable_disable_db_actions() self.__created = True def create_new_relation(self): dialog = new_relation_dialog.NewRelationDialog(self) dialog.show() def remove_relation(self): lateral = Pireal.get_service("lateral") lateral.remove_table() def new_query(self, filename=''): query_widget = Pireal.get_service("query_widget") self.addWidget(query_widget) if not query_widget.isVisible(): query_widget.show() pireal = Pireal.get_service("pireal") pireal.enable_disable_query_actions() query_widget.new_query(filename) self.connect(query_widget, SIGNAL("currentEditorSaved(QPlainTextEdit)"), self.save_query) def show_start_page(self): sp = start_page.StartPage() self.stacked.addWidget(sp) def close_db(self): widget = self.stacked.currentWidget() if isinstance(widget, table_widget.TableWidget): self.close() def save_query(self, weditor=None): if weditor is None: query_widget = Pireal.get_service("query_widget") # Editor instance weditor = query_widget.get_active_editor() if weditor.rfile.is_new: return self.save_query_as(weditor) content = weditor.toPlainText() weditor.rfile.write(content) weditor.document().setModified(False) self.emit(SIGNAL("currentFileSaved(QString)"), self.tr("Archivo guardado: {}").format(weditor.filename)) def open_file(self): directory = os.path.expanduser("~") filename = QFileDialog.getOpenFileName(self, self.tr("Abrir Archivo"), directory, settings.DBFILE, QFileDialog.DontUseNativeDialog) if not filename: return ext = file_manager.get_extension(filename) if ext == '.pqf': # Query file self.new_query(filename) else: self.create_data_base(filename) def save_query_as(self, editor=None): if editor is None: query_widget = Pireal.get_service("query_widget") editor = query_widget.get_active_editor() directory = os.path.expanduser("~") filename = QFileDialog.getSaveFileName(self, self.tr("Guardar Archivo"), directory) if not filename: return content = editor.toPlainText() editor.rfile.write(content, filename) editor.document().setModified(False) def load_relation(self, filenames=[]): """ Load relation from file """ import csv from PyQt4.QtGui import QTableWidgetItem, QTableWidget from src.core import relation if not filenames: native_dialog = QFileDialog.DontUseNativeDialog directory = os.path.expanduser("~") ffilter = settings.RFILES.split(';;')[-1] filenames = QFileDialog.getOpenFileNames(self, self.tr("Abrir Archivo"), directory, ffilter, native_dialog) if not filenames: return lateral = Pireal.get_service("lateral") for filename in filenames: rel = relation.Relation(filename) relation_name = os.path.splitext(os.path.basename(filename))[0] self.table_widget.relations[relation_name] = rel table = QTableWidget() with open(filename, newline='') as f: table.setRowCount(0) table.setColumnCount(0) csv_reader = csv.reader(f) for row_data in csv_reader: row = table.rowCount() table.setColumnCount(len(row_data)) for column, data in enumerate(row_data): item = QTableWidgetItem() item.setText(data) if row == 0: table.setHorizontalHeaderItem(column, item) else: table.setItem(row - 1, column, item) table.insertRow(row) table.removeRow(table.rowCount() - 1) self.table_widget.stacked.addWidget(table) #FIXME: names names = [os.path.splitext(os.path.basename(i))[0] for i in filenames] lateral.add_item_list(names) lateral.show() def execute_queries(self): query_widget = Pireal.get_service("query_widget") query_widget.execute_queries() def undo_action(self): query_widget = Pireal.get_service("query_widget") query_widget.undo() def redo_action(self): query_widget = Pireal.get_service("query_widget") query_widget.redo() def cut_action(self): query_widget = Pireal.get_service("query_widget") query_widget.cut() def copy_action(self): query_widget = Pireal.get_service("query_widget") query_widget.copy() def paste_action(self): query_widget = Pireal.get_service("query_widget") query_widget.paste() def check_opened_query_files(self): query_widget = Pireal.get_service("query_widget") return query_widget.opened_files()
class EditorContainer(QWidget): # Señales closedFile = pyqtSignal(int) savedFile = pyqtSignal('QString') cursorPosition = pyqtSignal(int, int, int) updateSymbols = pyqtSignal('PyQt_PyObject') fileChanged = pyqtSignal('QString') openedFile = pyqtSignal('QString') def __init__(self, edis=None): QWidget.__init__(self, edis) self.setAcceptDrops(True) self.box = QVBoxLayout(self) self.box.setContentsMargins(0, 0, 0, 0) self.box.setSpacing(0) # Stacked self.stack = QStackedWidget() self.box.addWidget(self.stack) # Replace widget #FIXME: mover esto self._replace_widget = replace_widget.ReplaceWidget() self._replace_widget.hide() self.box.addWidget(self._replace_widget) # Editor widget self.editor_widget = editor_widget.EditorWidget() # Conexiones self.connect(self.editor_widget, SIGNAL("saveCurrentFile()"), self.save_file) self.connect(self.editor_widget, SIGNAL("fileClosed(int)"), self._file_closed) self.connect(self.editor_widget, SIGNAL("recentFile(QStringList)"), self.update_recents_files) self.connect(self.editor_widget, SIGNAL("allFilesClosed()"), self.add_start_page) self.connect(self.editor_widget, SIGNAL("currentWidgetChanged(int)"), self.change_widget) Edis.load_component("principal", self) def update_recents_files(self, recents_files): """ Actualiza el submenú de archivos recientes """ menu = Edis.get_component("menu_recent_file") self.connect(menu, SIGNAL("triggered(QAction*)"), self._open_recent_file) menu.clear() for _file in recents_files: menu.addAction(_file) def _open_recent_file(self, accion): """ Abre el archivo desde el menú """ self.open_file(accion.text()) def get_recents_files(self): """ Devuelve una lista con los archivos recientes en el menú """ menu = Edis.get_component('menu_recent_file') actions = menu.actions() recents_files = [] for filename in actions: recents_files.append(filename.text()) return recents_files def _file_closed(self, index): self.closedFile.emit(index) def _file_modified(self, value): self.editor_widget.editor_modified(value) def _file_saved(self, filename): self.editor_widget.editor_modified(False) self.emit(SIGNAL("updateSymbols(QString)"), filename) def change_widget(self, index): weditor = self.get_active_editor() self.editor_widget.combo.combo_file.setCurrentIndex(index) if weditor is not None and not weditor.obj_file.is_new: self.emit(SIGNAL("updateSymbols(QString)"), weditor.filename) self.emit(SIGNAL("fileChanged(QString)"), weditor.filename) weditor.setFocus() def create_editor(self, obj_file=None, filename=""): if obj_file is None: obj_file = object_file.EdisFile(filename) self.stack.addWidget(self.editor_widget) # Quito la página de inicio, si está _start_page = self.stack.widget(0) if isinstance(_start_page, start_page.StartPage): self.remove_widget(_start_page) # Detengo el tiimer _start_page.timer.stop() weditor = editor.Editor(obj_file) self.editor_widget.add_widget(weditor) self.editor_widget.add_item_combo(obj_file.filename) lateral = Edis.get_component("tab_container") if not lateral.isVisible(): lateral.show() # Conexiones self.connect(obj_file, SIGNAL("fileChanged(PyQt_PyObject)"), self._file_changed) self.connect(weditor, SIGNAL("cursorPositionChanged(int, int)"), self.update_cursor) self.connect(weditor, SIGNAL("modificationChanged(bool)"), self._file_modified) self.connect(weditor, SIGNAL("fileSaved(QString)"), self._file_saved) self.connect(weditor, SIGNAL("linesChanged(int)"), self.editor_widget.combo.move_to_symbol) self.connect(weditor, SIGNAL("dropEvent(PyQt_PyObject)"), self._drop_editor) self.emit(SIGNAL("fileChanged(QString)"), obj_file.filename) weditor.setFocus() return weditor def _file_changed(self, obj_file): filename = obj_file.filename flags = QMessageBox.Yes flags |= QMessageBox.No result = QMessageBox.information(self, self.tr("Monitoreo de Archivo"), self.tr("El archivo <b>{0}</b> fué " "modificado fuera de Edis. " "<br><br> Quieres recar" "garlo?".format(filename)), flags) if result == QMessageBox.No: return self.reload_file(obj_file) def reload_file(self, obj_file=None): weditor = self.get_active_editor() if weditor is None: return if obj_file is None: obj_file = weditor.obj_file content = obj_file.read() if weditor.is_modified: result = QMessageBox.information(self, self.tr( "Archivo no guardado!"), self.tr("Seguro que quieres recargar el archivo <b>{0}</b>?" "<br><br>" "Se perderán los cambios no guardados.").format( obj_file.filename), QMessageBox.Cancel | QMessageBox.Yes) if result == QMessageBox.Cancel: return weditor.setText(content) weditor.markerDeleteAll() weditor.setModified(False) def open_file(self, filename="", cursor_position=None): filter_files = "Archivos C(*.cpp *.c);;ASM(*.s);;HEADERS(*.h);;(*.*)" if not filename: working_directory = os.path.expanduser("~") weditor = self.get_active_editor() if weditor and weditor.filename: working_directory = self._last_folder(weditor.filename) filenames = QFileDialog.getOpenFileNames(self, self.tr("Abrir Archivo"), working_directory, filter_files) else: filenames = [filename] try: for _file in filenames: if not self._is_open(_file): #self.editor_widget.not_open = False # Creo el objeto Edis File obj_file = object_file.EdisFile(_file) content = obj_file.read() weditor = self.create_editor(obj_file, _file) weditor.setText(content) # Cuando se setea el contenido en el editor # se emite la señal textChanged() por lo tanto se agrega # el marker, entonces se procede a borrarlo weditor.markerDelete(0, 3) # FIXME: Cursor position not found #if cursor_position is not None: #line, row = cursor_position #weditor.setCursorPosition(line, row) weditor.setModified(False) obj_file.run_system_watcher() else: # Se cambia el índice del stacked # para mostrar el archivo que ya fué abierto for index in range(self.editor_widget.count()): editor = self.editor_widget.widget(index) if editor.filename == _file: self.change_widget(index) self.emit(SIGNAL("fileChanged(QString)"), _file) self.emit(SIGNAL("openedFile(QString)"), _file) self.emit(SIGNAL("updateSymbols(QString)"), _file) except EdisIOError as error: ERROR('Error al intentar abrir archivo: %s', error) QMessageBox.critical(self, self.tr('No se pudo abrir el archivo'), str(error)) #self.editor_widget.not_open = True def _last_folder(self, path): """ Devuelve la última carpeta a la que se accedió """ return QFileInfo(path).absolutePath() def _is_open(self, archivo): """ Retorna True si un archivo ya esta abierto, False en caso contrario """ for index in range(self.editor_widget.count()): widget = self.editor_widget.widget(index) if widget.filename == archivo: return True return False def add_widget(self, widget): """ Agrega @widget al stacked """ self.editor_widget.add_widget(widget) def add_start_page(self): """ Agrega la página de inicio al stack """ if settings.get_setting('general/show-start-page'): _start_page = start_page.StartPage() self.stack.insertWidget(0, _start_page) self.stack.setCurrentIndex(0) else: self.editor_widget.combo.setVisible(False) def remove_widget(self, widget): """ Elimina el @widget del stacked """ self.stack.removeWidget(widget) def current_widget(self): """ Widget actual """ return self.editor_widget.current_widget() def current_index(self): return self.editor_widget.current_index() def get_active_editor(self): """ Devuelve el Editor si el widget actual es una instancia de él, de lo contrario devuelve None. """ widget = self.current_widget() if isinstance(widget, editor.Editor): return widget return None def close_file(self): self.editor_widget.close_file() def close_file_from_project(self, filename): #FIXME: revisar for index in range(self.editor_widget.count()): widget = self.editor_widget.widget(index) if widget.filename == filename: editor, i = widget, index self.editor_widget.close_file_project(editor, i) def close_all(self): self.editor_widget.close_all() def show_selector(self): if self.get_active_editor() is not None: selector = file_selector.FileSelector(self) selector.show() def save_file(self, weditor=None): if weditor is None: weditor = self.get_active_editor() if weditor is None: return if weditor.obj_file.is_new: return self.save_file_as(weditor) source_code = weditor.text() weditor.obj_file.write(source_code) weditor.saved() # System watcher weditor.obj_file.run_system_watcher() self.savedFile.emit(weditor.filename) return weditor.filename def save_file_as(self, weditor=None): if weditor is None: weditor = self.get_active_editor() if weditor is None: return working_directory = os.path.expanduser("~") filename = QFileDialog.getSaveFileName(self, self.tr("Guardar Archivo"), working_directory) if not filename: return False content = weditor.text() weditor.obj_file.write(content, filename) weditor.saved() self.fileChanged.emit(weditor.filename) self.savedFile.emit(weditor.filename) return filename def save_selected(self, filename): for index in range(self.editor_widget.count()): if self.editor_widget.widget(index).filename == filename: self.save_file(self.editor_widget.widget(index)) def files_not_saved(self): return self.editor_widget.files_not_saved() def check_files_not_saved(self): return self.editor_widget.check_files_not_saved() def find(self): weditor = self.get_active_editor() if weditor is not None: dialog = find_popup.PopupBusqueda(self.get_active_editor()) dialog.show() def find_and_replace(self): weditor = self.get_active_editor() if weditor is not None: if self._replace_widget.isVisible(): self._replace_widget.hide() weditor.setFocus() else: self._replace_widget.show() def action_undo(self): weditor = self.get_active_editor() if weditor is not None: weditor.undo() def action_redo(self): weditor = self.get_active_editor() if weditor is not None: weditor.redo() def action_cut(self): weditor = self.get_active_editor() if weditor is not None: weditor.cut() def action_copy(self): weditor = self.get_active_editor() if weditor is not None: weditor.copy() def action_paste(self): weditor = self.get_active_editor() if weditor is not None: weditor.paste() def show_tabs_and_spaces(self): weditor = self.get_active_editor() if weditor is not None: tabs_spaces = settings.get_setting('editor/show-tabs-spaces') settings.set_setting('editor/show-tabs-spaces', not tabs_spaces) weditor.update_options() def show_indentation_guides(self): weditor = self.get_active_editor() if weditor is not None: guides = settings.get_setting('editor/show-guides') settings.set_setting('editor/show-guides', not guides) weditor.update_options() def delete_editor_markers(self): weditor = self.get_active_editor() if weditor is not None: weditor.markerDeleteAll() def action_zoom_in(self): weditor = self.get_active_editor() if weditor is not None: weditor.zoom_in() def action_zoom_out(self): weditor = self.get_active_editor() if weditor is not None: weditor.zoom_out() def action_normal_size(self): """ Carga el tamaño por default de la fuente """ weditor = self.get_active_editor() if weditor is not None: weditor.zoomTo(0) def action_select_all(self): weditor = self.get_active_editor() if weditor is not None: weditor.selectAll() def opened_files(self): return self.editor_widget.opened_files() def opened_files_for_selector(self): self.index_file_selector = 0 files = [] for index in range(self.editor_widget.count()): weditor = self.editor_widget.widget(index) path = weditor.filename if not path: path = weditor.display + ' (%s)' % self.index_file_selector self.index_file_selector += 1 files.append(path) return files def get_open_projects(self): tree_projects = Edis.get_lateral("tree_projects") return tree_projects.get_open_projects() def file_properties(self): weditor = self.get_active_editor() if weditor is not None: dialog = file_properties.FileProperty(weditor, self) dialog.show() def update_cursor(self, line, row): weditor = self.get_active_editor() lines = weditor.lines() self.editor_widget.combo.update_cursor_position( line + 1, row + 1, lines) self.cursorPosition.emit(line + 1, row + 1, lines) def build_source_code(self): output = Edis.get_component("output") project = Edis.get_lateral("tree_projects") weditor = self.get_active_editor() if weditor is not None: filename = self.save_file() if project.sources: output.build((filename, project.sources)) else: if filename: output.build((weditor.filename, [])) def run_binary(self): """ Ejecuta el programa objeto """ output = Edis.get_component("output") output.run() def build_and_run(self): output = Edis.get_component("output") weditor = self.get_active_editor() if weditor is not None: self.save_file() output.build_and_run(weditor.filename) def clean_construction(self): output = Edis.get_component("output") output.clean() def stop_program(self): output = Edis.get_component("output") output.stop() def action_comment(self): weditor = self.get_active_editor() if weditor is not None: weditor.comment() def action_uncomment(self): weditor = self.get_active_editor() if weditor is not None: weditor.uncomment() def action_indent(self): weditor = self.get_active_editor() if weditor is not None: weditor.indent_more() def action_unindent(self): weditor = self.get_active_editor() if weditor is not None: weditor.indent_less() def action_to_lowercase(self): weditor = self.get_active_editor() if weditor is not None: weditor.to_lowercase() def action_to_uppercase(self): weditor = self.get_active_editor() if weditor is not None: weditor.to_uppercase() def action_to_title(self): weditor = self.get_active_editor() if weditor is not None: weditor.to_title() def action_duplicate_line(self): weditor = self.get_active_editor() if weditor is not None: weditor.duplicate_line() def action_delete_line(self): weditor = self.get_active_editor() if weditor is not None: weditor.delete_line() def action_move_down(self): weditor = self.get_active_editor() if weditor is not None: weditor.move_down() def action_move_up(self): weditor = self.get_active_editor() if weditor is not None: weditor.move_up() def reemplazar_tabs_por_espacios(self): weditor = self.get_active_editor() if weditor is not None: weditor.reemplazar_tabs_por_espacios() def go_to_line(self, line): weditor = self.get_active_editor() if weditor is not None: weditor.setCursorPosition(line, 0) def show_go_to_line(self): weditor = self.get_active_editor() if weditor is not None: dialog = goto_line_widget.GoToLineDialog(weditor) dialog.show() def add_symbols_combo(self, symbols): self.editor_widget.add_symbols(symbols) def dragEnterEvent(self, event): data = event.mimeData() if data.hasText(): # Se acepta el evento de arrastrado event.accept() def dropEvent(self, event): self._drop_event(event) def _drop_editor(self, event): self._drop_event(event) def _drop_event(self, event): data = event.mimeData() filename = data.urls()[0].toLocalFile() self.open_file(filename) def show_settings(self): preferences_widget = Edis.get_component("preferences") current_widget = self.stack.currentWidget() if isinstance(current_widget, preferences_widget.__class__): return self.connect(preferences_widget, SIGNAL("configurationsClose(PyQt_PyObject)"), lambda widget: self.remove_widget(widget)) index = self.stack.addWidget(preferences_widget) self.stack.setCurrentIndex(index) def open_project(self, filename='', edis_project=True): if edis_project: if not filename: filename = QFileDialog.getOpenFileName(self, self.tr("Cargar " "Proyecto"), paths.PROJECT_DIR, "Archivo Edis(*.epf)") if not filename: return project_file = json.load(open(filename)) project_path = project_file.get('path', '') else: project_path = os.path.dirname(filename) else: result = QFileDialog.getExistingDirectory(self, self.tr("Selecciona " "la carpeta")) if not result: return project_path = result project_structure = {} filter_files = ['.c', '.h'] for parent, dirs, files in os.walk(project_path): files = [fi for fi in files if os.path.splitext(fi)[-1] in filter_files] project_structure[parent] = (files, dirs) self.emit(SIGNAL("projectOpened(PyQt_PyObject)"), (project_structure, project_path, edis_project, filename)) def open_directory(self): self.open_project(edis_project=False) def create_new_project(self): project_creator = new_project.NewProjectDialog(self) self.connect(project_creator, SIGNAL("projectReady(PyQt_PyObject)"), self._update_data) project_creator.show() def _update_data(self, data): self.emit(SIGNAL("projectReady(PyQt_PyObject)"), data) def opened_projects(self): pass def code_pasting(self): if self.get_active_editor() is not None: code = self.get_active_editor().text() code_pasting = code_pasting_dialog.CodePastingDialog(self, code) code_pasting.exec_() def show_snake(self): from src.ui.widgets.pyborita import pyborita_widget w = pyborita_widget.PyboritaWidget(self) toolbar = Edis.get_component("toolbar") lateral = Edis.get_component("tab_container") status = Edis.get_component("status_bar") output = Edis.get_component("output") widgets = [toolbar, status, lateral, output] for widget in widgets: widget.hide() self.stack.insertWidget(0, w) self.stack.setCurrentIndex(0)
class DataSelectionGui(QWidget): """ Manages all GUI elements in the data selection applet. This class itself is the central widget and also owns/manages the applet drawer widgets. """ ########################################### ### AppletGuiInterface Concrete Methods ### ########################################### def centralWidget(self): return self def appletDrawer(self): return self._drawer def menus(self): return [] def viewerControlWidget(self): return self._viewerControlWidgetStack def setImageIndex(self, imageIndex): if imageIndex is not None: self.laneSummaryTableView.selectRow(imageIndex) for detailWidget in self._detailViewerWidgets: detailWidget.selectRow(imageIndex) def stopAndCleanUp(self): self._cleaning_up = True for editor in self.volumeEditors.values(): self.viewerStack.removeWidget(editor) self._viewerControlWidgetStack.removeWidget( editor.viewerControlWidget()) editor.stopAndCleanUp() self.volumeEditors.clear() def imageLaneAdded(self, laneIndex): if len(self.laneSummaryTableView.selectedIndexes()) == 0: self.laneSummaryTableView.selectRow(laneIndex) # We don't have any real work to do because this gui initiated the lane addition in the first place if self.guiMode != GuiMode.Batch: if (len(self.topLevelOperator.DatasetGroup) != laneIndex + 1): import warnings warnings.warn( "DataSelectionGui.imageLaneAdded(): length of dataset multislot out of sync with laneindex [%s != %s + 1]" % (len(self.topLevelOperator.DatasetGroup), laneIndex)) def imageLaneRemoved(self, laneIndex, finalLength): # There's nothing to do here because the GUI already # handles operator resizes via slot callbacks. pass def allowLaneSelectionChange(self): return False ########################################### ########################################### class UserCancelledError(Exception): # This exception type is raised when the user cancels the # addition of dataset files in the middle of the process somewhere. # It isn't an error -- it's used for control flow. pass def __init__(self, parentApplet, dataSelectionOperator, serializer, instructionText, guiMode=GuiMode.Normal, max_lanes=None, show_axis_details=False): """ Constructor. :param dataSelectionOperator: The top-level operator. Must be of type :py:class:`OpMultiLaneDataSelectionGroup`. :param serializer: The applet's serializer. Must be of type :py:class:`DataSelectionSerializer` :param instructionText: A string to display in the applet drawer. :param guiMode: Either ``GuiMode.Normal`` or ``GuiMode.Batch``. Currently, there is no difference between normal and batch mode. :param max_lanes: The maximum number of lanes that the user is permitted to add to this workflow. If ``None``, there is no maximum. """ super(DataSelectionGui, self).__init__() self._cleaning_up = False self.parentApplet = parentApplet self._max_lanes = max_lanes self._default_h5_volumes = {} self.show_axis_details = show_axis_details self._viewerControls = QWidget() self.topLevelOperator = dataSelectionOperator self.guiMode = guiMode self.serializer = serializer self.threadRouter = ThreadRouter(self) self._initCentralUic() self._initAppletDrawerUic(instructionText) self._viewerControlWidgetStack = QStackedWidget(self) def handleImageRemove(multislot, index, finalLength): # Remove the viewer for this dataset datasetSlot = self.topLevelOperator.DatasetGroup[index] if datasetSlot in self.volumeEditors.keys(): editor = self.volumeEditors[datasetSlot] self.viewerStack.removeWidget(editor) self._viewerControlWidgetStack.removeWidget( editor.viewerControlWidget()) editor.stopAndCleanUp() self.topLevelOperator.DatasetGroup.notifyRemove( bind(handleImageRemove)) opWorkflow = self.topLevelOperator.parent assert hasattr(opWorkflow.shell, 'onSaveProjectActionTriggered'), \ "This class uses the IlastikShell.onSaveProjectActionTriggered function. Did you rename it?" def _initCentralUic(self): """ Load the GUI from the ui file into this class and connect it with event handlers. """ # Load the ui file into this class (find it in our own directory) localDir = os.path.split(__file__)[0] + '/' uic.loadUi(localDir + "/dataSelection.ui", self) self._initTableViews() self._initViewerStack() self.splitter.setSizes([150, 850]) def _initAppletDrawerUic(self, instructionText): """ Load the ui file for the applet drawer, which we own. """ localDir = os.path.split(__file__)[0] + '/' self._drawer = uic.loadUi(localDir + "/dataSelectionDrawer.ui") self._drawer.instructionLabel.setText(instructionText) def _initTableViews(self): self.fileInfoTabWidget.setTabText(0, "Summary") self.laneSummaryTableView.setModel( DataLaneSummaryTableModel(self, self.topLevelOperator)) self.laneSummaryTableView.dataLaneSelected.connect(self.showDataset) self.laneSummaryTableView.addFilesRequested.connect(self.addFiles) self.laneSummaryTableView.addStackRequested.connect(self.addStack) self.laneSummaryTableView.removeLanesRequested.connect( self.handleRemoveLaneButtonClicked) # These two helper functions enable/disable an 'add files' button for a given role # based on the the max lane index for that role and the overall permitted max_lanes def _update_button_status(viewer, role_index): if self._max_lanes: viewer.setEnabled( self._findFirstEmptyLane(role_index) < self._max_lanes) def _handle_lane_added(button, role_index, lane_slot, lane_index): def _handle_role_slot_added(role_slot, added_slot_index, *args): if added_slot_index == role_index: role_slot.notifyReady( bind(_update_button_status, button, role_index)) role_slot.notifyUnready( bind(_update_button_status, button, role_index)) lane_slot[lane_index].notifyInserted(_handle_role_slot_added) self._retained = [] # Retain menus so they don't get deleted self._detailViewerWidgets = [] for roleIndex, role in enumerate( self.topLevelOperator.DatasetRoles.value): detailViewer = DatasetDetailedInfoTableView(self) detailViewer.setModel( DatasetDetailedInfoTableModel(self, self.topLevelOperator, roleIndex)) self._detailViewerWidgets.append(detailViewer) # Button detailViewer.addFilesRequested.connect( partial(self.addFiles, roleIndex)) detailViewer.addStackRequested.connect( partial(self.addStack, roleIndex)) detailViewer.addRemoteVolumeRequested.connect( partial(self.addDvidVolume, roleIndex)) # Monitor changes to each lane so we can enable/disable the 'add lanes' button for each tab self.topLevelOperator.DatasetGroup.notifyInserted( bind(_handle_lane_added, detailViewer, roleIndex)) self.topLevelOperator.DatasetGroup.notifyRemoved( bind(_update_button_status, detailViewer, roleIndex)) # While we're at it, do the same for the buttons in the summary table, too self.topLevelOperator.DatasetGroup.notifyInserted( bind(_handle_lane_added, self.laneSummaryTableView.addFilesButtons[roleIndex], roleIndex)) self.topLevelOperator.DatasetGroup.notifyRemoved( bind(_update_button_status, self.laneSummaryTableView.addFilesButtons[roleIndex], roleIndex)) # Context menu detailViewer.replaceWithFileRequested.connect( partial(self.handleReplaceFile, roleIndex)) detailViewer.replaceWithStackRequested.connect( partial(self.addStack, roleIndex)) detailViewer.editRequested.connect( partial(self.editDatasetInfo, roleIndex)) detailViewer.resetRequested.connect( partial(self.handleClearDatasets, roleIndex)) # Drag-and-drop detailViewer.addFilesRequestedDrop.connect( partial(self.addFileNames, roleIndex=roleIndex)) # Selection handling def showFirstSelectedDataset(_roleIndex, lanes): if lanes: self.showDataset(lanes[0], _roleIndex) detailViewer.dataLaneSelected.connect( partial(showFirstSelectedDataset, roleIndex)) self.fileInfoTabWidget.insertTab(roleIndex, detailViewer, role) self.fileInfoTabWidget.currentChanged.connect(self.handleSwitchTabs) self.fileInfoTabWidget.setCurrentIndex(0) def handleSwitchTabs(self, tabIndex): if tabIndex < len(self._detailViewerWidgets): roleIndex = tabIndex # If summary tab is moved to the front, change this line. detailViewer = self._detailViewerWidgets[roleIndex] selectedLanes = detailViewer.selectedLanes if selectedLanes: self.showDataset(selectedLanes[0], roleIndex) def _initViewerStack(self): self.volumeEditors = {} self.viewerStack.addWidget(QWidget()) def handleRemoveLaneButtonClicked(self): """ The user clicked the "Remove" button. Remove the currently selected row(s) from both the GUI and the top-level operator. """ # Figure out which lanes to remove selectedIndexes = self.laneSummaryTableView.selectedIndexes() rows = set() for modelIndex in selectedIndexes: rows.add(modelIndex.row()) # Don't remove the last row, which is just buttons. rows.discard(self.laneSummaryTableView.model().rowCount() - 1) # Remove in reverse order so row numbers remain consistent for row in reversed(sorted(rows)): # Remove lanes from the operator. # The table model will notice the changes and update the rows accordingly. finalSize = len(self.topLevelOperator.DatasetGroup) - 1 self.topLevelOperator.DatasetGroup.removeSlot(row, finalSize) @threadRouted def showDataset(self, laneIndex, roleIndex=None): if self._cleaning_up: return if laneIndex == -1: self.viewerStack.setCurrentIndex(0) return assert threading.current_thread().name == "MainThread" if laneIndex >= len(self.topLevelOperator.DatasetGroup): return datasetSlot = self.topLevelOperator.DatasetGroup[laneIndex] # Create if necessary if datasetSlot not in self.volumeEditors.keys(): class DatasetViewer(LayerViewerGui): def moveToTop(self, roleIndex): opLaneView = self.topLevelOperatorView if not opLaneView.DatasetRoles.ready(): return datasetRoles = opLaneView.DatasetRoles.value if roleIndex >= len(datasetRoles): return roleName = datasetRoles[roleIndex] try: layerIndex = [l.name for l in self.layerstack].index(roleName) except ValueError: return else: self.layerstack.selectRow(layerIndex) self.layerstack.moveSelectedToTop() def setupLayers(self): opLaneView = self.topLevelOperatorView if not opLaneView.DatasetRoles.ready(): return [] layers = [] datasetRoles = opLaneView.DatasetRoles.value for roleIndex, slot in enumerate(opLaneView.ImageGroup): if slot.ready(): roleName = datasetRoles[roleIndex] layer = self.createStandardLayerFromSlot(slot) layer.name = roleName layers.append(layer) return layers opLaneView = self.topLevelOperator.getLane(laneIndex) layerViewer = DatasetViewer(self.parentApplet, opLaneView, crosshair=False) # Maximize the x-y view by default. layerViewer.volumeEditorWidget.quadview.ensureMaximized(2) self.volumeEditors[datasetSlot] = layerViewer self.viewerStack.addWidget(layerViewer) self._viewerControlWidgetStack.addWidget( layerViewer.viewerControlWidget()) # Show the right one viewer = self.volumeEditors[datasetSlot] displayedRole = self.fileInfoTabWidget.currentIndex() viewer.moveToTop(displayedRole) self.viewerStack.setCurrentWidget(viewer) self._viewerControlWidgetStack.setCurrentWidget( viewer.viewerControlWidget()) def handleReplaceFile(self, roleIndex, startingLane): self.addFiles(roleIndex, startingLane) def addFiles(self, roleIndex, startingLane=None): """ The user clicked the "Add File" button. Ask him to choose a file (or several) and add them to both the GUI table and the top-level operator inputs. """ # Find the directory of the most recently opened image file mostRecentImageFile = PreferencesManager().get('DataSelection', 'recent image') if mostRecentImageFile is not None: defaultDirectory = os.path.split(mostRecentImageFile)[0] else: defaultDirectory = os.path.expanduser('~') # Launch the "Open File" dialog fileNames = self.getImageFileNamesToOpen(self, defaultDirectory) # If the user didn't cancel if len(fileNames) > 0: PreferencesManager().set('DataSelection', 'recent image', fileNames[0]) try: self.addFileNames(fileNames, roleIndex, startingLane) except Exception as ex: log_exception(logger) QMessageBox.critical(self, "Error loading file", str(ex)) @classmethod def getImageFileNamesToOpen(cls, parent_window, defaultDirectory): """ Launch an "Open File" dialog to ask the user for one or more image files. """ extensions = OpDataSelection.SupportedExtensions filter_strs = ["*." + x for x in extensions] filters = ["{filt} ({filt})".format(filt=x) for x in filter_strs] filt_all_str = "Image files (" + ' '.join(filter_strs) + ')' fileNames = [] if ilastik_config.getboolean("ilastik", "debug"): # use Qt dialog in debug mode (more portable?) file_dialog = QFileDialog(parent_window, "Select Images") file_dialog.setOption(QFileDialog.DontUseNativeDialog, True) # do not display file types associated with a filter # the line for "Image files" is too long otherwise file_dialog.setFilters([filt_all_str] + filters) file_dialog.setNameFilterDetailsVisible(False) # select multiple files file_dialog.setFileMode(QFileDialog.ExistingFiles) file_dialog.setDirectory(defaultDirectory) if file_dialog.exec_(): fileNames = file_dialog.selectedFiles() else: # otherwise, use native dialog of the present platform fileNames = QFileDialog.getOpenFileNames(parent_window, "Select Images", defaultDirectory, filt_all_str) # Convert from QtString to python str fileNames = map(encode_from_qstring, fileNames) return fileNames def _findFirstEmptyLane(self, roleIndex): opTop = self.topLevelOperator # Determine the number of files this role already has # Search for the last valid value. firstNewLane = 0 for laneIndex, slot in reversed( zip(range(len(opTop.DatasetGroup)), opTop.DatasetGroup)): if slot[roleIndex].ready(): firstNewLane = laneIndex + 1 break return firstNewLane def addFileNames(self, fileNames, roleIndex, startingLane=None, rois=None): """ Add the given filenames to both the GUI table and the top-level operator inputs. If startingLane is None, the filenames will be *appended* to the role's list of files. If rois is provided, it must be a list of (start,stop) tuples (one for each fileName) """ # What lanes will we touch? startingLane, endingLane = self._determineLaneRange( fileNames, roleIndex, startingLane) if startingLane is None: # Something went wrong. return # If we're only adding new lanes, NOT modifying existing lanes... adding_only = startingLane == len(self.topLevelOperator) # Create a list of DatasetInfos try: infos = self._createDatasetInfos(roleIndex, fileNames, rois) except DataSelectionGui.UserCancelledError: return # If no exception was thrown so far, set up the operator now loaded_all = self._configureOpWithInfos(roleIndex, startingLane, endingLane, infos) if loaded_all: # Now check the resulting slots. # If they should be copied to the project file, say so. self._reconfigureDatasetLocations(roleIndex, startingLane, endingLane) self._checkDataFormatWarnings(roleIndex, startingLane, endingLane) # If we succeeded in adding all images, show the first one. self.showDataset(startingLane, roleIndex) # Notify the workflow that we just added some new lanes. if adding_only: workflow = self.parentApplet.topLevelOperator.parent workflow.handleNewLanesAdded() # Notify the workflow that something that could affect applet readyness has occurred. self.parentApplet.appletStateUpdateRequested.emit() self.updateInternalPathVisiblity() def _determineLaneRange(self, fileNames, roleIndex, startingLane=None): """ Determine which lanes should be configured if the user wants to add the given fileNames to the specified role, starting at startingLane. If startingLane is None, assume the user wants to APPEND the files to the role's slots. """ if startingLane is None or startingLane == -1: startingLane = len(self.topLevelOperator.DatasetGroup) endingLane = startingLane + len(fileNames) - 1 else: assert startingLane < len(self.topLevelOperator.DatasetGroup) max_files = len(self.topLevelOperator.DatasetGroup) - \ startingLane if len(fileNames) > max_files: msg = "You selected {num_selected} files for {num_slots} "\ "slots. To add new files use the 'Add new...' option "\ "in the context menu or the button in the last row."\ .format(num_selected=len(fileNames), num_slots=max_files) QMessageBox.critical(self, "Too many files", msg) return (None, None) endingLane = min(startingLane + len(fileNames) - 1, len(self.topLevelOperator.DatasetGroup)) if self._max_lanes and endingLane >= self._max_lanes: msg = "You may not add more than {} file(s) to this workflow. Please try again.".format( self._max_lanes) QMessageBox.critical(self, "Too many files", msg) return (None, None) return (startingLane, endingLane) def _createDatasetInfos(self, roleIndex, filePaths, rois): """ Create a list of DatasetInfos for the given filePaths and rois rois may be None, in which case it is ignored. """ if rois is None: rois = [None] * len(filePaths) assert len(rois) == len(filePaths) infos = [] for filePath, roi in zip(filePaths, rois): info = self._createDatasetInfo(roleIndex, filePath, roi) infos.append(info) return infos def _createDatasetInfo(self, roleIndex, filePath, roi): """ Create a DatasetInfo object for the given filePath and roi. roi may be None, in which case it is ignored. """ cwd = self.topLevelOperator.WorkingDirectory.value datasetInfo = DatasetInfo(filePath, cwd=cwd) datasetInfo.subvolume_roi = roi # (might be None) absPath, relPath = getPathVariants(filePath, cwd) # If the file is in a totally different tree from the cwd, # then leave the path as absolute. Otherwise, override with the relative path. if relPath is not None and len(os.path.commonprefix([cwd, absPath ])) > 1: datasetInfo.filePath = relPath h5Exts = ['.ilp', '.h5', '.hdf5'] if os.path.splitext(datasetInfo.filePath)[1] in h5Exts: datasetNames = self.getPossibleInternalPaths(absPath) if len(datasetNames) == 0: raise RuntimeError("HDF5 file %s has no image datasets" % datasetInfo.filePath) elif len(datasetNames) == 1: datasetInfo.filePath += str(datasetNames[0]) else: # If exactly one of the file's datasets matches a user's previous choice, use it. if roleIndex not in self._default_h5_volumes: self._default_h5_volumes[roleIndex] = set() previous_selections = self._default_h5_volumes[roleIndex] possible_auto_selections = previous_selections.intersection( datasetNames) if len(possible_auto_selections) == 1: datasetInfo.filePath += str( list(possible_auto_selections)[0]) else: # Ask the user which dataset to choose dlg = H5VolumeSelectionDlg(datasetNames, self) if dlg.exec_() == QDialog.Accepted: selected_index = dlg.combo.currentIndex() selected_dataset = str(datasetNames[selected_index]) datasetInfo.filePath += selected_dataset self._default_h5_volumes[roleIndex].add( selected_dataset) else: raise DataSelectionGui.UserCancelledError() # Allow labels by default if this gui isn't being used for batch data. datasetInfo.allowLabels = (self.guiMode == GuiMode.Normal) return datasetInfo def _configureOpWithInfos(self, roleIndex, startingLane, endingLane, infos): """ Attempt to configure the specified role and lanes of the top-level operator with the given DatasetInfos. Returns True if all lanes were configured successfully, or False if something went wrong. """ opTop = self.topLevelOperator originalSize = len(opTop.DatasetGroup) # Resize the slot if necessary if len(opTop.DatasetGroup) < endingLane + 1: opTop.DatasetGroup.resize(endingLane + 1) # Configure each subslot for laneIndex, info in zip(range(startingLane, endingLane + 1), infos): try: self.topLevelOperator.DatasetGroup[laneIndex][ roleIndex].setValue(info) except DatasetConstraintError as ex: return_val = [False] # Give the user a chance to fix the problem self.handleDatasetConstraintError(info, info.filePath, ex, roleIndex, laneIndex, return_val) if not return_val[0]: # Not successfully repaired. Roll back the changes and give up. opTop.DatasetGroup.resize(originalSize) return False except OpDataSelection.InvalidDimensionalityError as ex: opTop.DatasetGroup.resize(originalSize) QMessageBox.critical(self, "Dataset has different dimensionality", ex.message) return False except Exception as ex: msg = "Wasn't able to load your dataset into the workflow. See error log for details." log_exception(logger, msg) QMessageBox.critical(self, "Dataset Load Error", msg) opTop.DatasetGroup.resize(originalSize) return False return True def _reconfigureDatasetLocations(self, roleIndex, startingLane, endingLane): """ Check the metadata for the given slots. If the data is stored a format that is poorly optimized for 3D access, then configure it to be copied to the project file. Finally, save the project if we changed something. """ save_needed = False opTop = self.topLevelOperator for lane_index in range(startingLane, endingLane + 1): output_slot = opTop.ImageGroup[lane_index][roleIndex] if output_slot.meta.prefer_2d and 'z' in output_slot.meta.axistags: shape = numpy.array(output_slot.meta.shape) total_volume = numpy.prod(shape) # Only copy to the project file if the total volume is reasonably small if total_volume < 0.5e9: info_slot = opTop.DatasetGroup[lane_index][roleIndex] info = info_slot.value info.location = DatasetInfo.Location.ProjectInternal info_slot.setValue(info, check_changed=False) save_needed = True if save_needed: logger.info( "Some of your data cannot be accessed efficiently in 3D in its current format." " It will now be copied to the project file.") opWorkflow = self.topLevelOperator.parent opWorkflow.shell.onSaveProjectActionTriggered() def _checkDataFormatWarnings(self, roleIndex, startingLane, endingLane): warn_needed = False opTop = self.topLevelOperator for lane_index in range(startingLane, endingLane + 1): output_slot = opTop.ImageGroup[lane_index][roleIndex] if output_slot.meta.inefficient_format: warn_needed = True if warn_needed: QMessageBox.warning( self, "Inefficient Data Format", "Your data cannot be accessed efficiently in its current format. " "Check the console output for details.\n" "(For HDF5 files, be sure to enable chunking on your dataset.)" ) @threadRouted def handleDatasetConstraintError(self, info, filename, ex, roleIndex, laneIndex, return_val=[False]): msg = "Can't use default properties for dataset:\n\n" + \ filename + "\n\n" + \ "because it violates a constraint of the {} applet.\n\n".format( ex.appletName ) + \ ex.message + "\n\n" + \ "Please enter valid dataset properties to continue." QMessageBox.warning(self, "Dataset Needs Correction", msg) # The success of this is 'returned' via our special out-param # (We can't return a value from this func because it is @threadRouted. successfully_repaired = self.repairDatasetInfo(info, roleIndex, laneIndex) return_val[0] = successfully_repaired def repairDatasetInfo(self, info, roleIndex, laneIndex): """Open the dataset properties editor and return True if the new properties are acceptable.""" defaultInfos = {} defaultInfos[laneIndex] = info editorDlg = DatasetInfoEditorWidget( self, self.topLevelOperator, roleIndex, [laneIndex], defaultInfos, show_axis_details=self.show_axis_details) dlg_state = editorDlg.exec_() return (dlg_state == QDialog.Accepted) @classmethod def getPossibleInternalPaths(cls, absPath, min_ndim=3, max_ndim=5): datasetNames = [] # Open the file as a read-only so we can get a list of the internal paths with h5py.File(absPath, 'r') as f: # Define a closure to collect all of the dataset names in the file. def accumulateDatasetPaths(name, val): if type(val) == h5py._hl.dataset.Dataset and min_ndim <= len( val.shape) <= max_ndim: datasetNames.append('/' + name) # Visit every group/dataset in the file f.visititems(accumulateDatasetPaths) return datasetNames def addStack(self, roleIndex, laneIndex): """ The user clicked the "Import Stack Files" button. """ stackDlg = StackFileSelectionWidget(self) stackDlg.exec_() if stackDlg.result() != QDialog.Accepted: return files = stackDlg.selectedFiles sequence_axis = stackDlg.sequence_axis if len(files) == 0: return cwd = self.topLevelOperator.WorkingDirectory.value info = DatasetInfo(os.path.pathsep.join(files), cwd=cwd) originalNumLanes = len(self.topLevelOperator.DatasetGroup) if laneIndex is None or laneIndex == -1: laneIndex = len(self.topLevelOperator.DatasetGroup) if len(self.topLevelOperator.DatasetGroup) < laneIndex + 1: self.topLevelOperator.DatasetGroup.resize(laneIndex + 1) def importStack(): self.parentApplet.busy = True self.parentApplet.appletStateUpdateRequested.emit() # Serializer will update the operator for us, which will propagate to the GUI. try: self.serializer.importStackAsLocalDataset(info, sequence_axis) try: self.topLevelOperator.DatasetGroup[laneIndex][ roleIndex].setValue(info) except DatasetConstraintError as ex: # Give the user a chance to repair the problem. filename = files[0] + "\n...\n" + files[-1] return_val = [False] self.handleDatasetConstraintError(info, filename, ex, roleIndex, laneIndex, return_val) if not return_val[0]: # Not successfully repaired. Roll back the changes and give up. self.topLevelOperator.DatasetGroup.resize( originalNumLanes) finally: self.parentApplet.busy = False self.parentApplet.appletStateUpdateRequested.emit() req = Request(importStack) req.notify_finished( lambda result: self.showDataset(laneIndex, roleIndex)) req.notify_failed( partial(self.handleFailedStackLoad, files, originalNumLanes)) req.submit() @threadRouted def handleFailedStackLoad(self, files, originalNumLanes, exc, exc_info): msg = "Failed to load stack due to the following error:\n{}".format( exc) msg += "\nAttempted stack files were:\n" msg += "\n".join(files) log_exception(logger, msg, exc_info) QMessageBox.critical(self, "Failed to load image stack", msg) self.topLevelOperator.DatasetGroup.resize(originalNumLanes) def handleClearDatasets(self, roleIndex, selectedRows): for row in selectedRows: self.topLevelOperator.DatasetGroup[row][roleIndex].disconnect() # Remove all operators that no longer have any connected slots laneIndexes = range(len(self.topLevelOperator.DatasetGroup)) for laneIndex, multislot in reversed( zip(laneIndexes, self.topLevelOperator.DatasetGroup)): any_ready = False for slot in multislot: any_ready |= slot.ready() if not any_ready: self.topLevelOperator.DatasetGroup.removeSlot( laneIndex, len(self.topLevelOperator.DatasetGroup) - 1) # Notify the workflow that something that could affect applet readyness has occurred. self.parentApplet.appletStateUpdateRequested.emit() def editDatasetInfo(self, roleIndex, laneIndexes): editorDlg = DatasetInfoEditorWidget( self, self.topLevelOperator, roleIndex, laneIndexes, show_axis_details=self.show_axis_details) editorDlg.exec_() self.parentApplet.appletStateUpdateRequested.emit() def updateInternalPathVisiblity(self): for view in self._detailViewerWidgets: model = view.model() view.setColumnHidden(DatasetDetailedInfoColumn.InternalID, not model.hasInternalPaths()) def addDvidVolume(self, roleIndex, laneIndex): # TODO: Provide list of recently used dvid hosts, loaded from user preferences recent_hosts_pref = PreferencesManager.Setting("DataSelection", "Recent DVID Hosts") recent_hosts = recent_hosts_pref.get() if not recent_hosts: recent_hosts = ["localhost:8000"] recent_hosts = filter(lambda h: h, recent_hosts) from dvidDataSelectionBrowser import DvidDataSelectionBrowser browser = DvidDataSelectionBrowser(recent_hosts, parent=self) if browser.exec_() == DvidDataSelectionBrowser.Rejected: return if None in browser.get_selection(): QMessageBox.critical("Couldn't use your selection.") return rois = None hostname, dset_uuid, volume_name, uuid = browser.get_selection() dvid_url = 'http://{hostname}/api/node/{uuid}/{volume_name}'.format( **locals()) subvolume_roi = browser.get_subvolume_roi() # Relocate host to top of 'recent' list, and limit list to 10 items. try: i = recent_hosts.index(hostname) del recent_hosts[i] except ValueError: pass finally: recent_hosts.insert(0, hostname) recent_hosts = recent_hosts[:10] # Save pref recent_hosts_pref.set(recent_hosts) if subvolume_roi is None: self.addFileNames([dvid_url], roleIndex, laneIndex) else: # In ilastik, we display the dvid volume axes in C-order, despite the dvid convention of F-order # Transpose the subvolume roi to match # (see implementation of OpDvidVolume) start, stop = subvolume_roi start = tuple(reversed(start)) stop = tuple(reversed(stop)) self.addFileNames([dvid_url], roleIndex, laneIndex, [(start, stop)])
class TableWidget(QWidget): def __init__(self): super(TableWidget, self).__init__() vbox = QVBoxLayout(self) vbox.setContentsMargins(0, 0, 0, 0) self.relations = {} # Stack self.stacked = QStackedWidget() vbox.addWidget(self.stacked) def count(self): return self.stacked.count() def add_data_base(self, data): lateral = Pireal.get_service("lateral") rel = None for part in data.split('@'): for e, line in enumerate(part.splitlines()): if e == 0: name = line.split(':')[0] rel = relation.Relation() rel.fields = line.split(':')[-1].split(',') else: rel.insert(line.split(',')) if rel is not None: table = Table() table.setRowCount(1) table.setColumnCount(0) self.relations[name] = rel for _tuple in rel.content: row = table.rowCount() table.setColumnCount(len(rel.fields)) for column, text in enumerate(_tuple): item = Item() item.setText(text) table.setItem(row - 1, column, item) table.insertRow(row) table.setHorizontalHeaderLabels(rel.fields) self.stacked.addWidget(table) table.removeRow(table.rowCount() - 1) lateral.add_item_list([name]) def load_relation(self, filenames): lateral = Pireal.get_service("lateral") for filename in filenames: rel = relation.Relation(filename) rel_name = file_manager.get_basename(filename) self.relations[rel_name] = rel table = Table() table.setRowCount(1) table.setColumnCount(0) for _tuple in rel.content: row = table.rowCount() table.setColumnCount(len(rel.fields)) for column, text in enumerate(_tuple): item = Item() item.setText(text) table.setItem(row - 1, column, item) table.insertRow(row) table.removeRow(table.rowCount() - 1) table.setHorizontalHeaderLabels(rel.fields) self.stacked.addWidget(table) lateral.add_item_list([rel_name]) def add_table(self, rows, columns, name, data, fields): table = Table() table.setRowCount(rows) table.setColumnCount(columns) table.setHorizontalHeaderLabels(fields) for k, v in list(data.items()): item = QTableWidgetItem() item.setText(v) table.setItem(k[0] - 1, k[1], item) self.stacked.addWidget(table) self.stacked.setCurrentIndex(self.stacked.count() - 1) lateral = Pireal.get_service("lateral") lateral.add_item_list([name]) def add_new_table(self, rel, name): import itertools table = Table() table.setRowCount(0) table.setColumnCount(0) data = itertools.chain([rel.fields], rel.content) for row_data in data: row = table.rowCount() table.setColumnCount(len(row_data)) for col, text in enumerate(row_data): item = QTableWidgetItem() item.setText(text) if row == 0: table.setHorizontalHeaderItem(col, item) else: table.setItem(row - 1, col, item) table.insertRow(row) table.removeRow(table.rowCount() - 1) self.stacked.addWidget(table) self.stacked.setCurrentIndex(self.stacked.count() - 1) lateral = Pireal.get_service("lateral") lateral.add_item_list([name]) def remove_table(self, index): table = self.stacked.widget(index) self.stacked.removeWidget(table) def add_table_from_rdb_content(self, content): lateral = Pireal.get_service("lateral") lateral.show() for line in content.splitlines(): if line.startswith('@'): table = Table() name = line.split(':')[0][1:] lateral.add_item_list([name]) fields = line.split(':')[-1].split(',')[:-1] table.setColumnCount(len(fields)) table.setHorizontalHeaderLabels(fields) self.stacked.addWidget(table) else: row = table.rowCount() for e, i in enumerate(line.split(',')): item = QTableWidgetItem() item.setText(i) table.setItem(row - 1, e, item) table.insertRow(row)
class LunchMenuWidget(QWidget): textViewIndex = 0 textViewAdditivesMap = {} def __init__(self, parent): super(LunchMenuWidget, self).__init__(parent) box = QVBoxLayout(self) box.addWidget(QLabel(u"Initializing...", self)) def initializeLayout(self): layout = self.layout() child = layout.takeAt(0) while child != None: child.widget().deleteLater() child = layout.takeAt(0) self.messages = LunchMenu.messages() self.toggleMessages = LunchMenu.toggleMessages() self.additives = LunchMenu.additives() self.toggleAdditives = LunchMenu.toggleAdditives() buttonBar = self.createButtonBar(self) layout.addLayout(buttonBar) self.menuNotebook = QStackedWidget(self) self.createNotebook() layout.addWidget(self.menuNotebook) self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) def create_arrow_button(self, parent, arrow_type): button = QToolButton(parent) button.setArrowType(arrow_type) return button def goLeft(self): curIndex = self.combobox.currentIndex() if curIndex > 0: self.combobox.setCurrentIndex(curIndex - 1) def goRight(self): curIndex = self.combobox.currentIndex() if curIndex < 4: self.combobox.setCurrentIndex(curIndex + 1) def goToday(self): now = LunchMenu.today() minDelta = sys.maxint minDeltaI = 0 i = 0 for aLunchMenu in LunchMenu.allLunchMenus(): if aLunchMenu == None or isinstance(aLunchMenu, Exception): # parse error, use current day of week if now.weekday() < 5: minDeltaI = now.weekday() else: minDeltaI = 4 break td = now - aLunchMenu.lunchDate delta = abs((td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / 10**6) if delta < minDelta: minDelta = delta minDeltaI = i i = i + 1 self.combobox.setCurrentIndex(minDeltaI) def goTodayClicked(self): self.goToday() def isToggled(self): index = self.menuNotebook.currentIndex() return (index >= 5) def changed_combo(self,index): if self.isToggled(): self.menuNotebook.setCurrentIndex(index + 5) else: self.menuNotebook.setCurrentIndex(index) self.leftButton.setEnabled(index != 0) self.rightButton.setEnabled(index != 4) def toggleLanguage(self): index = self.menuNotebook.currentIndex() isToggle = (index >= 5) if isToggle: self.switchLanguageButton.setText(self.messages["toggleLanguage"]) index = index - 5 else: self.switchLanguageButton.setText(self.messages["toggleLanguage2"]) index = index + 5 self.menuNotebook.setCurrentIndex(index) def createButtonBar(self, parent): self.combobox = QComboBox(parent) self.combobox.addItem(self.messages['monday']) self.combobox.addItem(self.messages['tuesday']) self.combobox.addItem(self.messages['wednesday']) self.combobox.addItem(self.messages['thursday']) self.combobox.addItem(self.messages['friday']) self.combobox.currentIndexChanged.connect(self.changed_combo) comboBoxHeight = self.combobox.sizeHint().height() self.leftButton = self.create_arrow_button(parent, Qt.LeftArrow) self.leftButton.clicked.connect(self.goLeft) self.leftButton.setMinimumSize(comboBoxHeight, comboBoxHeight) self.rightButton = self.create_arrow_button(parent, Qt.RightArrow) self.rightButton.clicked.connect(self.goRight) self.rightButton.setMinimumSize(comboBoxHeight, comboBoxHeight) navButtons = QHBoxLayout() navButtons.addWidget(self.leftButton, 0, Qt.AlignRight) navButtons.addWidget(self.combobox, 0, Qt.AlignCenter) navButtons.addWidget(self.rightButton, 0, Qt.AlignLeft) buttonBar = QHBoxLayout() todayButton = QPushButton(self.messages['today'], parent) todayButton.clicked.connect(self.goTodayClicked) todayButton.setMinimumHeight(comboBoxHeight) buttonBar.addWidget(todayButton) buttonBar.addWidget(QWidget(parent), 1) buttonBar.addLayout(navButtons, 1) buttonBar.addWidget(QWidget(parent), 1) self.switchLanguageButton = QPushButton(self.messages["toggleLanguage"], parent) self.switchLanguageButton.clicked.connect(self.toggleLanguage) self.switchLanguageButton.setMinimumHeight(comboBoxHeight) buttonBar.addWidget(self.switchLanguageButton, 0, Qt.AlignRight) return buttonBar def addMenuLine(self, parent, text, box, header = False): aLabel = QLabel(text, parent) if header: aLabel.setAlignment(Qt.AlignCenter) oldFont = aLabel.font() aLabel.setFont(QFont(oldFont.family(), 13, QFont.Bold)) box.addWidget(aLabel, 0, Qt.AlignBottom) def addLocaleErrorPage(self, parent, box, toggle): aLabel = QLabel(self.messages['parseLocaleError'], parent) aLabel.setWordWrap(True) box.addWidget(aLabel) aButton = QPushButton(self.messages['installLocaleButton'], parent) if toggle: aButton.clicked.connect(self.installLanguageSupportToggle) else: aButton.clicked.connect(self.installLanguageSupport) box.addWidget(aButton) def addExceptionPage(self, parent, box, error, _toggle): aLabel = QLabel(self.messages['otherException'] + u" " + unicode(error), parent) aLabel.setWordWrap(True) box.addWidget(aLabel) def installLanguageSupportForLocale(self, locale): locale = locale.partition("_")[0] if subprocess.call(['gksu', "apt-get -q -y install language-pack-%s" % locale])!=0: QMessageBox().critical(self.menuNotebook, "Installation Error", self.messages['installLocaleError'], buttons=QMessageBox.Ok, defaultButton=QMessageBox.Ok) else: QMessageBox().information(self.menuNotebook, "Success", self.messages['installLocaleSuccess'], buttons=QMessageBox.Ok, defaultButton=QMessageBox.Ok) def installLanguageSupport(self): self.installLanguageSupportForLocale(self.defaultLocaleString) def installLanguageSupportToggle(self): self.installLanguageSupportForLocale(self.messages['toggleLocale']) def formatTitleAndDescription(self, title, description, keyInfo): if title and description: result = "%s, %s" % (title, description) elif title: result = title else: result = description if keyInfo: return "%s: %s" % (keyInfo.title(), result) return result def addMenuContent(self, parent, desc, menuContents, box, messages, additivesDict): self.addMenuLine(parent, desc, box) if desc in menuContents: contentList = menuContents[desc] else: contentList = [(messages[u'noContents'], None, [], None)] log_debug("lunch menu does not contain key '%s'" % desc) textview = GrowingTextEdit(parent, messages, additivesDict) textview.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) textview.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) textview.setLineWrapMode(QTextEdit.WidgetWidth) textview.setReadOnly(True) textview.document().setIndentWidth(10) if len(contentList) == 1: title, description, additives, keyInfo = contentList[0] textview.append(self.formatTitleAndDescription(title, description, keyInfo), additives) elif len(contentList) > 1: cursor = textview.textCursor() listFormat = QTextListFormat() listFormat.setStyle(QTextListFormat.ListDisc) listFormat.setIndent(1) cursor.createList(listFormat) for title, description, additives, keyInfo in contentList: textview.append(self.formatTitleAndDescription(title, description, keyInfo), additives) box.addWidget(textview, 0) def createNotebook(self): self.combobox.setCurrentIndex(0) for _ in range(self.menuNotebook.count()): self.menuNotebook.removeWidget(self.menuNotebook.widget(0)) curMessages = self.messages curAdditives = self.additives for index in range(10): if index == 5: try: if getPlatform() != PLATFORM_WINDOWS: locale.setlocale(locale.LC_TIME, (self.messages["toggleLocale"],"UTF-8")) except: log_exception("error setting locale") curMessages = self.toggleMessages curAdditives = self.toggleAdditives pageWidget = QWidget(self.menuNotebook) page = QVBoxLayout(pageWidget) thisLunchMenu = LunchMenu.allLunchMenus()[index] if thisLunchMenu != None and type(thisLunchMenu) == LunchMenu: title = curMessages['lunchMenuFor'] + u" " + thisLunchMenu.lunchDate.strftime(curMessages['dateFormatDisplayed']).decode("utf-8") self.addMenuLine(pageWidget, title, page, True) if thisLunchMenu.isValid(): self.addMenuContent(pageWidget, curMessages['soupDisplayed'], thisLunchMenu.contents, page, curMessages, curAdditives) self.addMenuContent(pageWidget, curMessages['mainDishesDisplayed'], thisLunchMenu.contents, page, curMessages, curAdditives) self.addMenuContent(pageWidget, curMessages['supplementsDisplayed'], thisLunchMenu.contents, page, curMessages, curAdditives) self.addMenuContent(pageWidget, curMessages['dessertsDisplayed'], thisLunchMenu.contents, page, curMessages, curAdditives) else: self.addMenuLine(pageWidget, curMessages['noLunchToday'], page) elif type(thisLunchMenu) == locale.Error: self.addLocaleErrorPage(pageWidget, page, index >= 5) pass elif isinstance(thisLunchMenu, Exception): self.addExceptionPage(pageWidget, page, thisLunchMenu, index >= 5) self.menuNotebook.addWidget(pageWidget) try: if getPlatform() != PLATFORM_WINDOWS: locale.setlocale(locale.LC_TIME, (LunchMenu.defaultLocaleString,"UTF-8")) except: log_exception("error setting locale") self.goToday()
class Preview(QWidget): def __init__(self, parent): QWidget.__init__(self, parent) self.layout = QVBoxLayout(self) self.layout.setSpacing(0) self.layout.setContentsMargins(0, 0, 0, 0) self.stack = QStackedWidget(self) self.toolBar = QToolBar(self) self.connect(self.toolBar, SIGNAL('actionTriggered(QAction*)'), self.clicked) self.layout.addWidget(self.toolBar) self.layout.addWidget(self.stack) self.setLayout(self.layout) self.__mainWindow = parent self.name = "Preview" self.loader = loader() self.lmodules = self.loader.modules void = QWidget() self.previousWidget = void self.stack.addWidget(void) self.setWindowIcon(QIcon(QPixmap(":viewer.png"))) self.retranslateUi(self) self.previousNode = None self.currentNode = None self.mustUpdate = True def updateCheckState(self, state): self.mustUpdate = state if state: self.setDisabled(False) self.update(self.currentNode) else: self.setDisabled(True) def clicked(self, action): if self.isVisible( ) and self.mustUpdate and self.currentNode and self.currentNode.size(): self.display(self.currentNode, str(action.text())) def update(self, node): if self.isVisible() and self.mustUpdate and node and node.size(): if self.previousNode == node.this: return else: self.toolBar.clear() self.toolBar.addAction("hexadecimal") self.previousNode = node.this self.currentNode = node previewModules = [] compat = node.compatibleModules() if len(compat): for module in compat: if "Viewers" in self.lmodules[module].tags: previewModules.append(module) self.toolBar.addAction(module) if not len(previewModules): self.display(node, "hexadecimal") else: self.display(node, str(previewModules[0])) elif node and node.size(): self.currentNode = node def display(self, node, previewModule): try: args = {} args["file"] = node args["preview"] = True conf = self.loader.get_conf(previewModule) genargs = conf.generate(args) if self.previousWidget: self.stack.removeWidget(self.previousWidget) self.previousWidget.close() del self.previousWidget self.previousWidget = self.lmodules[previewModule].create() self.previousWidget.start(genargs) self.previousWidget.g_display() if str(self.previousWidget).find("player.PLAYER") == -1: self.previousWidget.setAttribute(Qt.WA_DeleteOnClose) self.stack.addWidget(self.previousWidget) except: pass def retranslateUi(self, widget): widget.setWindowTitle( QApplication.translate("Preview", "Preview", None, QApplication.UnicodeUTF8))
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 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 LunchMenuWidget(QWidget): textViewIndex = 0 textViewAdditivesMap = {} def __init__(self, parent): super(LunchMenuWidget, self).__init__(parent) box = QVBoxLayout(self) box.addWidget(QLabel(u"Initializing...", self)) def initializeLayout(self): layout = self.layout() child = layout.takeAt(0) while child != None: child.widget().deleteLater() child = layout.takeAt(0) self.messages = LunchMenu.messages() self.toggleMessages = LunchMenu.toggleMessages() self.additives = LunchMenu.additives() self.toggleAdditives = LunchMenu.toggleAdditives() buttonBar = self.createButtonBar(self) layout.addLayout(buttonBar) self.menuNotebook = QStackedWidget(self) self.createNotebook() layout.addWidget(self.menuNotebook) self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) def create_arrow_button(self, parent, arrow_type): button = QToolButton(parent) button.setArrowType(arrow_type) return button def goLeft(self): curIndex = self.combobox.currentIndex() if curIndex > 0: self.combobox.setCurrentIndex(curIndex - 1) def goRight(self): curIndex = self.combobox.currentIndex() if curIndex < 4: self.combobox.setCurrentIndex(curIndex + 1) def goToday(self): now = LunchMenu.today() minDelta = sys.maxint minDeltaI = 0 i = 0 for aLunchMenu in LunchMenu.allLunchMenus(): if aLunchMenu == None or isinstance(aLunchMenu, Exception): # parse error, use current day of week if now.weekday() < 5: minDeltaI = now.weekday() else: minDeltaI = 4 break td = now - aLunchMenu.lunchDate delta = abs((td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / 10**6) if delta < minDelta: minDelta = delta minDeltaI = i i = i + 1 self.combobox.setCurrentIndex(minDeltaI) def goTodayClicked(self): self.goToday() def isToggled(self): index = self.menuNotebook.currentIndex() return (index >= 5) def changed_combo(self, index): if self.isToggled(): self.menuNotebook.setCurrentIndex(index + 5) else: self.menuNotebook.setCurrentIndex(index) self.leftButton.setEnabled(index != 0) self.rightButton.setEnabled(index != 4) def toggleLanguage(self): index = self.menuNotebook.currentIndex() isToggle = (index >= 5) if isToggle: self.switchLanguageButton.setText(self.messages["toggleLanguage"]) index = index - 5 else: self.switchLanguageButton.setText(self.messages["toggleLanguage2"]) index = index + 5 self.menuNotebook.setCurrentIndex(index) def createButtonBar(self, parent): self.combobox = QComboBox(parent) self.combobox.addItem(self.messages['monday']) self.combobox.addItem(self.messages['tuesday']) self.combobox.addItem(self.messages['wednesday']) self.combobox.addItem(self.messages['thursday']) self.combobox.addItem(self.messages['friday']) self.combobox.currentIndexChanged.connect(self.changed_combo) comboBoxHeight = self.combobox.sizeHint().height() self.leftButton = self.create_arrow_button(parent, Qt.LeftArrow) self.leftButton.clicked.connect(self.goLeft) self.leftButton.setMinimumSize(comboBoxHeight, comboBoxHeight) self.rightButton = self.create_arrow_button(parent, Qt.RightArrow) self.rightButton.clicked.connect(self.goRight) self.rightButton.setMinimumSize(comboBoxHeight, comboBoxHeight) navButtons = QHBoxLayout() navButtons.addWidget(self.leftButton, 0, Qt.AlignRight) navButtons.addWidget(self.combobox, 0, Qt.AlignCenter) navButtons.addWidget(self.rightButton, 0, Qt.AlignLeft) buttonBar = QHBoxLayout() todayButton = QPushButton(self.messages['today'], parent) todayButton.clicked.connect(self.goTodayClicked) todayButton.setMinimumHeight(comboBoxHeight) buttonBar.addWidget(todayButton) buttonBar.addWidget(QWidget(parent), 1) buttonBar.addLayout(navButtons, 1) buttonBar.addWidget(QWidget(parent), 1) self.switchLanguageButton = QPushButton( self.messages["toggleLanguage"], parent) self.switchLanguageButton.clicked.connect(self.toggleLanguage) self.switchLanguageButton.setMinimumHeight(comboBoxHeight) buttonBar.addWidget(self.switchLanguageButton, 0, Qt.AlignRight) return buttonBar def addMenuLine(self, parent, text, box, header=False): aLabel = QLabel(text, parent) if header: aLabel.setAlignment(Qt.AlignCenter) oldFont = aLabel.font() aLabel.setFont(QFont(oldFont.family(), 13, QFont.Bold)) box.addWidget(aLabel, 0, Qt.AlignBottom) def addLocaleErrorPage(self, parent, box, toggle): aLabel = QLabel(self.messages['parseLocaleError'], parent) aLabel.setWordWrap(True) box.addWidget(aLabel) aButton = QPushButton(self.messages['installLocaleButton'], parent) if toggle: aButton.clicked.connect(self.installLanguageSupportToggle) else: aButton.clicked.connect(self.installLanguageSupport) box.addWidget(aButton) def addExceptionPage(self, parent, box, error, _toggle): aLabel = QLabel( self.messages['otherException'] + u" " + unicode(error), parent) aLabel.setWordWrap(True) box.addWidget(aLabel) def installLanguageSupportForLocale(self, locale): locale = locale.partition("_")[0] if subprocess.call( ['gksu', "apt-get -q -y install language-pack-%s" % locale]) != 0: QMessageBox().critical(self.menuNotebook, "Installation Error", self.messages['installLocaleError'], buttons=QMessageBox.Ok, defaultButton=QMessageBox.Ok) else: QMessageBox().information(self.menuNotebook, "Success", self.messages['installLocaleSuccess'], buttons=QMessageBox.Ok, defaultButton=QMessageBox.Ok) def installLanguageSupport(self): self.installLanguageSupportForLocale(self.defaultLocaleString) def installLanguageSupportToggle(self): self.installLanguageSupportForLocale(self.messages['toggleLocale']) def formatTitleAndDescription(self, title, description, keyInfo): if title and description: result = "%s, %s" % (title, description) elif title: result = title else: result = description if keyInfo: return "%s: %s" % (keyInfo.title(), result) return result def addMenuContent(self, parent, desc, menuContents, box, messages, additivesDict): self.addMenuLine(parent, desc, box) if desc in menuContents: contentList = menuContents[desc] else: contentList = [(messages[u'noContents'], None, [], None)] log_debug("lunch menu does not contain key '%s'" % desc) textview = GrowingTextEdit(parent, messages, additivesDict) textview.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) textview.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) textview.setLineWrapMode(QTextEdit.WidgetWidth) textview.setReadOnly(True) textview.document().setIndentWidth(10) if len(contentList) == 1: title, description, additives, keyInfo = contentList[0] textview.append( self.formatTitleAndDescription(title, description, keyInfo), additives) elif len(contentList) > 1: cursor = textview.textCursor() listFormat = QTextListFormat() listFormat.setStyle(QTextListFormat.ListDisc) listFormat.setIndent(1) cursor.createList(listFormat) for title, description, additives, keyInfo in contentList: textview.append( self.formatTitleAndDescription(title, description, keyInfo), additives) box.addWidget(textview, 0) def createNotebook(self): self.combobox.setCurrentIndex(0) for _ in range(self.menuNotebook.count()): self.menuNotebook.removeWidget(self.menuNotebook.widget(0)) curMessages = self.messages curAdditives = self.additives for index in range(10): if index == 5: try: if getPlatform() != PLATFORM_WINDOWS: locale.setlocale( locale.LC_TIME, (self.messages["toggleLocale"], "UTF-8")) except: log_exception("error setting locale") curMessages = self.toggleMessages curAdditives = self.toggleAdditives pageWidget = QWidget(self.menuNotebook) page = QVBoxLayout(pageWidget) thisLunchMenu = LunchMenu.allLunchMenus()[index] if thisLunchMenu != None and type(thisLunchMenu) == LunchMenu: title = curMessages[ 'lunchMenuFor'] + u" " + thisLunchMenu.lunchDate.strftime( curMessages['dateFormatDisplayed']).decode("utf-8") self.addMenuLine(pageWidget, title, page, True) if thisLunchMenu.isValid(): self.addMenuContent(pageWidget, curMessages['soupDisplayed'], thisLunchMenu.contents, page, curMessages, curAdditives) self.addMenuContent(pageWidget, curMessages['mainDishesDisplayed'], thisLunchMenu.contents, page, curMessages, curAdditives) self.addMenuContent(pageWidget, curMessages['supplementsDisplayed'], thisLunchMenu.contents, page, curMessages, curAdditives) self.addMenuContent(pageWidget, curMessages['dessertsDisplayed'], thisLunchMenu.contents, page, curMessages, curAdditives) else: self.addMenuLine(pageWidget, curMessages['noLunchToday'], page) elif type(thisLunchMenu) == locale.Error: self.addLocaleErrorPage(pageWidget, page, index >= 5) pass elif isinstance(thisLunchMenu, Exception): self.addExceptionPage(pageWidget, page, thisLunchMenu, index >= 5) self.menuNotebook.addWidget(pageWidget) try: if getPlatform() != PLATFORM_WINDOWS: locale.setlocale(locale.LC_TIME, (LunchMenu.defaultLocaleString, "UTF-8")) except: log_exception("error setting locale") self.goToday()
class EditorWidget(QWidget): # Señales allFilesClosed = pyqtSignal() def __init__(self): super(EditorWidget, self).__init__() self._recents_files = [] box = QVBoxLayout(self) box.setContentsMargins(0, 0, 0, 0) box.setSpacing(0) # Combo container self.combo = ComboContainer(self) box.addWidget(self.combo) # Stacked self.stack = QStackedWidget() box.addWidget(self.stack) self.connect(self.combo.combo_file, SIGNAL("currentIndexChanged(int)"), self.change_item) def add_widget(self, widget): index = self.stack.addWidget(widget) if not self.combo.isVisible(): self.combo.setVisible(True) self.stack.setCurrentIndex(index) def add_item_combo(self, text): self.combo.combo_file.addItem(text) self.combo.combo_file.setCurrentIndex( self.combo.combo_file.count() - 1) def remove_item_combo(self, index): self.combo.combo_file.removeItem(index) def change_item(self, index): self.stack.setCurrentIndex(index) self.emit(SIGNAL("currentWidgetChanged(int)"), index) def current_widget(self): return self.stack.currentWidget() def current_index(self): return self.stack.currentIndex() def widget(self, index): return self.stack.widget(index) def count(self): return self.stack.count() def close_file(self): self.remove_widget(self.current_widget(), self.current_index()) def close_file_project(self, widget, index): #FIXME: unir con close file self.remove_widget(widget, index) def close_all(self): for index in range(self.count()): self.remove_widget(self.current_widget(), 0) def editor_modified(self, value): weditor = self.current_widget() index = self.current_index() self.combo.set_modified(weditor, index, value) def _add_to_recent(self, filename): if filename == 'Untitled': return if filename not in self._recents_files: self._recents_files.append(filename) self.emit(SIGNAL("recentFile(QStringList)"), self._recents_files) def check_files_not_saved(self): value = False for index in range(self.count()): weditor = self.widget(index) value = value or weditor.is_modified return value def files_not_saved(self): files = [] for index in range(self.count()): weditor = self.widget(index) if weditor.is_modified: files.append(weditor.filename) return files def opened_files(self): files = [] for index in range(self.count()): weditor = self.widget(index) path = weditor.filename if path == 'Untitled': continue files.append(path) return files def remove_widget(self, widget, index): if not isinstance(widget, editor.Editor): return if index != -1: self.stack.setCurrentIndex(index) flags = QMessageBox.Yes flags |= QMessageBox.No flags |= QMessageBox.Cancel result = QMessageBox.No if widget.is_modified: result = QMessageBox.question(self, self.tr( "Archivo no guardado!"), self.tr("El archivo <b>{0}</b> " "tiene cambios sin guardar. " "Quieres guardarlos?").format(widget.filename), QMessageBox.Yes, QMessageBox.No, QMessageBox.Cancel) if result == QMessageBox.Cancel: return elif result == QMessageBox.Yes: self.emit(SIGNAL("saveCurrentFile()")) self._add_to_recent(widget.filename) self.stack.removeWidget(widget) self.emit(SIGNAL("fileClosed(int)"), index) self.remove_item_combo(index) widget.obj_file.stop_system_watcher() if self.current_widget() is not None: self.current_widget().setFocus() else: self.allFilesClosed.emit() def add_symbols(self, symbols): self.combo.add_symbols_combo(symbols)
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 FltsSearchDockWidget(QDockWidget): """ Dock widget for showing search widgets with each widget corresponding to a search configuration. """ def __init__(self, *args, **kwargs): super(FltsSearchDockWidget, self).__init__(*args, **kwargs) self.setAllowedAreas(Qt.BottomDockWidgetArea) self._search_reg = SearchConfigurationRegistry.instance() self._stack_widget = QStackedWidget(self) self.setWidget(self._stack_widget) # Index data source to their location in the stack widget self._search_widget_idx = dict() def show_search_widget(self, data_source): """ Shows the search widget associated with the given data source. If not found then it will create a new one by using the factory method in the search configuration. :param data_source: Data source name. :type data_source: str :return: Returns True if the operation was successful else False. It will be false if the data source is not found in the registry. :rtype: bool """ config = self._search_reg.search_config(data_source) if not config: return False # Set current search widget self._set_current_widget(config) return True def _set_current_widget(self, config): # Updates dock widget based on the specified config. data_source = config.data_source # Create widget if it does not exist in the stack if not data_source in self._search_widget_idx: search_widget = config.create_widget() idx = self._stack_widget.addWidget(search_widget) self._search_widget_idx[data_source] = idx else: idx = self._search_widget_idx[data_source] self._stack_widget.setCurrentIndex(idx) # Set title self.setWindowTitle(u'Search {0}'.format(config.display_name)) def current_widget(self): """ :return: Returns the current search widget or None if there are no widgets in the stack. :rtype: QWidget """ return self._stack_widget.currentWidget() def clear(self): """ Removes all the search widgets and resets the indices. """ while self._stack_widget.count() > 0: sw = self._stack_widget.widget(0) self._stack_widget.removeWidget(sw) del sw self._search_widget_idx = dict()
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 FragmentFrame(QFrame): # Signals: SIG_REMOVE_FRAGMENT = 'removeFragment()' # Fragment edition switch FRAGMENT_EDITION = False view = None fetcher = None def __init__(self, fragment, args, client, animation_manager=None, parent=None, has_menu_pos=True): """ @fragment [Fragment] user_settings.Fragment object to describe fragment @client [RpcdClient] client @parent [QWidget] parent widget """ #assert isinstance(client, RpcdClient) assert frag_types.has_key(fragment.type) QFrame.__init__(self, parent) self.animation_manager = animation_manager self.has_menu_pos = has_menu_pos self.fragment = fragment self.fetcher = frag_types[fragment.type].fetcher(fragment, args, client) self.connect(self.fetcher, SIGNAL(self.fetcher.ERROR_SIGNAL), self.errorHandler) self.window = parent self.cumulative_mode = False self.interval = Interval('daily') self.setFragmentColor() self.setFrameShadow(QFrame.Sunken) self.setFrameShape(QFrame.StyledPanel) self.setContextMenuPolicy(Qt.ActionsContextMenu) self.toolspace = None self.vbox = QVBoxLayout() # self.vbox.setContentsMargins(9,0,9,9) self.vbox.setContentsMargins(9,0,9,0) self.setLayout(self.vbox) self.stacked = QStackedWidget(self) updating_label = QLabel("<img src=\":/icons/refresh.png\" /><br />%s" % self.tr("Updating...")) updating_label.setAlignment(Qt.AlignHCenter|Qt.AlignVCenter) self.stacked.addWidget(updating_label) # Create all actions for the rightclick context menu self.action_list = [] self.switch_actions = {} self.viewlayout = QHBoxLayout() self.viewlayout.setContentsMargins(0,0,0,0) self.viewlayout.setSpacing(2) self.viewlayout.addStretch() widget = QWidget() widget.setLayout(self.viewlayout) self.vbox.addWidget(widget) self.line = QFrame(self) self.line.setFrameShape(QFrame.HLine) self.line.setFrameShadow(QFrame.Sunken) self.line.setObjectName("line") # Menu to choose position of fragment if self.has_menu_pos: self.pos_menu = QMenu(tr('Position'), self) # self.pos_action = QAction(tr('Position'), self.pos_menu) def make_lambda(l): """ usefull to create the lambda function with a copied parameter. or it'll bug """ return lambda: QTimer.singleShot(0, lambda: self.setView(l)) self.buttons = [] button = QToolButton() button.visible = True button.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Ignored) button.setMinimumSize(0,16) button.setIcon(QIcon(":/icons/refresh.png")) button.setFixedHeight(16) button.setToolTip(tr("Refresh")) self.connect(button, SIGNAL("clicked()"), self.updateData) self.viewlayout.addWidget(button) self.buttons.append(button) # All of the views available for this kind of fragment. if len(frag_types[fragment.type].views) > 1: for label in frag_types[fragment.type].views: try: item_name = views_list_label[label] except KeyError: continue # item_name returns a unicode string, but PyQT (Qt 4.2.1) won't convert it to a char* # unless we convert it to a non-unicode string .... action = QAction(QIcon(':/icons/%s' % label), tr("Switch to %s") % self.tr(unicode(item_name)), self) self.connect(action, SIGNAL("triggered()"), make_lambda(label)) self.action_list += [action] self.switch_actions[label] = action button = QToolButton() button.visible = True button.setBackgroundRole(QPalette.Button) button.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Minimum) button.setMinimumSize(0,16) button.setFixedHeight(16) button.setIcon(QIcon(":/icons/%s" % label)) button.setToolTip(tr("Switch to %s") % self.tr(unicode(item_name))) self.connect(button, SIGNAL("clicked()"), make_lambda(label)) self.viewlayout.addWidget(button) self.buttons.append(button) # Separator action = QAction(self) action.setSeparator(True) self.action_list += [action] # Refresh action = QAction(QIcon(':/icons/refresh.png'), tr('Refresh'), self) self.connect(action, SIGNAL('triggered()'), self.updateData) self.action_list += [action] if self.FRAGMENT_EDITION: # Edit action = QAction(QIcon(':/icons/edit.png'), tr('Edit this fragment...'), self) self.connect(action, SIGNAL('triggered()'), self.editFragment) self.action_list += [action] # Delete action = QAction(QIcon(':/icons/moins.png'), tr('Delete this fragment'), self) self.connect(action, SIGNAL('triggered()'), self.removeFragment) self.action_list += [action] self.setView(fragment.view, update=False) self.setAcceptDrops(True) self.pos = -1 def mouseMoveEvent(self, event): if event.buttons() != Qt.LeftButton: return mimeData = QMimeData() if self.pos == -1: return mimeData.setData("splitter/fragment", QByteArray.number(self.pos)) drag = QDrag(self) drag.setMimeData(mimeData) drag.setHotSpot(event.pos() - self.rect().topLeft()) dropAction = drag.start(Qt.MoveAction) if dropAction == Qt.MoveAction: self.close() def dragEnterEvent(self, event): event.accept() def dropEvent(self, event): data = event.mimeData().data("splitter/fragment").toInt() if not data[1]: return frame_pos = data[0] if frame_pos == self.pos: return event.setDropAction(Qt.MoveAction) event.accept() self.window.changedPositionFragment(self.pos, frame_pos) # def __del__(self): # self.destructor() def destructor(self): if self.view: self.freeView() if self.fetcher: self.disconnect(self.fetcher, SIGNAL(self.fetcher.ERROR_SIGNAL), self.errorHandler) self.fetcher.destructor() self.fetcher = None def getView(self): return self.view def getFragment(self): return self.fragment def setCumulative(self, cumulative): self.cumulative_mode = cumulative if self.view: self.view.setCumulative(cumulative) def setInterval(self, interval): self.interval = interval if self.view: self.view.setInterval(interval) def setFragmentColor(self): # XXX: We have to put classes which we colorize, because some objects bug # when we try to put a background on them. For example, do NOT colorize # QScrollBar, it'll not work anyway: # http://doc.trolltech.com/4.4/stylesheet-examples.html#customizing-qscrollbar # "The QScrollBar can be styled using its subcontrols like handle, # add-line, sub-line, and so on. Note that if one property or # sub-control is customized, all the other properties or sub-controls # must be customized as well." self.setStyleSheet(""" QFrame, QPushButton, QTableCornerButton, QAbstractSpinBox, QLineEdit { background-color: #%06X; } """ % self.fragment.background_color) def editFragment(self): if AddFragDialog(self.window, self.fragment, self).run(): self.setFragmentColor() self.setView(self.fragment.view, update=True) def removeFragment(self): reply = QMessageBox.question(self, self.tr("Delete a fragment"), unicode(self.tr('Are you sure to delete the fragment "%s" from the view?')) % self.fragment.title, QMessageBox.Yes|QMessageBox.No) if reply == QMessageBox.Yes: QTimer.singleShot(0, self._removeFragment_emit) def _removeFragment_emit(self): self.emit(SIGNAL(self.SIG_REMOVE_FRAGMENT)) # def resizeEvent(self, event): # QFrame.resizeEvent(self, event) # self.view.resize(event.size()) def freeView(self): if self.view: self.view.destructor() self.disconnect(self.view, SIGNAL('open_page'), self._open_page) self.disconnect(self.view, SIGNAL('add_filter'), self._add_filter) self.disconnect(self.view, SIGNAL('updating'), self._show_animation) self.disconnect(self.view, SIGNAL('updated'), self._show_view) self.disconnect(self.view, SIGNAL('EAS_Message'), self.EAS_SendMessage) self.stacked.removeWidget(self.view) self.vbox.removeWidget(self.stacked) self.vbox.removeWidget(self.line) self.view.setParent(None) self.view.hide() self.view.deleteLater() if self.toolspace: self.viewlayout.removeWidget(self.toolspace) self.toolspace.setParent(None) self.toolspace.hide() self.toolspace.deleteLater() if self.view and hasattr(self.view, "uuid"): if self.animation_manager: self.animation_manager.remove(self.view.uuid) self.view = None self.toolspace = None def setView(self, label, update=True): # If there isn't any view for this fragment, use the first available view # of this kind of fragment. if not label: assert frag_types.has_key(self.fragment.type) assert len(frag_types[self.fragment.type].views) > 0 label = frag_types[self.fragment.type].views[0] for button in self.buttons: if label in button.toolTip(): button.visible = False else: if not button.isEnabled(): button.visible = True # assert views_list.has_key(label) self.freeView() # Create the view object. self.view = views_list[label](self.fetcher, self) if label == "histo" or label == 'pie': self.view.is_graphics_view = True if label == 'histo': self.view.chart_type = BARCHART else: self.view.chart_type = PIECHART else: self.view.is_graphics_view = False if label != "error": self.connect(self, SIGNAL("closed"), self.setClosed) if self.animation_manager: if self.view.is_graphics_view: self.connect(self.view, SIGNAL("animation_done(QString)"), self, SIGNAL("animation_done(QString)")) self.animation_manager.addView(self.view) self.connect(self.view, SIGNAL("showButtons"), self.showButtonsSlot) self.connect(self.view, SIGNAL("autoRefresh"), self.updateData) self.view.setCumulative(self.cumulative_mode) self.view.setInterval(self.interval) self.stacked.insertWidget(0, self.view) self._show_view() self.connect(self.view, SIGNAL('open_page'), self._open_page) self.connect(self.view, SIGNAL('add_filter'), self._add_filter) self.connect(self.view, SIGNAL('updating'), self._show_animation) self.connect(self.view, SIGNAL('updated'), self._show_view) self.connect(self.view, SIGNAL('EAS_Message'), self.EAS_SendMessage) # Set some features if there are available on each or each type of widget. if hasattr(self.view, 'setFrameShape'): self.view.setFrameShape(QFrame.NoFrame) self.view.setContextMenuPolicy(Qt.ActionsContextMenu) # All views can ask me to display a toolspace (a widget with all kind of # things in). self.view.title.setText(self.view.getTitle()) self.fragment.view = label self.toolspace = self.view.getToolspace() self.viewlayout.insertWidget(0, self.toolspace) self.vbox.addWidget(self.line) self.vbox.addWidget(self.stacked) # Set the new menu. for action in self.actions(): self.removeAction(action) for view_label, action in self.switch_actions.items(): action.setEnabled(view_label != self.fragment.view) for action in self.action_list: self.addAction(action) self.view.addAction(action) # Add view's eventual menu. view_actions = self.view.getActions() if view_actions: separator = QAction(self) separator.setSeparator(True) view_actions = [separator] + view_actions for action in view_actions: self.view.addAction(action) self.addAction(action) if self.has_menu_pos: self.view.addAction(self.pos_menu.menuAction()) self.addAction(self.pos_menu.menuAction()) if update: self.updateData() def setClosed(self): if self.view: self.view.setClosed() self.destructor() def errorHandler(self, e): """ This method is called when fetcher raises an error. """ # Store error in fragment, and the ErrorFragmentView will able to # load it error = exceptionAsUnicode(e) self.fragment.error = error # We keep last view in the fragment, to prevent setView() method to # put 'error' in the fragment.view string attribute. last_view = self.fragment.view self.setView('error', update=False) # load the error fragment self.fragment.view = last_view def showButtonsSlot(self): for button in self.buttons: if hasattr(button, 'visible'): if button.visible: button.setEnabled(True) else: button.setEnabled(False) def updateData(self): if hasattr(self.fragment, 'error'): self.setView(self.fragment.view, update=False) del self.fragment.error for button in self.buttons: button.setEnabled(False) self.view.requestData() def _open_page(self, *args, **kwargs): self.emit(SIGNAL('open_page'), *args, **kwargs) def _add_filter(self, *args, **kwargs): self.emit(SIGNAL('add_filter'), *args, **kwargs) def EAS_SendMessage(self, *args, **kwargs): self.emit(SIGNAL('EAS_Message'), *args, **kwargs) def _show_animation(self): self.stacked.setCurrentIndex(1) def _show_view(self): self.stacked.setCurrentIndex(0)