class IlastikShell( QMainWindow ): """ The GUI's main window. Simply a standard 'container' GUI for one or more applets. """ def __init__( self, workflow = [], parent = None, flags = QtCore.Qt.WindowFlags(0), sideSplitterSizePolicy=SideSplitterSizePolicy.Manual ): QMainWindow.__init__(self, parent = parent, flags = flags ) # Register for thunk events (easy UI calls from non-GUI threads) self.thunkEventHandler = ThunkEventHandler(self) self._sideSplitterSizePolicy = sideSplitterSizePolicy self.projectManager = ProjectManager() import inspect, os ilastikShellFilePath = os.path.dirname(inspect.getfile(inspect.currentframe())) uic.loadUi( ilastikShellFilePath + "/ui/ilastikShell.ui", self ) self._applets = [] self.appletBarMapping = {} self.setAttribute(Qt.WA_AlwaysShowToolTips) if 'Ubuntu' in platform.platform(): # Native menus are prettier, but aren't working on Ubuntu at this time (Qt 4.7, Ubuntu 11) self.menuBar().setNativeMenuBar(False) (self._projectMenu, self._shellActions) = self._createProjectMenu() self._settingsMenu = self._createSettingsMenu() self.menuBar().addMenu( self._projectMenu ) self.menuBar().addMenu( self._settingsMenu ) self.updateShellProjectDisplay() self.progressDisplayManager = ProgressDisplayManager(self.statusBar) for applet in workflow: self.addApplet(applet) self.appletBar.expanded.connect(self.handleAppleBarItemExpanded) self.appletBar.clicked.connect(self.handleAppletBarClick) self.appletBar.setVerticalScrollMode( QAbstractItemView.ScrollPerPixel ) # By default, make the splitter control expose a reasonable width of the applet bar self.mainSplitter.setSizes([300,1]) self.currentAppletIndex = 0 self.currentImageIndex = -1 self.populatingImageSelectionCombo = False self.imageSelectionCombo.currentIndexChanged.connect( self.changeCurrentInputImageIndex ) self.enableWorkflow = False # Global mask applied to all applets self._controlCmds = [] # Track the control commands that have been issued by each applet so they can be popped. self._disableCounts = [] # Controls for each applet can be disabled by his peers. # No applet can be enabled unless his disableCount == 0 def _createProjectMenu(self): # Create a menu for "General" (non-applet) actions menu = QMenu("&Project", self) shellActions = ShellActions() # Menu item: New Project shellActions.newProjectAction = menu.addAction("&New Project...") shellActions.newProjectAction.setShortcuts( QKeySequence.New ) shellActions.newProjectAction.triggered.connect(self.onNewProjectActionTriggered) # Menu item: Open Project shellActions.openProjectAction = menu.addAction("&Open Project...") shellActions.openProjectAction.setShortcuts( QKeySequence.Open ) shellActions.openProjectAction.triggered.connect(self.onOpenProjectActionTriggered) # Menu item: Save Project shellActions.saveProjectAction = menu.addAction("&Save Project") shellActions.saveProjectAction.setShortcuts( QKeySequence.Save ) shellActions.saveProjectAction.triggered.connect(self.onSaveProjectActionTriggered) # Menu item: Save Project As shellActions.saveProjectAsAction = menu.addAction("&Save Project As...") shellActions.saveProjectAsAction.setShortcuts( QKeySequence.SaveAs ) shellActions.saveProjectAsAction.triggered.connect(self.onSaveProjectAsActionTriggered) # Menu item: Save Project Snapshot shellActions.saveProjectSnapshotAction = menu.addAction("&Take Snapshot...") shellActions.saveProjectSnapshotAction.triggered.connect(self.onSaveProjectSnapshotActionTriggered) # Menu item: Import Project shellActions.importProjectAction = menu.addAction("&Import Project...") shellActions.importProjectAction.triggered.connect(self.onImportProjectActionTriggered) # Menu item: Quit shellActions.quitAction = menu.addAction("&Quit") shellActions.quitAction.setShortcuts( QKeySequence.Quit ) shellActions.quitAction.triggered.connect(self.onQuitActionTriggered) shellActions.quitAction.setShortcut( QKeySequence.Quit ) return (menu, shellActions) def _createSettingsMenu(self): menu = QMenu("&Settings", self) # Menu item: Keyboard Shortcuts def editShortcuts(): mgrDlg = ShortcutManagerDlg(self) shortcutsAction = menu.addAction("&Keyboard Shortcuts") shortcutsAction.triggered.connect(editShortcuts) return menu def show(self): """ Show the window, and enable/disable controls depending on whether or not a project file present. """ super(IlastikShell, self).show() self.enableWorkflow = (self.projectManager.currentProjectFile is not None) self.updateAppletControlStates() self.updateShellProjectDisplay() if self._sideSplitterSizePolicy == SideSplitterSizePolicy.Manual: self.autoSizeSideSplitter( SideSplitterSizePolicy.AutoLargestDrawer ) else: self.autoSizeSideSplitter( SideSplitterSizePolicy.AutoCurrentDrawer ) def updateShellProjectDisplay(self): """ Update the title bar and allowable shell actions based on the state of the currently loaded project. """ windowTitle = "ilastik - " projectPath = self.projectManager.currentProjectPath if projectPath is None: windowTitle += "No Project Loaded" else: windowTitle += projectPath readOnly = self.projectManager.currentProjectIsReadOnly if readOnly: windowTitle += " [Read Only]" self.setWindowTitle(windowTitle) # Enable/Disable menu items projectIsOpen = self.projectManager.currentProjectFile is not None self._shellActions.saveProjectAction.setEnabled(projectIsOpen and not readOnly) # Can't save a read-only project self._shellActions.saveProjectAsAction.setEnabled(projectIsOpen) self._shellActions.saveProjectSnapshotAction.setEnabled(projectIsOpen) def setImageNameListSlot(self, multiSlot): assert multiSlot.level == 1 self.imageNamesSlot = multiSlot def insertImageName( index, slot ): self.imageSelectionCombo.setItemText( index, slot.value ) if self.currentImageIndex == -1: self.changeCurrentInputImageIndex(index) def handleImageNameSlotInsertion(multislot, index): assert multislot == self.imageNamesSlot self.populatingImageSelectionCombo = True self.imageSelectionCombo.insertItem(index, "uninitialized") self.populatingImageSelectionCombo = False multislot[index].notifyDirty( bind( insertImageName, index) ) multiSlot.notifyInserted( bind(handleImageNameSlotInsertion) ) def handleImageNameSlotRemoval(multislot, index): # Simply remove the combo entry, which causes the currentIndexChanged signal to fire if necessary. self.imageSelectionCombo.removeItem(index) if len(multislot) == 0: self.changeCurrentInputImageIndex(-1) multiSlot.notifyRemove( bind(handleImageNameSlotRemoval) ) def changeCurrentInputImageIndex(self, newImageIndex): if newImageIndex != self.currentImageIndex \ and self.populatingImageSelectionCombo == False: if newImageIndex != -1: try: # Accessing the image name value will throw if it isn't properly initialized self.imageNamesSlot[newImageIndex].value except: # Revert to the original image index. if self.currentImageIndex != -1: self.imageSelectionCombo.setCurrentIndex(self.currentImageIndex) return # Alert each central widget and viewer control widget that the image selection changed for i in range( len(self._applets) ): self._applets[i].gui.setImageIndex(newImageIndex) self.currentImageIndex = newImageIndex def handleAppleBarItemExpanded(self, modelIndex): """ The user wants to view a different applet bar item. """ drawerIndex = modelIndex.row() self.setSelectedAppletDrawer(drawerIndex) def setSelectedAppletDrawer(self, drawerIndex): """ Show the correct applet central widget, viewer control widget, and applet drawer widget for this drawer index. """ if self.currentAppletIndex != drawerIndex: self.currentAppletIndex = drawerIndex # Collapse all drawers in the applet bar... self.appletBar.collapseAll() # ...except for the newly selected item. self.appletBar.expand( self.getModelIndexFromDrawerIndex(drawerIndex) ) if len(self.appletBarMapping) != 0: # Determine which applet this drawer belongs to assert drawerIndex in self.appletBarMapping applet_index = self.appletBarMapping[drawerIndex] # Select the appropriate central widget, menu widget, and viewer control widget for this applet self.appletStack.setCurrentIndex(applet_index) self.viewerControlStack.setCurrentIndex(applet_index) self.menuBar().clear() self.menuBar().addMenu(self._projectMenu) self.menuBar().addMenu(self._settingsMenu) for m in self._applets[applet_index].gui.menus(): self.menuBar().addMenu(m) self.autoSizeSideSplitter( self._sideSplitterSizePolicy ) def getModelIndexFromDrawerIndex(self, drawerIndex): drawerTitleItem = self.appletBar.invisibleRootItem().child(drawerIndex) return self.appletBar.indexFromItem(drawerTitleItem) def autoSizeSideSplitter(self, sizePolicy): if sizePolicy == SideSplitterSizePolicy.Manual: # In manual mode, don't resize the splitter at all. return if sizePolicy == SideSplitterSizePolicy.AutoCurrentDrawer: # Get the height of the current applet drawer rootItem = self.appletBar.invisibleRootItem() appletDrawerItem = rootItem.child(self.currentAppletIndex).child(0) appletDrawerWidget = self.appletBar.itemWidget(appletDrawerItem, 0) appletDrawerHeight = appletDrawerWidget.frameSize().height() if sizePolicy == SideSplitterSizePolicy.AutoLargestDrawer: appletDrawerHeight = 0 # Get the height of the largest drawer in the bar for drawerIndex in range( len(self.appletBarMapping) ): rootItem = self.appletBar.invisibleRootItem() appletDrawerItem = rootItem.child(drawerIndex).child(0) appletDrawerWidget = self.appletBar.itemWidget(appletDrawerItem, 0) appletDrawerHeight = max( appletDrawerHeight, appletDrawerWidget.frameSize().height() ) # Get total height of the titles in the applet bar (not the widgets) firstItem = self.appletBar.invisibleRootItem().child(0) titleHeight = self.appletBar.visualItemRect(firstItem).size().height() numDrawers = len(self.appletBarMapping) totalTitleHeight = numDrawers * titleHeight # Auto-size the splitter height based on the height of the applet bar. totalSplitterHeight = sum(self.sideSplitter.sizes()) appletBarHeight = totalTitleHeight + appletDrawerHeight + 10 # Add a small margin so the scroll bar doesn't appear self.sideSplitter.setSizes([appletBarHeight, totalSplitterHeight-appletBarHeight]) def handleAppletBarClick(self, modelIndex): # If the user clicks on a top-level item, automatically expand it. if modelIndex.parent() == self.appletBar.rootIndex(): self.appletBar.expand(modelIndex) else: self.appletBar.setCurrentIndex( modelIndex.parent() ) def addApplet( self, app ): assert isinstance( app, Applet ), "Applets must inherit from Applet base class." assert app.base_initialized, "Applets must call Applet.__init__ upon construction." assert issubclass( type(app.gui), AppletGuiInterface ), "Applet GUIs must conform to the Applet GUI interface." self._applets.append(app) applet_index = len(self._applets) - 1 self.appletStack.addWidget( app.gui.centralWidget() ) # Viewer controls are optional. If the applet didn't provide one, create an empty widget for him. if app.gui.viewerControlWidget() is None: self.viewerControlStack.addWidget( QWidget(parent=self) ) else: self.viewerControlStack.addWidget( app.gui.viewerControlWidget() ) # Add rows to the applet bar model rootItem = self.appletBar.invisibleRootItem() # Add all of the applet bar's items to the toolbox widget for controlName, controlGuiItem in app.gui.appletDrawers(): appletNameItem = QTreeWidgetItem( self.appletBar, QtCore.QStringList( controlName ) ) appletNameItem.setFont( 0, QFont("Ubuntu", 14) ) drawerItem = QTreeWidgetItem(appletNameItem) drawerItem.setSizeHint( 0, controlGuiItem.frameSize() ) # drawerItem.setBackground( 0, QBrush( QColor(224, 224, 224) ) ) # drawerItem.setForeground( 0, QBrush( QColor(0,0,0) ) ) self.appletBar.setItemWidget( drawerItem, 0, controlGuiItem ) # Since each applet can contribute more than one applet bar item, # we need to keep track of which applet this item is associated with self.appletBarMapping[rootItem.childCount()-1] = applet_index # Set up handling of GUI commands from this applet app.guiControlSignal.connect( bind(self.handleAppletGuiControlSignal, applet_index) ) self._disableCounts.append(0) self._controlCmds.append( [] ) # Set up handling of progress updates from this applet self.progressDisplayManager.addApplet(applet_index, app) # Set up handling of shell requests from this applet app.shellRequestSignal.connect( partial(self.handleShellRequest, applet_index) ) self.projectManager.addApplet(app) return applet_index def handleAppletGuiControlSignal(self, applet_index, command=ControlCommand.DisableAll): """ Applets fire a signal when they want other applet GUIs to be disabled. This function handles the signal. Each signal is treated as a command to disable other applets. A special command, Pop, undoes the applet's most recent command (i.e. re-enables the applets that were disabled). If an applet is disabled twice (e.g. by two different applets), then it won't become enabled again until both commands have been popped. """ if command == ControlCommand.Pop: command = self._controlCmds[applet_index].pop() step = -1 # Since we're popping this command, we'll subtract from the disable counts else: step = 1 self._controlCmds[applet_index].append( command ) # Push command onto the stack so we can pop it off when the applet isn't busy any more # Increase the disable count for each applet that is affected by this command. for index, count in enumerate(self._disableCounts): if (command == ControlCommand.DisableAll) \ or (command == ControlCommand.DisableDownstream and index > applet_index) \ or (command == ControlCommand.DisableUpstream and index < applet_index) \ or (command == ControlCommand.DisableSelf and index == applet_index): self._disableCounts[index] += step # Update the control states in the GUI thread self.thunkEventHandler.post( self.updateAppletControlStates ) def handleShellRequest(self, applet_index, requestAction): """ An applet is asking us to do something. Handle the request. """ with Tracer(traceLogger): if requestAction == ShellRequest.RequestSave: # Call the handler directly to ensure this is a synchronous call (not queued to the GUI thread) self.projectManager.saveProject() def __len__( self ): return self.appletBar.count() def __getitem__( self, index ): return self._applets[index] def ensureNoCurrentProject(self, assertClean=False): """ Close the current project. If it's dirty, we ask the user for confirmation. The assertClean parameter is for tests. Setting it to True will raise an assertion if the project was dirty. """ closeProject = True if self.projectManager.isProjectDataDirty(): # Testing assertion assert not assertClean, "Expected a clean project but found it to be dirty!" message = "Your current project is about to be closed, but it has unsaved changes which will be lost.\n" message += "Are you sure you want to proceed?" buttons = QMessageBox.Yes | QMessageBox.Cancel response = QMessageBox.warning(self, "Discard unsaved changes?", message, buttons, defaultButton=QMessageBox.Cancel) closeProject = (response == QMessageBox.Yes) if closeProject: self.closeCurrentProject() return closeProject def closeCurrentProject(self): for applet in self._applets: applet.gui.reset() self.projectManager.closeCurrentProject() self.enableWorkflow = False self.updateAppletControlStates() self.updateShellProjectDisplay() def onNewProjectActionTriggered(self): logger.debug("New Project action triggered") # Make sure the user is finished with the currently open project if not self.ensureNoCurrentProject(): return newProjectFilePath = self.getProjectPathToCreate() if newProjectFilePath is not None: self.createAndLoadNewProject(newProjectFilePath) def createAndLoadNewProject(self, newProjectFilePath): newProjectFile = self.projectManager.createBlankProjectFile(newProjectFilePath) self.loadProject(newProjectFile, newProjectFilePath, False) def getProjectPathToCreate(self, defaultPath=None, caption="Create Ilastik Project"): """ Ask the user where he would like to create a project file. """ if defaultPath is None: defaultPath = os.path.expanduser("~/MyProject.ilp") fileSelected = False while not fileSelected: projectFilePath = QFileDialog.getSaveFileName( self, caption, defaultPath, "Ilastik project files (*.ilp)", options=QFileDialog.Options(QFileDialog.DontUseNativeDialog)) # If the user cancelled, stop now if projectFilePath.isNull(): return None projectFilePath = str(projectFilePath) fileSelected = True # Add extension if necessary fileExtension = os.path.splitext(projectFilePath)[1].lower() if fileExtension != '.ilp': projectFilePath += ".ilp" if os.path.exists(projectFilePath): # Since we changed the file path, we need to re-check if we're overwriting an existing file. message = "A file named '" + projectFilePath + "' already exists in this location.\n" message += "Are you sure you want to overwrite it?" buttons = QMessageBox.Yes | QMessageBox.Cancel response = QMessageBox.warning(self, "Overwrite existing project?", message, buttons, defaultButton=QMessageBox.Cancel) if response == QMessageBox.Cancel: # Try again... fileSelected = False return projectFilePath def onImportProjectActionTriggered(self): """ Import an existing project into a new file. This involves opening the old file, saving it to a new file, and then opening the new file. """ logger.debug("Import Project Action") if not self.ensureNoCurrentProject(): return # Find the directory of the most recently *imported* project mostRecentImportPath = PreferencesManager().get( 'shell', 'recently imported' ) if mostRecentImportPath is not None: defaultDirectory = os.path.split(mostRecentImportPath)[0] else: defaultDirectory = os.path.expanduser('~') # Select the paths to the ilp to import and the name of the new one we'll create importedFilePath = self.getProjectPathToOpen(defaultDirectory) if importedFilePath is not None: PreferencesManager().set('shell', 'recently imported', importedFilePath) defaultFile, ext = os.path.splitext(importedFilePath) defaultFile += "_imported" defaultFile += ext newProjectFilePath = self.getProjectPathToCreate(defaultFile) # If the user didn't cancel if importedFilePath is not None and newProjectFilePath is not None: self.importProject( importedFilePath, newProjectFilePath ) def importProject(self, originalPath, newProjectFilePath): newProjectFile = self.projectManager.createBlankProjectFile(newProjectFilePath) self.projectManager.importProject(originalPath, newProjectFile, newProjectFilePath) self.updateShellProjectDisplay() # Enable all the applet controls self.enableWorkflow = True self.updateAppletControlStates() def getProjectPathToOpen(self, defaultDirectory): """ Return the path of the project the user wants to open (or None if he cancels). """ projectFilePath = QFileDialog.getOpenFileName( self, "Open Ilastik Project", defaultDirectory, "Ilastik project files (*.ilp)", options=QFileDialog.Options(QFileDialog.DontUseNativeDialog)) # If the user canceled, stop now if projectFilePath.isNull(): return None return str(projectFilePath) def onOpenProjectActionTriggered(self): logger.debug("Open Project action triggered") # Make sure the user is finished with the currently open project if not self.ensureNoCurrentProject(): return # Find the directory of the most recently opened project mostRecentProjectPath = PreferencesManager().get( 'shell', 'recently opened' ) if mostRecentProjectPath is not None: defaultDirectory = os.path.split(mostRecentProjectPath)[0] else: defaultDirectory = os.path.expanduser('~') projectFilePath = self.getProjectPathToOpen(defaultDirectory) if projectFilePath is not None: PreferencesManager().set('shell', 'recently opened', projectFilePath) self.openProjectFile(projectFilePath) def openProjectFile(self, projectFilePath): try: hdf5File, readOnly = self.projectManager.openProjectFile(projectFilePath) except ProjectManager.ProjectVersionError,e: QMessageBox.warning(self, "Old Project", "Could not load old project file: " + projectFilePath + ".\nPlease try 'Import Project' instead.") except ProjectManager.FileMissingError: QMessageBox.warning(self, "Missing File", "Could not find project file: " + projectFilePath)
class HeadlessShell(object): """ For now, this class is just a stand-in for the GUI shell (used when running from the command line). """ def __init__(self, workflow_cmdline_args=None): self._workflow_cmdline_args = workflow_cmdline_args or [] self.projectManager = None @property def workflow(self): return self.projectManager.workflow def createAndLoadNewProject(self, newProjectFilePath, workflow_class): hdf5File = ProjectManager.createBlankProjectFile(newProjectFilePath) readOnly = False self.projectManager = ProjectManager( self, workflow_class, headless=True, workflow_cmdline_args=self._workflow_cmdline_args ) self.projectManager._loadProject(hdf5File, newProjectFilePath, readOnly) self.projectManager.saveProject() def openProjectFile(self, projectFilePath, force_readonly=False): # Make sure all workflow sub-classes have been loaded, # so we can detect the workflow type in the project. import ilastik.workflows try: # Open the project file hdf5File, workflow_class, readOnly = ProjectManager.openProjectFile(projectFilePath, force_readonly) # If there are any "creation-time" command-line args saved to the project file, # load them so that the workflow can be instantiated with the same settings # that were used when the project was first created. project_creation_args = [] if "workflow_cmdline_args" in hdf5File.keys(): if len(hdf5File["workflow_cmdline_args"]) > 0: project_creation_args = map(str, hdf5File["workflow_cmdline_args"][...]) if workflow_class is None: # If the project file has no known workflow, we assume pixel classification import ilastik.workflows workflow_class = ilastik.workflows.pixelClassification.PixelClassificationWorkflow import warnings warnings.warn( "Your project file ({}) does not specify a workflow type. " "Assuming Pixel Classification".format( projectFilePath ) ) # Create our project manager # This instantiates the workflow and applies all settings from the project. self.projectManager = ProjectManager( self, workflow_class, headless=True, workflow_cmdline_args=self._workflow_cmdline_args, project_creation_args=project_creation_args ) self.projectManager._loadProject(hdf5File, projectFilePath, readOnly) except ProjectManager.FileMissingError: logger.error("Couldn't find project file: {}".format( projectFilePath )) raise except ProjectManager.ProjectVersionError: # Couldn't open project. Try importing it. oldProjectFilePath = projectFilePath name, ext = os.path.splitext(oldProjectFilePath) # Create a brand new project file. projectFilePath = name + "_imported" + ext logger.info("Importing project as '" + projectFilePath + "'") hdf5File = ProjectManager.createBlankProjectFile(projectFilePath) # For now, we assume that any imported projects are pixel classification workflow projects. import ilastik.workflows default_workflow = ilastik.workflows.pixelClassification.PixelClassificationWorkflow # Create the project manager. # Here, we provide an additional parameter: the path of the project we're importing from. self.projectManager = ProjectManager( self, default_workflow, importFromPath=oldProjectFilePath, headless=True, workflow_cmdline_args=self._workflow_cmdline_args, project_creation_args=self._workflow_cmdline_args ) self.projectManager._importProject(oldProjectFilePath, hdf5File, projectFilePath,readOnly = False) def setAppletEnabled(self, applet, enabled): """ Provided here to satisfy the ShellABC. For now, HeadlessShell has no concept of "enabled" or "disabled" applets. """ pass def isAppletEnabled(self, applet): return False def enableProjectChanges(self, enabled): """ Provided here to satisfy the ShellABC. For now, HeadlessShell has no mechanism for closing projects. """ pass def closeCurrentProject(self): self.projectManager._closeCurrentProject() self.projectManager.cleanUp() self.projectManager = None
class HeadlessShell(object): """ For now, this class is just a stand-in for the GUI shell (used when running from the command line). """ def __init__(self, workflow_cmdline_args=None): self._workflow_cmdline_args = workflow_cmdline_args or [] self.projectManager = None @property def workflow(self): if self.projectManager is not None: return self.projectManager.workflow @property def currentImageIndex(self): return -1 def createAndLoadNewProject(self, newProjectFilePath, workflow_class): hdf5File = ProjectManager.createBlankProjectFile(newProjectFilePath) readOnly = False self.projectManager = ProjectManager( self, workflow_class, headless=True, workflow_cmdline_args=self._workflow_cmdline_args ) self.projectManager._loadProject(hdf5File, newProjectFilePath, readOnly) self.projectManager.saveProject() @classmethod def downloadProjectFromDvid(cls, dvid_key_url): dvid_key_url = str(dvid_key_url) # By convention, command-line users specify the location of the project # keyvalue data using the same format that the DVID API itself uses. url_format = "^protocol://hostname/api/node/uuid/kv_instance_name(/key/keyname)?" for field in ["protocol", "hostname", "uuid", "kv_instance_name", "keyname"]: url_format = url_format.replace(field, "(?P<" + field + ">[^?/]+)") match = re.match(url_format, dvid_key_url) if not match: # DVID is the only url-based format we support right now. # So if it looks like the user gave a URL that isn't a valid DVID node, then error. raise RuntimeError("Invalid URL format for DVID key-value data: {}".format(projectFilePath)) fields = match.groupdict() projectFilePath = ProjectManager.downloadProjectFromDvid( fields["hostname"], fields["uuid"], fields["kv_instance_name"], fields["keyname"] ) return projectFilePath def openProjectFile(self, projectFilePath, force_readonly=False): # If the user gave a URL to a DVID key, then download the project file from dvid first. # (So far, DVID is the only type of URL access we support for project files.) if isUrl(projectFilePath): projectFilePath = HeadlessShell.downloadProjectFromDvid(projectFilePath) # Make sure all workflow sub-classes have been loaded, # so we can detect the workflow type in the project. import ilastik.workflows try: # Open the project file hdf5File, workflow_class, readOnly = ProjectManager.openProjectFile(projectFilePath, force_readonly) # If there are any "creation-time" command-line args saved to the project file, # load them so that the workflow can be instantiated with the same settings # that were used when the project was first created. project_creation_args = [] if "workflow_cmdline_args" in list(hdf5File.keys()): if len(hdf5File["workflow_cmdline_args"]) > 0: project_creation_args = list(map(str, hdf5File["workflow_cmdline_args"][...])) if workflow_class is None: # If the project file has no known workflow, we assume pixel classification import ilastik.workflows workflow_class = ilastik.workflows.pixelClassification.PixelClassificationWorkflow import warnings warnings.warn( "Your project file ({}) does not specify a workflow type. " "Assuming Pixel Classification".format(projectFilePath) ) # Create our project manager # This instantiates the workflow and applies all settings from the project. self.projectManager = ProjectManager( self, workflow_class, headless=True, workflow_cmdline_args=self._workflow_cmdline_args, project_creation_args=project_creation_args, ) self.projectManager._loadProject(hdf5File, projectFilePath, readOnly) except ProjectManager.FileMissingError: logger.error("Couldn't find project file: {}".format(projectFilePath)) raise except ProjectManager.ProjectVersionError: # Couldn't open project. Try importing it. oldProjectFilePath = projectFilePath name, ext = os.path.splitext(oldProjectFilePath) # Create a brand new project file. projectFilePath = name + "_imported" + ext logger.info("Importing project as '" + projectFilePath + "'") hdf5File = ProjectManager.createBlankProjectFile(projectFilePath) # For now, we assume that any imported projects are pixel classification workflow projects. import ilastik.workflows default_workflow = ilastik.workflows.pixelClassification.PixelClassificationWorkflow # Create the project manager. self.projectManager = ProjectManager( self, default_workflow, headless=True, workflow_cmdline_args=self._workflow_cmdline_args, project_creation_args=self._workflow_cmdline_args, ) self.projectManager._importProject(oldProjectFilePath, hdf5File, projectFilePath) def setAppletEnabled(self, applet, enabled): """ Provided here to satisfy the ShellABC. For now, HeadlessShell has no concept of "enabled" or "disabled" applets. """ pass def isAppletEnabled(self, applet): return False def enableProjectChanges(self, enabled): """ Provided here to satisfy the ShellABC. For now, HeadlessShell has no mechanism for closing projects. """ pass def closeCurrentProject(self): if self.projectManager is not None: self.projectManager._closeCurrentProject() self.projectManager.cleanUp() self.projectManager = None
class HeadlessShell(object): """ For now, this class is just a stand-in for the GUI shell (used when running from the command line). """ def __init__(self, workflow_cmdline_args=None): self._workflow_cmdline_args = workflow_cmdline_args or [] self.projectManager = None @property def workflow(self): return self.projectManager.workflow @property def currentImageIndex(self): return -1 def createAndLoadNewProject(self, newProjectFilePath, workflow_class): hdf5File = ProjectManager.createBlankProjectFile(newProjectFilePath) readOnly = False self.projectManager = ProjectManager( self, workflow_class, headless=True, workflow_cmdline_args=self._workflow_cmdline_args ) self.projectManager._loadProject(hdf5File, newProjectFilePath, readOnly) self.projectManager.saveProject() @classmethod def downloadProjectFromDvid(cls, dvid_key_url): dvid_key_url = str(dvid_key_url) # By convention, command-line users specify the location of the project # keyvalue data using the same format that the DVID API itself uses. url_format = "^protocol://hostname/api/node/uuid/kv_instance_name(/key/keyname)?" for field in ['protocol', 'hostname', 'uuid', 'kv_instance_name', 'keyname']: url_format = url_format.replace( field, '(?P<' + field + '>[^?/]+)' ) match = re.match( url_format, dvid_key_url ) if not match: # DVID is the only url-based format we support right now. # So if it looks like the user gave a URL that isn't a valid DVID node, then error. raise RuntimeError("Invalid URL format for DVID key-value data: {}".format(projectFilePath)) fields = match.groupdict() projectFilePath = ProjectManager.downloadProjectFromDvid( fields['hostname'], fields['uuid'], fields['kv_instance_name'], fields['keyname'] ) return projectFilePath def openProjectFile(self, projectFilePath, force_readonly=False): # If the user gave a URL to a DVID key, then download the project file from dvid first. # (So far, DVID is the only type of URL access we support for project files.) if isUrl(projectFilePath): projectFilePath = HeadlessShell.downloadProjectFromDvid(projectFilePath) # Make sure all workflow sub-classes have been loaded, # so we can detect the workflow type in the project. import ilastik.workflows try: # Open the project file hdf5File, workflow_class, readOnly = ProjectManager.openProjectFile(projectFilePath, force_readonly) # If there are any "creation-time" command-line args saved to the project file, # load them so that the workflow can be instantiated with the same settings # that were used when the project was first created. project_creation_args = [] if "workflow_cmdline_args" in hdf5File.keys(): if len(hdf5File["workflow_cmdline_args"]) > 0: project_creation_args = map(str, hdf5File["workflow_cmdline_args"][...]) if workflow_class is None: # If the project file has no known workflow, we assume pixel classification import ilastik.workflows workflow_class = ilastik.workflows.pixelClassification.PixelClassificationWorkflow import warnings warnings.warn( "Your project file ({}) does not specify a workflow type. " "Assuming Pixel Classification".format( projectFilePath ) ) # Create our project manager # This instantiates the workflow and applies all settings from the project. self.projectManager = ProjectManager( self, workflow_class, headless=True, workflow_cmdline_args=self._workflow_cmdline_args, project_creation_args=project_creation_args ) self.projectManager._loadProject(hdf5File, projectFilePath, readOnly) except ProjectManager.FileMissingError: logger.error("Couldn't find project file: {}".format( projectFilePath )) raise except ProjectManager.ProjectVersionError: # Couldn't open project. Try importing it. oldProjectFilePath = projectFilePath name, ext = os.path.splitext(oldProjectFilePath) # Create a brand new project file. projectFilePath = name + "_imported" + ext logger.info("Importing project as '" + projectFilePath + "'") hdf5File = ProjectManager.createBlankProjectFile(projectFilePath) # For now, we assume that any imported projects are pixel classification workflow projects. import ilastik.workflows default_workflow = ilastik.workflows.pixelClassification.PixelClassificationWorkflow # Create the project manager. self.projectManager = ProjectManager( self, default_workflow, headless=True, workflow_cmdline_args=self._workflow_cmdline_args, project_creation_args=self._workflow_cmdline_args ) self.projectManager._importProject(importFromPath, hdf5File, projectFilePath) def setAppletEnabled(self, applet, enabled): """ Provided here to satisfy the ShellABC. For now, HeadlessShell has no concept of "enabled" or "disabled" applets. """ pass def isAppletEnabled(self, applet): return False def enableProjectChanges(self, enabled): """ Provided here to satisfy the ShellABC. For now, HeadlessShell has no mechanism for closing projects. """ pass def closeCurrentProject(self): self.projectManager._closeCurrentProject() self.projectManager.cleanUp() self.projectManager = None
class IlastikShell(QMainWindow): """ The GUI's main window. Simply a standard 'container' GUI for one or more applets. """ def __init__(self, workflow=[], parent=None, flags=QtCore.Qt.WindowFlags(0), sideSplitterSizePolicy=SideSplitterSizePolicy.Manual): QMainWindow.__init__(self, parent=parent, flags=flags) # Register for thunk events (easy UI calls from non-GUI threads) self.thunkEventHandler = ThunkEventHandler(self) self._sideSplitterSizePolicy = sideSplitterSizePolicy self.projectManager = ProjectManager() import inspect, os ilastikShellFilePath = os.path.dirname( inspect.getfile(inspect.currentframe())) uic.loadUi(ilastikShellFilePath + "/ui/ilastikShell.ui", self) self._applets = [] self.appletBarMapping = {} self.setAttribute(Qt.WA_AlwaysShowToolTips) if 'Ubuntu' in platform.platform(): # Native menus are prettier, but aren't working on Ubuntu at this time (Qt 4.7, Ubuntu 11) self.menuBar().setNativeMenuBar(False) (self._projectMenu, self._shellActions) = self._createProjectMenu() self._settingsMenu = self._createSettingsMenu() self.menuBar().addMenu(self._projectMenu) self.menuBar().addMenu(self._settingsMenu) self.updateShellProjectDisplay() self.progressDisplayManager = ProgressDisplayManager(self.statusBar) for applet in workflow: self.addApplet(applet) self.appletBar.expanded.connect(self.handleAppleBarItemExpanded) self.appletBar.clicked.connect(self.handleAppletBarClick) self.appletBar.setVerticalScrollMode(QAbstractItemView.ScrollPerPixel) # By default, make the splitter control expose a reasonable width of the applet bar self.mainSplitter.setSizes([300, 1]) self.currentAppletIndex = 0 self.currentImageIndex = -1 self.populatingImageSelectionCombo = False self.imageSelectionCombo.currentIndexChanged.connect( self.changeCurrentInputImageIndex) self.enableWorkflow = False # Global mask applied to all applets self._controlCmds = [ ] # Track the control commands that have been issued by each applet so they can be popped. self._disableCounts = [ ] # Controls for each applet can be disabled by his peers. # No applet can be enabled unless his disableCount == 0 def _createProjectMenu(self): # Create a menu for "General" (non-applet) actions menu = QMenu("&Project", self) shellActions = ShellActions() # Menu item: New Project shellActions.newProjectAction = menu.addAction("&New Project...") shellActions.newProjectAction.setShortcuts(QKeySequence.New) shellActions.newProjectAction.triggered.connect( self.onNewProjectActionTriggered) # Menu item: Open Project shellActions.openProjectAction = menu.addAction("&Open Project...") shellActions.openProjectAction.setShortcuts(QKeySequence.Open) shellActions.openProjectAction.triggered.connect( self.onOpenProjectActionTriggered) # Menu item: Save Project shellActions.saveProjectAction = menu.addAction("&Save Project") shellActions.saveProjectAction.setShortcuts(QKeySequence.Save) shellActions.saveProjectAction.triggered.connect( self.onSaveProjectActionTriggered) # Menu item: Save Project As shellActions.saveProjectAsAction = menu.addAction( "&Save Project As...") shellActions.saveProjectAsAction.setShortcuts(QKeySequence.SaveAs) shellActions.saveProjectAsAction.triggered.connect( self.onSaveProjectAsActionTriggered) # Menu item: Save Project Snapshot shellActions.saveProjectSnapshotAction = menu.addAction( "&Take Snapshot...") shellActions.saveProjectSnapshotAction.triggered.connect( self.onSaveProjectSnapshotActionTriggered) # Menu item: Import Project shellActions.importProjectAction = menu.addAction("&Import Project...") shellActions.importProjectAction.triggered.connect( self.onImportProjectActionTriggered) # Menu item: Quit shellActions.quitAction = menu.addAction("&Quit") shellActions.quitAction.setShortcuts(QKeySequence.Quit) shellActions.quitAction.triggered.connect(self.onQuitActionTriggered) shellActions.quitAction.setShortcut(QKeySequence.Quit) return (menu, shellActions) def _createSettingsMenu(self): menu = QMenu("&Settings", self) # Menu item: Keyboard Shortcuts def editShortcuts(): mgrDlg = ShortcutManagerDlg(self) shortcutsAction = menu.addAction("&Keyboard Shortcuts") shortcutsAction.triggered.connect(editShortcuts) return menu def show(self): """ Show the window, and enable/disable controls depending on whether or not a project file present. """ super(IlastikShell, self).show() self.enableWorkflow = (self.projectManager.currentProjectFile is not None) self.updateAppletControlStates() self.updateShellProjectDisplay() if self._sideSplitterSizePolicy == SideSplitterSizePolicy.Manual: self.autoSizeSideSplitter(SideSplitterSizePolicy.AutoLargestDrawer) else: self.autoSizeSideSplitter(SideSplitterSizePolicy.AutoCurrentDrawer) def updateShellProjectDisplay(self): """ Update the title bar and allowable shell actions based on the state of the currently loaded project. """ windowTitle = "ilastik - " projectPath = self.projectManager.currentProjectPath if projectPath is None: windowTitle += "No Project Loaded" else: windowTitle += projectPath readOnly = self.projectManager.currentProjectIsReadOnly if readOnly: windowTitle += " [Read Only]" self.setWindowTitle(windowTitle) # Enable/Disable menu items projectIsOpen = self.projectManager.currentProjectFile is not None self._shellActions.saveProjectAction.setEnabled( projectIsOpen and not readOnly) # Can't save a read-only project self._shellActions.saveProjectAsAction.setEnabled(projectIsOpen) self._shellActions.saveProjectSnapshotAction.setEnabled(projectIsOpen) def setImageNameListSlot(self, multiSlot): assert multiSlot.level == 1 self.imageNamesSlot = multiSlot def insertImageName(index, slot): self.imageSelectionCombo.setItemText(index, slot.value) if self.currentImageIndex == -1: self.changeCurrentInputImageIndex(index) def handleImageNameSlotInsertion(multislot, index): assert multislot == self.imageNamesSlot self.populatingImageSelectionCombo = True self.imageSelectionCombo.insertItem(index, "uninitialized") self.populatingImageSelectionCombo = False multislot[index].notifyDirty(bind(insertImageName, index)) multiSlot.notifyInserted(bind(handleImageNameSlotInsertion)) def handleImageNameSlotRemoval(multislot, index): # Simply remove the combo entry, which causes the currentIndexChanged signal to fire if necessary. self.imageSelectionCombo.removeItem(index) if len(multislot) == 0: self.changeCurrentInputImageIndex(-1) multiSlot.notifyRemove(bind(handleImageNameSlotRemoval)) def changeCurrentInputImageIndex(self, newImageIndex): if newImageIndex != self.currentImageIndex \ and self.populatingImageSelectionCombo == False: if newImageIndex != -1: try: # Accessing the image name value will throw if it isn't properly initialized self.imageNamesSlot[newImageIndex].value except: # Revert to the original image index. if self.currentImageIndex != -1: self.imageSelectionCombo.setCurrentIndex( self.currentImageIndex) return # Alert each central widget and viewer control widget that the image selection changed for i in range(len(self._applets)): self._applets[i].gui.setImageIndex(newImageIndex) self.currentImageIndex = newImageIndex def handleAppleBarItemExpanded(self, modelIndex): """ The user wants to view a different applet bar item. """ drawerIndex = modelIndex.row() self.setSelectedAppletDrawer(drawerIndex) def setSelectedAppletDrawer(self, drawerIndex): """ Show the correct applet central widget, viewer control widget, and applet drawer widget for this drawer index. """ if self.currentAppletIndex != drawerIndex: self.currentAppletIndex = drawerIndex # Collapse all drawers in the applet bar... self.appletBar.collapseAll() # ...except for the newly selected item. self.appletBar.expand( self.getModelIndexFromDrawerIndex(drawerIndex)) if len(self.appletBarMapping) != 0: # Determine which applet this drawer belongs to assert drawerIndex in self.appletBarMapping applet_index = self.appletBarMapping[drawerIndex] # Select the appropriate central widget, menu widget, and viewer control widget for this applet self.appletStack.setCurrentIndex(applet_index) self.viewerControlStack.setCurrentIndex(applet_index) self.menuBar().clear() self.menuBar().addMenu(self._projectMenu) self.menuBar().addMenu(self._settingsMenu) for m in self._applets[applet_index].gui.menus(): self.menuBar().addMenu(m) self.autoSizeSideSplitter(self._sideSplitterSizePolicy) def getModelIndexFromDrawerIndex(self, drawerIndex): drawerTitleItem = self.appletBar.invisibleRootItem().child(drawerIndex) return self.appletBar.indexFromItem(drawerTitleItem) def autoSizeSideSplitter(self, sizePolicy): if sizePolicy == SideSplitterSizePolicy.Manual: # In manual mode, don't resize the splitter at all. return if sizePolicy == SideSplitterSizePolicy.AutoCurrentDrawer: # Get the height of the current applet drawer rootItem = self.appletBar.invisibleRootItem() appletDrawerItem = rootItem.child(self.currentAppletIndex).child(0) appletDrawerWidget = self.appletBar.itemWidget(appletDrawerItem, 0) appletDrawerHeight = appletDrawerWidget.frameSize().height() if sizePolicy == SideSplitterSizePolicy.AutoLargestDrawer: appletDrawerHeight = 0 # Get the height of the largest drawer in the bar for drawerIndex in range(len(self.appletBarMapping)): rootItem = self.appletBar.invisibleRootItem() appletDrawerItem = rootItem.child(drawerIndex).child(0) appletDrawerWidget = self.appletBar.itemWidget( appletDrawerItem, 0) appletDrawerHeight = max( appletDrawerHeight, appletDrawerWidget.frameSize().height()) # Get total height of the titles in the applet bar (not the widgets) firstItem = self.appletBar.invisibleRootItem().child(0) titleHeight = self.appletBar.visualItemRect(firstItem).size().height() numDrawers = len(self.appletBarMapping) totalTitleHeight = numDrawers * titleHeight # Auto-size the splitter height based on the height of the applet bar. totalSplitterHeight = sum(self.sideSplitter.sizes()) appletBarHeight = totalTitleHeight + appletDrawerHeight + 10 # Add a small margin so the scroll bar doesn't appear self.sideSplitter.setSizes( [appletBarHeight, totalSplitterHeight - appletBarHeight]) def handleAppletBarClick(self, modelIndex): # If the user clicks on a top-level item, automatically expand it. if modelIndex.parent() == self.appletBar.rootIndex(): self.appletBar.expand(modelIndex) else: self.appletBar.setCurrentIndex(modelIndex.parent()) def addApplet(self, app): assert isinstance( app, Applet), "Applets must inherit from Applet base class." assert app.base_initialized, "Applets must call Applet.__init__ upon construction." assert issubclass( type(app.gui), AppletGuiInterface ), "Applet GUIs must conform to the Applet GUI interface." self._applets.append(app) applet_index = len(self._applets) - 1 self.appletStack.addWidget(app.gui.centralWidget()) # Viewer controls are optional. If the applet didn't provide one, create an empty widget for him. if app.gui.viewerControlWidget() is None: self.viewerControlStack.addWidget(QWidget(parent=self)) else: self.viewerControlStack.addWidget(app.gui.viewerControlWidget()) # Add rows to the applet bar model rootItem = self.appletBar.invisibleRootItem() # Add all of the applet bar's items to the toolbox widget for controlName, controlGuiItem in app.gui.appletDrawers(): appletNameItem = QTreeWidgetItem(self.appletBar, QtCore.QStringList(controlName)) appletNameItem.setFont(0, QFont("Ubuntu", 14)) drawerItem = QTreeWidgetItem(appletNameItem) drawerItem.setSizeHint(0, controlGuiItem.frameSize()) # drawerItem.setBackground( 0, QBrush( QColor(224, 224, 224) ) ) # drawerItem.setForeground( 0, QBrush( QColor(0,0,0) ) ) self.appletBar.setItemWidget(drawerItem, 0, controlGuiItem) # Since each applet can contribute more than one applet bar item, # we need to keep track of which applet this item is associated with self.appletBarMapping[rootItem.childCount() - 1] = applet_index # Set up handling of GUI commands from this applet app.guiControlSignal.connect( bind(self.handleAppletGuiControlSignal, applet_index)) self._disableCounts.append(0) self._controlCmds.append([]) # Set up handling of progress updates from this applet self.progressDisplayManager.addApplet(applet_index, app) # Set up handling of shell requests from this applet app.shellRequestSignal.connect( partial(self.handleShellRequest, applet_index)) self.projectManager.addApplet(app) return applet_index def handleAppletGuiControlSignal(self, applet_index, command=ControlCommand.DisableAll): """ Applets fire a signal when they want other applet GUIs to be disabled. This function handles the signal. Each signal is treated as a command to disable other applets. A special command, Pop, undoes the applet's most recent command (i.e. re-enables the applets that were disabled). If an applet is disabled twice (e.g. by two different applets), then it won't become enabled again until both commands have been popped. """ if command == ControlCommand.Pop: command = self._controlCmds[applet_index].pop() step = -1 # Since we're popping this command, we'll subtract from the disable counts else: step = 1 self._controlCmds[applet_index].append( command ) # Push command onto the stack so we can pop it off when the applet isn't busy any more # Increase the disable count for each applet that is affected by this command. for index, count in enumerate(self._disableCounts): if (command == ControlCommand.DisableAll) \ or (command == ControlCommand.DisableDownstream and index > applet_index) \ or (command == ControlCommand.DisableUpstream and index < applet_index) \ or (command == ControlCommand.DisableSelf and index == applet_index): self._disableCounts[index] += step # Update the control states in the GUI thread self.thunkEventHandler.post(self.updateAppletControlStates) def handleShellRequest(self, applet_index, requestAction): """ An applet is asking us to do something. Handle the request. """ with Tracer(traceLogger): if requestAction == ShellRequest.RequestSave: # Call the handler directly to ensure this is a synchronous call (not queued to the GUI thread) self.projectManager.saveProject() def __len__(self): return self.appletBar.count() def __getitem__(self, index): return self._applets[index] def ensureNoCurrentProject(self, assertClean=False): """ Close the current project. If it's dirty, we ask the user for confirmation. The assertClean parameter is for tests. Setting it to True will raise an assertion if the project was dirty. """ closeProject = True if self.projectManager.isProjectDataDirty(): # Testing assertion assert not assertClean, "Expected a clean project but found it to be dirty!" message = "Your current project is about to be closed, but it has unsaved changes which will be lost.\n" message += "Are you sure you want to proceed?" buttons = QMessageBox.Yes | QMessageBox.Cancel response = QMessageBox.warning(self, "Discard unsaved changes?", message, buttons, defaultButton=QMessageBox.Cancel) closeProject = (response == QMessageBox.Yes) if closeProject: self.closeCurrentProject() return closeProject def closeCurrentProject(self): for applet in self._applets: applet.gui.reset() self.projectManager.closeCurrentProject() self.enableWorkflow = False self.updateAppletControlStates() self.updateShellProjectDisplay() def onNewProjectActionTriggered(self): logger.debug("New Project action triggered") # Make sure the user is finished with the currently open project if not self.ensureNoCurrentProject(): return newProjectFilePath = self.getProjectPathToCreate() if newProjectFilePath is not None: self.createAndLoadNewProject(newProjectFilePath) def createAndLoadNewProject(self, newProjectFilePath): newProjectFile = self.projectManager.createBlankProjectFile( newProjectFilePath) self.loadProject(newProjectFile, newProjectFilePath, False) def getProjectPathToCreate(self, defaultPath=None, caption="Create Ilastik Project"): """ Ask the user where he would like to create a project file. """ if defaultPath is None: defaultPath = os.path.expanduser("~/MyProject.ilp") fileSelected = False while not fileSelected: projectFilePath = QFileDialog.getSaveFileName( self, caption, defaultPath, "Ilastik project files (*.ilp)", options=QFileDialog.Options(QFileDialog.DontUseNativeDialog)) # If the user cancelled, stop now if projectFilePath.isNull(): return None projectFilePath = str(projectFilePath) fileSelected = True # Add extension if necessary fileExtension = os.path.splitext(projectFilePath)[1].lower() if fileExtension != '.ilp': projectFilePath += ".ilp" if os.path.exists(projectFilePath): # Since we changed the file path, we need to re-check if we're overwriting an existing file. message = "A file named '" + projectFilePath + "' already exists in this location.\n" message += "Are you sure you want to overwrite it?" buttons = QMessageBox.Yes | QMessageBox.Cancel response = QMessageBox.warning( self, "Overwrite existing project?", message, buttons, defaultButton=QMessageBox.Cancel) if response == QMessageBox.Cancel: # Try again... fileSelected = False return projectFilePath def onImportProjectActionTriggered(self): """ Import an existing project into a new file. This involves opening the old file, saving it to a new file, and then opening the new file. """ logger.debug("Import Project Action") if not self.ensureNoCurrentProject(): return # Find the directory of the most recently *imported* project mostRecentImportPath = PreferencesManager().get( 'shell', 'recently imported') if mostRecentImportPath is not None: defaultDirectory = os.path.split(mostRecentImportPath)[0] else: defaultDirectory = os.path.expanduser('~') # Select the paths to the ilp to import and the name of the new one we'll create importedFilePath = self.getProjectPathToOpen(defaultDirectory) if importedFilePath is not None: PreferencesManager().set('shell', 'recently imported', importedFilePath) defaultFile, ext = os.path.splitext(importedFilePath) defaultFile += "_imported" defaultFile += ext newProjectFilePath = self.getProjectPathToCreate(defaultFile) # If the user didn't cancel if importedFilePath is not None and newProjectFilePath is not None: self.importProject(importedFilePath, newProjectFilePath) def importProject(self, originalPath, newProjectFilePath): newProjectFile = self.projectManager.createBlankProjectFile( newProjectFilePath) self.projectManager.importProject(originalPath, newProjectFile, newProjectFilePath) self.updateShellProjectDisplay() # Enable all the applet controls self.enableWorkflow = True self.updateAppletControlStates() def getProjectPathToOpen(self, defaultDirectory): """ Return the path of the project the user wants to open (or None if he cancels). """ projectFilePath = QFileDialog.getOpenFileName( self, "Open Ilastik Project", defaultDirectory, "Ilastik project files (*.ilp)", options=QFileDialog.Options(QFileDialog.DontUseNativeDialog)) # If the user canceled, stop now if projectFilePath.isNull(): return None return str(projectFilePath) def onOpenProjectActionTriggered(self): logger.debug("Open Project action triggered") # Make sure the user is finished with the currently open project if not self.ensureNoCurrentProject(): return # Find the directory of the most recently opened project mostRecentProjectPath = PreferencesManager().get( 'shell', 'recently opened') if mostRecentProjectPath is not None: defaultDirectory = os.path.split(mostRecentProjectPath)[0] else: defaultDirectory = os.path.expanduser('~') projectFilePath = self.getProjectPathToOpen(defaultDirectory) if projectFilePath is not None: PreferencesManager().set('shell', 'recently opened', projectFilePath) self.openProjectFile(projectFilePath) def openProjectFile(self, projectFilePath): try: hdf5File, readOnly = self.projectManager.openProjectFile( projectFilePath) except ProjectManager.ProjectVersionError, e: QMessageBox.warning( self, "Old Project", "Could not load old project file: " + projectFilePath + ".\nPlease try 'Import Project' instead.") except ProjectManager.FileMissingError: QMessageBox.warning( self, "Missing File", "Could not find project file: " + projectFilePath)
class HeadlessShell(object): """ For now, this class is just a stand-in for the GUI shell (used when running from the command line). """ def __init__(self, workflow_cmdline_args=None): self._workflow_cmdline_args = workflow_cmdline_args or [] self.projectManager = None @property def workflow(self): return self.projectManager.workflow def createAndLoadNewProject(self, newProjectFilePath, workflow_class): hdf5File = ProjectManager.createBlankProjectFile(newProjectFilePath) readOnly = False self.projectManager = ProjectManager( self, workflow_class, headless=True, workflow_cmdline_args=self._workflow_cmdline_args ) self.projectManager._loadProject(hdf5File, newProjectFilePath, readOnly) self.projectManager.saveProject() def openProjectFile(self, projectFilePath): # Make sure all workflow sub-classes have been loaded, # so we can detect the workflow type in the project. import ilastik.workflows try: # Open the project file hdf5File, workflow_class, _ = ProjectManager.openProjectFile(projectFilePath) # If there are any "creation-time" command-line args saved to the project file, # load them so that the workflow can be instantiated with the same settings # that were used when the project was first created. project_creation_args = [] if "workflow_cmdline_args" in hdf5File.keys(): if len(hdf5File["workflow_cmdline_args"]) > 0: project_creation_args = map(str, hdf5File["workflow_cmdline_args"][...]) if workflow_class is None: # If the project file has no known workflow, we assume pixel classification import ilastik.workflows workflow_class = ilastik.workflows.pixelClassification.PixelClassificationWorkflow import warnings warnings.warn( "Your project file ({}) does not specify a workflow type. " "Assuming Pixel Classification".format( projectFilePath ) ) # Create our project manager # This instantiates the workflow and applies all settings from the project. self.projectManager = ProjectManager( self, workflow_class, headless=True, workflow_cmdline_args=self._workflow_cmdline_args, project_creation_args=project_creation_args ) self.projectManager._loadProject(hdf5File, projectFilePath, readOnly = False) except ProjectManager.FileMissingError: logger.error("Couldn't find project file: {}".format( projectFilePath )) raise except ProjectManager.ProjectVersionError: # Couldn't open project. Try importing it. oldProjectFilePath = projectFilePath name, ext = os.path.splitext(oldProjectFilePath) # Create a brand new project file. projectFilePath = name + "_imported" + ext logger.info("Importing project as '" + projectFilePath + "'") hdf5File = ProjectManager.createBlankProjectFile(projectFilePath) # For now, we assume that any imported projects are pixel classification workflow projects. import ilastik.workflows default_workflow = ilastik.workflows.pixelClassification.PixelClassificationWorkflow # Create the project manager. # Here, we provide an additional parameter: the path of the project we're importing from. self.projectManager = ProjectManager( self, default_workflow, importFromPath=oldProjectFilePath, headless=True, workflow_cmdline_args=self._workflow_cmdline_args, project_creation_args=self._workflow_cmdline_args ) self.projectManager._importProject(oldProjectFilePath, hdf5File, projectFilePath,readOnly = False) def setAppletEnabled(self, applet, enabled): """ Provided here to satisfy the ShellABC. For now, HeadlessShell has no concept of "enabled" or "disabled" applets. """ pass def isAppletEnabled(self, applet): return False def enableProjectChanges(self, enabled): """ Provided here to satisfy the ShellABC. For now, HeadlessShell has no mechanism for closing projects. """ pass def closeCurrentProject(self): self.projectManager._closeCurrentProject() self.projectManager.cleanUp() self.projectManager = None