Slicer 4.2
Slicer is a multi-platform, free and open source software package for visualization and medical image computing
EditBox.py
Go to the documentation of this file.
00001 import os
00002 from __main__ import qt
00003 from EditOptions import *
00004 import EditUtil
00005 import EditorLib
00006 
00007 #########################################################
00008 #
00009 #
00010 comment = """
00011 
00012   EditBox is a wrapper around a set of Qt widgets and other
00013   structures to manage the slicer4 edit box.
00014 
00015 # TODO :
00016 """
00017 #
00018 #########################################################
00019 
00020 #
00021 # The parent class definition
00022 #
00023 
00024 class EditBox(object):
00025 
00026   def __init__(self, parent=None, optionsFrame=None):
00027     self.effects = []
00028     self.effectButtons = {}
00029     self.effectCursors = {}
00030     self.effectMapper = qt.QSignalMapper()
00031     self.effectMapper.connect('mapped(const QString&)', self.selectEffect)
00032     self.editUtil = EditUtil.EditUtil()
00033     self.undoRedo = EditUtil.UndoRedo()
00034     self.undoRedo.stateChangedCallback = self.updateUndoRedoButtons
00035     self.toggleShortcut = None
00036 
00037     # check for extensions - if none have been registered, just create the empty dictionary
00038     try:
00039       slicer.modules.editorExtensions
00040     except AttributeError:
00041       slicer.modules.editorExtensions = {}
00042 
00043     # register the builtin extensions
00044     self.editorBuiltins = {}
00045     self.editorBuiltins["PaintEffect"] = EditorLib.PaintEffect
00046     self.editorBuiltins["DrawEffect"] = EditorLib.DrawEffect
00047     self.editorBuiltins["ThresholdEffect"] = EditorLib.ThresholdEffect
00048     self.editorBuiltins["RectangleEffect"] = EditorLib.RectangleEffect
00049     self.editorBuiltins["LevelTracingEffect"] = EditorLib.LevelTracingEffect
00050     self.editorBuiltins["MakeModelEffect"] = EditorLib.MakeModelEffect
00051     self.editorBuiltins["ErodeEffect"] = EditorLib.ErodeEffect
00052     self.editorBuiltins["DilateEffect"] = EditorLib.DilateEffect
00053     self.editorBuiltins["ChangeLabelEffect"] = EditorLib.ChangeLabelEffect
00054     self.editorBuiltins["RemoveIslandsEffect"] = EditorLib.RemoveIslandsEffect
00055     self.editorBuiltins["IdentifyIslandsEffect"] = EditorLib.IdentifyIslandsEffect
00056     self.editorBuiltins["SaveIslandEffect"] = EditorLib.SaveIslandEffect
00057     self.editorBuiltins["ChangeIslandEffect"] = EditorLib.ChangeIslandEffect
00058     self.editorBuiltins["GrowCutEffect"] = EditorLib.GrowCutEffect
00059     self.editorBuiltins["FastMarchingEffect"] = EditorLib.FastMarchingEffect
00060     self.editorBuiltins["WandEffect"] = EditorLib.WandEffect
00061 
00062     if not parent:
00063       self.parent = qt.QFrame()
00064       self.parent.setLayout( qt.QVBoxLayout() )
00065       self.create()
00066       self.parent.show()
00067     else:
00068       self.parent = parent
00069       self.create()
00070 
00071     # frame that holds widgets specific for each effect
00072     if not optionsFrame:
00073       self.optionsFrame = qt.QFrame(self.parent)
00074     else:
00075       self.optionsFrame = optionsFrame
00076 
00077     # state variables for selected effect in the box
00078     # - currentOption is an instance of an option GUI
00079     # - currentTools is a list of EffectTool instances
00080     self.currentOption = None
00081     self.currentTools = []
00082 
00083     # listen for changes in the Interaction Mode
00084     appLogic = slicer.app.applicationLogic()
00085     interactionNode = appLogic.GetInteractionNode()
00086     self.interactionNodeTag = interactionNode.AddObserver(interactionNode.InteractionModeChangedEvent, self.onInteractionModeChanged)
00087 
00088   def __del__(self):
00089     appLogic = slicer.app.applicationLogic()
00090     interactionNode = appLogic.GetInteractionNode()
00091     interactionNode.RemoveObserver(self.interactionNodeTag)
00092 
00093   def onInteractionModeChanged(self, caller, event):
00094     if caller.IsA('vtkMRMLInteractionNode'):
00095       if caller.GetCurrentInteractionMode() != caller.ViewTransform:
00096         self.defaultEffect()
00097 
00098   #
00099   # Public lists of the available effects provided by the editor
00100   #
00101 
00102   # effects that change the mouse cursor
00103   availableMouseTools = (
00104     "Paint", "Draw", "LevelTracing", "Rectangle", "ChangeIsland", "SaveIsland", "Wand",
00105     )
00106 
00107   # effects that operate from the menu (non mouse)
00108   availableOperations = (
00109     "DefaultTool", "EraseLabel",
00110     "IdentifyIslands", "RemoveIslands",
00111     "ErodeLabel", "DilateLabel", "ChangeLabel",
00112     "MakeModel", "GrowCutSegment",
00113     "Threshold",
00114     "PreviousCheckPoint", "NextCheckPoint",
00115     )
00116 
00117   # allow overriding the developers name of the tool for a more user-friendly label name
00118   displayNames = {}
00119   displayNames["PreviousCheckPoint"] = "Undo"
00120   displayNames["NextCheckPoint"] = "Redo"
00121 
00122   def findEffects(self, path=""):
00123     """fill the effects based built in and extension effects"""
00124 
00125     # combined list of all effects
00126     self.mouseTools = EditBox.availableMouseTools
00127     self.effects = self.mouseTools + EditBox.availableOperations
00128 
00129     # add builtins that have been registered
00130     self.effects = self.effects + tuple(self.editorBuiltins.keys())
00131 
00132     # add any extensions that have been registered
00133     self.effects = self.effects + tuple(slicer.modules.editorExtensions.keys())
00134 
00135     # for each effect
00136     # - look for implementation class of pattern *Effect
00137     # - get an icon name for the pushbutton
00138     iconDir = EditorLib.ICON_DIR
00139 
00140     self.effectIconFiles = {}
00141     self.effectModes = {}
00142     self.icons = {}
00143     for effect in self.effects:
00144       self.effectIconFiles[effect] = iconDir + effect + '.png'
00145 
00146       if effect in slicer.modules.editorExtensions.keys():
00147         extensionEffect = slicer.modules.editorExtensions[effect]()
00148         module = eval('slicer.modules.%s' % effect.lower())
00149         iconPath = os.path.join( os.path.dirname(module.path),"%s.png" % effect)
00150         self.effectIconFiles[effect] = iconPath
00151 
00152     # special case for renamed effect
00153     self.effectIconFiles["Rectangle"] = iconDir + "ImplicitRectangle" + '.png'
00154 
00155     # TOOD: add icons for builtins as resource or installed image directory
00156     self.effectIconFiles["PaintEffect"] = self.effectIconFiles["Paint"]
00157     self.effectIconFiles["DrawEffect"] = self.effectIconFiles["Draw"]
00158     self.effectIconFiles["ThresholdEffect"] = self.effectIconFiles["Threshold"]
00159     self.effectIconFiles["RectangleEffect"] = self.effectIconFiles["Rectangle"]
00160     self.effectIconFiles["LevelTracingEffect"] = self.effectIconFiles["LevelTracing"]
00161     self.effectIconFiles["MakeModelEffect"] = self.effectIconFiles["MakeModel"]
00162     self.effectIconFiles["ErodeEffect"] = self.effectIconFiles["ErodeLabel"]
00163     self.effectIconFiles["DilateEffect"] = self.effectIconFiles["DilateLabel"]
00164     self.effectIconFiles["IdentifyIslandsEffect"] = self.effectIconFiles["IdentifyIslands"]
00165     self.effectIconFiles["ChangeIslandEffect"] = self.effectIconFiles["ChangeIsland"]
00166     self.effectIconFiles["RemoveIslandsEffect"] = self.effectIconFiles["RemoveIslands"]
00167     self.effectIconFiles["SaveIslandEffect"] = self.effectIconFiles["SaveIsland"]
00168     self.effectIconFiles["ChangeIslandEffect"] = self.effectIconFiles["ChangeIsland"]
00169     self.effectIconFiles["ChangeLabelEffect"] = self.effectIconFiles["ChangeLabel"]
00170     self.effectIconFiles["GrowCutEffect"] = self.effectIconFiles["GrowCutSegment"]
00171     self.effectIconFiles["Wand"] = self.effectIconFiles["WandEffect"]
00172 
00173   def createButtonRow(self, effects, rowLabel=""):
00174     """ create a row of the edit box given a list of
00175     effect names (items in _effects(list) """
00176 
00177     rowFrame = qt.QFrame(self.mainFrame)
00178     self.mainFrame.layout().addWidget(rowFrame)
00179     self.rowFrames.append(rowFrame)
00180     hbox = qt.QHBoxLayout()
00181     rowFrame.setLayout( hbox )
00182 
00183     if rowLabel:
00184       label = qt.QLabel(rowLabel)
00185       hbox.addWidget(label)
00186 
00187     for effect in effects:
00188       # check that the effect belongs in our list of effects before including
00189       if (effect in self.effects):
00190         i = self.icons[effect] = qt.QIcon(self.effectIconFiles[effect])
00191         a = self.actions[effect] = qt.QAction(i, '', rowFrame)
00192         self.effectButtons[effect] = b = self.buttons[effect] = qt.QToolButton()
00193         b.setDefaultAction(a)
00194         b.setToolTip(effect)
00195         if EditBox.displayNames.has_key(effect):
00196           b.setToolTip(EditBox.displayNames[effect])
00197         hbox.addWidget(b)
00198 
00199         # Setup the mapping between button and its associated effect name
00200         self.effectMapper.setMapping(self.buttons[effect], effect)
00201         # Connect button with signal mapper
00202         self.buttons[effect].connect('clicked()', self.effectMapper, 'map()')
00203 
00204     hbox.addStretch(1)
00205 
00206   # create the edit box
00207   def create(self):
00208 
00209     self.findEffects()
00210 
00211     self.mainFrame = qt.QFrame(self.parent)
00212     vbox = qt.QVBoxLayout()
00213     self.mainFrame.setLayout(vbox)
00214     self.parent.layout().addWidget(self.mainFrame)
00215 
00216     #
00217     # the buttons
00218     #
00219     self.rowFrames = []
00220     self.actions = {}
00221     self.buttons = {}
00222     self.icons = {}
00223     self.callbacks = {}
00224 
00225     # create all of the buttons
00226     # createButtonRow() ensures that only effects in self.effects are exposed,
00227     self.createButtonRow( ("DefaultTool", "EraseLabel", "PaintEffect", "DrawEffect", "WandEffect", "LevelTracingEffect", "RectangleEffect", "IdentifyIslandsEffect", "ChangeIslandEffect", "RemoveIslandsEffect", "SaveIslandEffect") )
00228     self.createButtonRow( ("ErodeEffect", "DilateEffect", "GrowCutEffect", "ThresholdEffect", "ChangeLabelEffect", "MakeModelEffect", "FastMarchingEffect") )
00229 
00230     extensions = []
00231     for k in slicer.modules.editorExtensions:
00232       extensions.append(k)
00233     self.createButtonRow( extensions )
00234 
00235     self.createButtonRow( ("PreviousCheckPoint", "NextCheckPoint"), rowLabel="Undo/Redo: " )
00236 
00237     #
00238     # the labels
00239     #
00240     self.toolsActiveToolFrame = qt.QFrame(self.parent)
00241     self.toolsActiveToolFrame.setLayout(qt.QHBoxLayout())
00242     self.parent.layout().addWidget(self.toolsActiveToolFrame)
00243     self.toolsActiveTool = qt.QLabel(self.toolsActiveToolFrame)
00244     self.toolsActiveTool.setText( 'Active Tool:' )
00245     self.toolsActiveTool.setStyleSheet("background-color: rgb(232,230,235)")
00246     self.toolsActiveToolFrame.layout().addWidget(self.toolsActiveTool)
00247     self.toolsActiveToolName = qt.QLabel(self.toolsActiveToolFrame)
00248     self.toolsActiveToolName.setText( '' )
00249     self.toolsActiveToolName.setStyleSheet("background-color: rgb(232,230,235)")
00250     self.toolsActiveToolFrame.layout().addWidget(self.toolsActiveToolName)
00251 
00252     vbox.addStretch(1)
00253 
00254     self.updateUndoRedoButtons()
00255 
00256   def setActiveToolLabel(self,name):
00257     if EditBox.displayNames.has_key(name):
00258       name = EditBox.displayNames[name]
00259     self.toolsActiveToolName.setText(name)
00260 
00261   #
00262   # switch to the default tool
00263   #
00264   def defaultEffect(self):
00265     self.selectEffect("DefaultTool")
00266 
00267   #
00268   # manage the editor effects
00269   #
00270   def selectEffect(self, effectName):
00271 
00272     if effectName ==  "EraseLabel":
00273         self.editUtil.toggleLabel()
00274         return
00275     elif effectName ==  "PreviousCheckPoint":
00276         self.undoRedo.undo()
00277         return
00278     elif effectName == "NextCheckPoint":
00279         self.undoRedo.redo()
00280         return
00281 
00282     #
00283     # If there is no background volume or label map, do nothing
00284     #
00285     if not self.editUtil.getBackgroundVolume():
00286       return
00287     if not self.editUtil.getLabelVolume():
00288       return
00289 
00290     #
00291     # an effect was selected, so build an options GUI
00292     # - check to see if it is an extension effect,
00293     # if not, try to create it, else ignore it
00294     # For extensions, look for 'effect'Options and 'effect'Tool
00295     # in the editorExtensions map and use those to create the
00296     # effect
00297     #
00298     if self.currentOption:
00299       # clean up any existing effect
00300       self.currentOption.__del__()
00301       self.currentOption = None
00302       for tool in self.currentTools:
00303         tool.sliceWidget.unsetCursor()
00304         tool.cleanup()
00305       self.currentTools = []
00306 
00307     # look at builtins and extensions
00308     # - TODO: other effect styles are deprecated
00309     effectClass = None
00310     if effectName in slicer.modules.editorExtensions.keys():
00311       effectClass = slicer.modules.editorExtensions[effectName]()
00312     elif effectName in self.editorBuiltins.keys():
00313       effectClass = self.editorBuiltins[effectName]()
00314     if effectClass:
00315       # for effects, create an options gui and an
00316       # instance for every slice view
00317       self.currentOption = effectClass.options(self.optionsFrame)
00318       self.currentOption.undoRedo = self.undoRedo
00319       self.currentOption.defaultEffect = self.defaultEffect
00320       self.currentOption.create()
00321       self.currentOption.updateGUI()
00322       layoutManager = slicer.app.layoutManager()
00323       sliceNodeCount = slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLSliceNode')
00324       for nodeIndex in xrange(sliceNodeCount):
00325         # find the widget for each node in scene
00326         sliceNode = slicer.mrmlScene.GetNthNodeByClass(nodeIndex, 'vtkMRMLSliceNode')
00327         sliceWidget = layoutManager.sliceWidget(sliceNode.GetLayoutName())
00328         if sliceWidget:
00329           tool = effectClass.tool(sliceWidget)
00330           tool.undoRedo = self.undoRedo
00331           self.currentTools.append(tool)
00332       self.currentOption.tools = self.currentTools
00333     else:
00334       # fallback to internal classes
00335       try:
00336         options = eval("%sOptions" % effectName)
00337         self.currentOption = options(self.optionsFrame)
00338       except NameError, AttributeError:
00339         # No options for this effect, skip it
00340         pass
00341 
00342     self.setActiveToolLabel(effectName)
00343 
00344     # mouse tool changes cursor, and dismisses popup/menu
00345     toolName = effectName
00346     if toolName.endswith('Effect'):
00347       toolName = effectName[:-len('Effect')]
00348 
00349     if toolName in self.mouseTools:
00350       # set the interaction mode in case there was an active place going on
00351       appLogic = slicer.app.applicationLogic()
00352       interactionNode = appLogic.GetInteractionNode()
00353       interactionNode.SetCurrentInteractionMode(interactionNode.ViewTransform)
00354       # make an appropriate cursor for the tool
00355       cursor = self.cursorForEffect(effectName)
00356       for tool in self.currentTools:
00357         tool.sliceWidget.setCursor(cursor)
00358 
00359   def cursorForEffect(self,effectName):
00360     """Return an instance of QCursor customized for the given effectName.
00361     TODO: this could be moved to the EffectTool class so that effects can manage
00362     per-widget cursors, possibly turning them off or making them dynamic
00363     """
00364     if not effectName in self.effectCursors:
00365       baseImage = qt.QImage(":/Icons/AnnotationPointWithArrow.png")
00366       effectImage = qt.QImage(self.effectIconFiles[effectName])
00367       width = max(baseImage.width(), effectImage.width())
00368       pad = -9
00369       height = pad + baseImage.height() + effectImage.height()
00370       width = height = max(width,height)
00371       center = int(width/2)
00372       cursorImage = qt.QImage(width, height, qt.QImage().Format_ARGB32)
00373       painter = qt.QPainter()
00374       cursorImage.fill(0)
00375       painter.begin(cursorImage)
00376       point = qt.QPoint(center - (baseImage.width()/2), 0)
00377       painter.drawImage(point, baseImage)
00378       point.setX(center - (effectImage.width()/2))
00379       point.setY(cursorImage.height() - effectImage.height())
00380       painter.drawImage(point, effectImage)
00381       painter.end()
00382       cursorPixmap = qt.QPixmap()
00383       cursorPixmap = cursorPixmap.fromImage(cursorImage)
00384       self.effectCursors[effectName] = qt.QCursor(cursorPixmap,center,0)
00385     return self.effectCursors[effectName]
00386 
00387   def updateUndoRedoButtons(self):
00388     self.effectButtons["PreviousCheckPoint"].enabled = self.undoRedo.undoEnabled()
00389     self.effectButtons["NextCheckPoint"].enabled = self.undoRedo.redoEnabled()
00390 
00391   def isFloatingMode(self):
00392     return self.mainFrame.parent() is None
00393 
00394   def enterFloatingMode(self):
00395     self.mainFrame.setParent(None)
00396     cursorPosition = qt.QCursor().pos()
00397     w = self.mainFrame.width
00398     h = self.mainFrame.height
00399     self.mainFrame.pos = qt.QPoint(cursorPosition.x() - w/2, cursorPosition.y() - h/2)
00400     self.mainFrame.show()
00401     self.mainFrame.raise_()
00402     Key_Space = 0x20 # not in PythonQt
00403     self.toggleShortcut = qt.QShortcut(self.mainFrame)
00404     self.toggleShortcut.setKey( qt.QKeySequence(Key_Space) )
00405     self.toggleShortcut.connect( 'activated()', self.toggleFloatingMode )
00406 
00407   def cancelFloatingMode(self):
00408     if self.isFloatingMode():
00409       if self.toggleShortcut:
00410         self.toggleShortcut.disconnect('activated()')
00411         self.toggleShortcut.setParent(None)
00412         self.toggleShortcut = None
00413       self.mainFrame.setParent(self.parent)
00414       self.parent.layout().addWidget(self.mainFrame)
00415 
00416   def toggleFloatingMode(self):
00417     """Set or clear the parent of the edit box so that it is a top level
00418     window or embeded in the gui as appropriate.  Meant to be associated
00419     with the space bar shortcut for the mainWindow, set in Editor.py"""
00420     if self.isFloatingMode():
00421       self.cancelFloatingMode()
00422     else:
00423       self.enterFloatingMode()
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Defines