Esempio n. 1
0
    def __init__( self, navigationController, brushingController ):
        QObject.__init__( self )
        self._navCtrl = navigationController
        self._navIntr = NavigationInterpreter( navigationController )
        self._brushingCtrl = brushingController
        self._current_state = self.FINAL
        self._temp_erasing = False # indicates, if user pressed shift
                                   # for temporary erasing (in
                                   # contrast to selecting the eraser brush)

        self._lineItems = [] # list of line items that have been
                            # added to the qgraphicsscene for drawing indication
                            
        self._lastEvent = None
        self._doubleClickTimer = None

        # clear the temporary line items once they
        # have been pushed to the sink
        self._brushingCtrl.wroteToSink.connect(self.clearLines)
Esempio n. 2
0
 def __init__( self, navigationController, layerStack, posModel ):
     QObject.__init__( self )
     self._navCtrl = navigationController
     self._navIntr = NavigationInterpreter( navigationController )
     self._layerStack = layerStack
     self._active_layer = None
     self._active_channel_idx = -1
     self._current_state = self.FINAL
     self._current_position = QPoint(0,0)
     # Setting default values, scaled on actual data later on 
     self._steps_mean = 10 
     self._steps_delta = self._steps_mean*2
     self._steps_scaling = 0.07
     self._range_max = 4096.0 # hardcoded, in the case drange is not set in the data file or in the dataSelectionDialogue
     self._range_min = -4096.0
     self._range = np.abs(self._range_max-self._range_min)
     self._channel_range = dict()
     self._posModel = posModel
Esempio n. 3
0
    def __init__( self, navigationController, brushingController ):
        QObject.__init__( self )
        self._navCtrl = navigationController
        self._navIntr = NavigationInterpreter( navigationController )
        self._brushingCtrl = brushingController
        self._current_state = self.FINAL
        self._temp_erasing = False # indicates, if user pressed shift
                                   # for temporary erasing (in
                                   # contrast to selecting the eraser brush)

        self._lineItems = [] # list of line items that have been
                            # added to the qgraphicsscene for drawing indication
                            
        self._lastEvent = None
        self._doubleClickTimer = None

        # clear the temporary line items once they
        # have been pushed to the sink
        self._brushingCtrl.wroteToSink.connect(self.clearLines)
Esempio n. 4
0
class BrushingInterpreter( QObject ):
    # states
    FINAL           = 0
    DEFAULT_MODE    = 1

    # FIXME: This state isn't really needed, now that we use a QTimer to manage the double-click case.
    #        (The state machine should be rewritten.)
    MAYBE_DRAW_MODE = 2 #received a single left-click; however, the next event
                        #might be a double-click event; therefore the state has
                        #not been decided yet
    DRAW_MODE       = 3

    @property
    def state( self ):
        return self._current_state

    def __init__( self, navigationController, brushingController ):
        QObject.__init__( self )
        self._navCtrl = navigationController
        self._navIntr = NavigationInterpreter( navigationController )
        self._brushingCtrl = brushingController
        self._current_state = self.FINAL
        self._temp_erasing = False # indicates, if user pressed shift
                                   # for temporary erasing (in
                                   # contrast to selecting the eraser brush)

        self._lineItems = [] # list of line items that have been
                            # added to the qgraphicsscene for drawing indication
                            
        self._lastEvent = None
        self._doubleClickTimer = None

        # clear the temporary line items once they
        # have been pushed to the sink
        self._brushingCtrl.wroteToSink.connect(self.clearLines)

    def start( self ):
        if self._current_state == self.FINAL:
            self._navIntr.start()
            self._current_state = self.DEFAULT_MODE
        else:
            pass # ignore

    def stop( self ):
        if self._brushingCtrl._isDrawing:
            for imageview in self._navCtrl._views:
                self._brushingCtrl.endDrawing(imageview.mousePos)
        self._current_state = self.FINAL
        self._navIntr.stop()

    def eventFilter( self, watched, event ):
        etype = event.type()

        # Before we steal this event from the scene, check that it is allowing brush strokes 
        allow_brushing = True
        for view in self._navCtrl._views:
            allow_brushing &= view.scene().allow_brushing
        if not allow_brushing:
            return self._navIntr.eventFilter( watched, event )
        
        if etype == QEvent.MouseButtonDblClick and self._doubleClickTimer is not None:
            # On doubleclick, cancel release handler that normally draws the stroke.
            self._doubleClickTimer.stop()
            self._doubleClickTimer = None
            self._current_state = self.DEFAULT_MODE
            self.onEntry_default( watched, event )
        
        if self._current_state == self.DEFAULT_MODE:
            if etype == QEvent.MouseButtonPress \
                and event.button() == Qt.LeftButton \
                and event.modifiers() == Qt.NoModifier \
                and self._navIntr.mousePositionValid(watched, event):
                
                ### default mode -> maybe draw mode
                self._current_state = self.MAYBE_DRAW_MODE

                # event will not be valid to use after this function exits,
                # so we must make a copy of it instead of just saving the pointer
                self._lastEvent = QMouseEvent( event.type(), event.pos(), event.globalPos(), event.button(), event.buttons(), event.modifiers() )
                
        elif self._current_state == self.MAYBE_DRAW_MODE:
            if etype == QEvent.MouseMove:
                # navigation interpreter also has to be in
                # default mode to avoid inconsistencies
                if self._navIntr.state == self._navIntr.DEFAULT_MODE:
                    ### maybe draw mode -> maybe draw mode
                    self._current_state = self.DRAW_MODE
                    self.onEntry_draw( watched, self._lastEvent )
                    self.onMouseMove_draw( watched, event )
                    return True
                else:
                    self._navIntr.eventFilter( watched, self._lastEvent )
                    return self._navIntr.eventFilter( watched, event )
            elif etype == QEvent.MouseButtonDblClick:
                ### maybe draw mode -> default mode
                self._current_state = self.DEFAULT_MODE
                return self._navIntr.eventFilter( watched, event )
            elif etype == QEvent.MouseButtonRelease:
                def handleRelease(releaseEvent):
                    self._current_state = self.DRAW_MODE
                    self.onEntry_draw( watched, self._lastEvent )
                    self.onExit_draw( watched, releaseEvent)
                    self._current_state = self.DEFAULT_MODE
                    self.onEntry_default( watched, releaseEvent )

                # If this event is part of a double-click, we don't really want to handle it.
                # Typical event sequence is press, release, double-click (not two presses).
                # Instead of handling this right away, set a timer to do the work.
                # We'll cancel the timer if we see a double-click event (see above).
                self._doubleClickTimer = QTimer(self)
                self._doubleClickTimer.setInterval(200)
                self._doubleClickTimer.setSingleShot(True)
                # event will not be valid to use after this function exits,
                # so we must make a copy of it instead of just saving the pointer
                eventCopy = QMouseEvent( event.type(), event.pos(), event.button(), event.buttons(), event.modifiers() )
                self._doubleClickTimer.timeout.connect( partial(handleRelease, eventCopy ) )
                self._doubleClickTimer.start()

                return True

        elif self._current_state == self.DRAW_MODE:
            if etype == QEvent.MouseButtonRelease and event.button() == Qt.LeftButton:
                self.onExit_draw( watched, event )
                ### draw mode -> default mode
                self._current_state = self.DEFAULT_MODE
                self.onEntry_default( watched, event )
                return True

            elif etype == QEvent.MouseMove and event.buttons() & Qt.LeftButton:
                if self._navIntr.mousePositionValid(watched, event):
                    self.onMouseMove_draw( watched, event )
                    return True
                else:
                    self.onExit_draw( watched, event )
                    ### draw mode -> default mode
                    self._current_state = self.DEFAULT_MODE
                    self.onEntry_default( watched, event )

        # let the navigation interpreter handle common events
        return self._navIntr.eventFilter( watched, event )

    ###
    ### Default Mode
    ###
    def onEntry_default( self, imageview, event ):
        pass

    ###
    ### Draw Mode
    ###
    def onEntry_draw( self, imageview, event ):
        if QApplication.keyboardModifiers() == Qt.ShiftModifier:
            self._brushingCtrl._brushingModel.setErasing()
            self._temp_erasing = True
        imageview.mousePos = imageview.mapScene2Data(imageview.mapToScene(event.pos()))
        self._brushingCtrl.beginDrawing(imageview, imageview.mousePos)

    def onExit_draw( self, imageview, event ):
        self._brushingCtrl.endDrawing(imageview.mousePos)
        if self._temp_erasing:
            self._brushingCtrl._brushingModel.disableErasing()
            self._temp_erasing = False

    def onMouseMove_draw( self, imageview, event ):
        self._navIntr.onMouseMove_default( imageview, event )

        o = imageview.scene().data2scene.map(QPointF(imageview.oldX,imageview.oldY))
        n = imageview.scene().data2scene.map(QPointF(imageview.x,imageview.y))

        # Draw temporary line for the brush stroke so the user gets feedback before the data is really updated.
        pen = QPen( QBrush(self._brushingCtrl._brushingModel.drawColor), self._brushingCtrl._brushingModel.brushSize, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)
        line = QGraphicsLineItem(o.x(), o.y(), n.x(), n.y())
        line.setPen(pen)
        
        imageview.scene().addItem(line)
        line.setParentItem(imageview.scene().dataRectItem)

        self._lineItems.append(line)
        self._brushingCtrl._brushingModel.moveTo(imageview.mousePos)

    def clearLines(self):
        # This is called after the brush stroke is stored to the data.
        # Our temporary line object is no longer needed because the data provides the true pixel labels that were stored.
        lines = self._lineItems
        self._lineItems = []
        for l in lines:
            l.hide()

    def updateCursorPosition(self, *args, **kwargs):
        self._navIntr.updateCursorPosition(*args, **kwargs)
Esempio n. 5
0
class ThresholdingInterpreter( QObject ):
    # states
    FINAL             = 0
    DEFAULT_MODE      = 1 # normal navigation functionality
    THRESHOLDING_MODE = 2 # while pressing left mouse button allow thresholding
    NO_VALID_LAYER    = 3 # not a grayscale layer 
    
    @property
    def state( self ):
        return self._current_state

    def __init__( self, navigationController, layerStack, posModel ):
        QObject.__init__( self )
        self._navCtrl = navigationController
        self._navIntr = NavigationInterpreter( navigationController )
        self._layerStack = layerStack
        self._active_layer = None
        self._active_channel_idx = -1
        self._current_state = self.FINAL
        self._current_position = QPoint(0,0)
        # Setting default values, scaled on actual data later on 
        self._steps_mean = 10 
        self._steps_delta = self._steps_mean*2
        self._steps_scaling = 0.07
        self._range_max = 4096.0 # hardcoded, in the case drange is not set in the data file or in the dataSelectionDialogue
        self._range_min = -4096.0
        self._range = np.abs(self._range_max-self._range_min)
        self._channel_range = dict()
        self._posModel = posModel

    def start( self ):
        if self._current_state == self.FINAL:
            self._navIntr.start()
            self._current_state = self.DEFAULT_MODE
            self._init_layer()
        else:
            pass 
    
    def stop( self ):
        self._current_state = self.FINAL
        if self.valid_layer():
            self._active_layer.channelChanged.disconnect(self.channel_changed)
        self._navIntr.stop()            
        
    def eventFilter( self, watched, event ):
        etype = event.type()
        if self._current_state == self.DEFAULT_MODE:
            if etype == QEvent.MouseButtonPress \
                    and event.button() == Qt.LeftButton \
                    and event.modifiers() == Qt.NoModifier \
                    and self._navIntr.mousePositionValid(watched, event): 
                # TODO maybe remove, if we can find out which view is active
                self.set_active_layer()
                if self.valid_layer():
                    self._current_state = self.THRESHOLDING_MODE
                    self._current_position = watched.mapToGlobal( event.pos() )
                    return True
                else:
                    self._current_state = self.NO_VALID_LAYER
                return self._navIntr.eventFilter( watched, event )
            elif etype == QEvent.MouseButtonPress \
                    and event.button() == Qt.RightButton \
                    and event.modifiers() == Qt.NoModifier \
                    and self._navIntr.mousePositionValid(watched, event):
                self.set_active_layer()
                if self.valid_layer():
                    self.onRightClick_resetThreshold(watched, event)
                else:
                    pass # do nothing
                return True
            else:
                return self._navIntr.eventFilter( watched, event )
        elif self._current_state == self.NO_VALID_LAYER:
            self.set_active_layer()
            if self.valid_layer():
                self._current_state = self.DEFAULT_MODE
            return self._navIntr.eventFilter( watched, event )
        elif self._current_state == self.THRESHOLDING_MODE:
            if self._active_layer == None: # No active layer set, should not go here
                return self._navIntr.eventFilter( watched, event )
            if etype == QEvent.MouseButtonRelease and event.button() == Qt.LeftButton:
                self._current_state = self.DEFAULT_MODE
                self._active_layer = None
                self.onExit_threshold( watched, event )
                return True
            elif etype == QEvent.MouseMove and event.buttons() == Qt.LeftButton:
                self.onMouseMove_thresholding(watched, event)
                return True
            else:
                return self._navIntr.eventFilter( watched, event )
        else:
            # let the navigation interpreter handle common events
            return self._navIntr.eventFilter( watched, event )
    
    def onRightClick_resetThreshold(self, imageview, event):
        range = self.get_min_max_of_current_view(imageview)
        self._active_layer.set_normalize(0, (range[0],range[1]))
        self._channel_range[self._active_channel_idx] = (range[0],range[1])

    def set_active_layer(self):
        """
        determines the layer postion in the stack and the currently displayed
        channel. Needs to be called constantly, because the user can change the 
        position of the input layer within the stack
        """
        for idx, layer in enumerate(self._layerStack):
            if isinstance(layer, GrayscaleLayer):
                if layer.window_leveling:
                    self._active_layer = layer
                    self._active_channel_idx= layer._channel
                    return
        self._active_layer = None

    def _init_layer(self):
        self.set_active_layer()
        if self.valid_layer():
            self._active_layer.channelChanged.connect(self.channel_changed)
            if self.get_drange() != None:
                self._range_min, self._range_max = self.get_drange()
        
    def onExit_threshold( self, watched, event ):
        pass

    def get_drange(self):
        """ 
        returns tuple of drange (min, max) as set in hdf5 file or None 
        if nothing is specified
        """
        return self._active_layer._datasources[0]._rawSource._op5.Output.meta.drange

    def valid_layer(self):
        if isinstance(self._active_layer, GrayscaleLayer):
            return self._active_layer.window_leveling
        else:
            return False

    def channel_changed(self):
        self.set_active_layer()
        if self._active_channel_idx in self._channel_range:
            self._active_layer.set_normalize(0, self._channel_range[self._active_channel_idx])
        else:
            self._active_layer.set_normalize(0,(self._range_min, 
                                                self._range_max))

    def get_min_max_of_current_view(self, imageview):
        """
        Function returns min and max value of the current view 
        based on the raw data.
        Ugly hack, but all we got for now
        """
        shape2D = posView2D( list(self._posModel.shape5D[1:4]), 
                             axis=self._posModel.activeView )
        data_x, data_y = 0, 0
        data_x2, data_y2 = shape2D[0], shape2D[1]
        
        if self._posModel.activeView == 0:
            x_pos = self._posModel.slicingPos5D[1]
            slicing = [slice(0, 1), 
                       slice(x_pos, x_pos+1), 
                       slice(data_x, data_x2), 
                       slice(data_y, data_y2), 
                       slice(self._active_channel_idx, self._active_channel_idx+1)]
        if self._posModel.activeView == 1:
            y_pos = self._posModel.slicingPos5D[2]
            slicing = [slice(0, 1), 
                       slice(data_x, data_x2), 
                       slice(y_pos, y_pos+1), 
                       slice(data_y, data_y2), 
                       slice(self._active_channel_idx, self._active_channel_idx+1)]
        if self._posModel.activeView == 2:
            z_pos = self._posModel.slicingPos5D[3]
            slicing = [slice(0, 1), 
                       slice(data_x, data_x2), 
                       slice(data_y, data_y2), 
                       slice(z_pos, z_pos+1), 
                       slice(self._active_channel_idx, self._active_channel_idx+1)]
        request = self._active_layer._datasources[0].request(slicing)
        result = request.wait()
        return result.min(), result.max()

    def onMouseMove_thresholding(self, imageview, event):
        if self._active_channel_idx not in self._channel_range:
            range = self.get_min_max_of_current_view(imageview)
            range_lower = range[0]
            range_upper = range[1]
        else:
            range = self._channel_range[self._active_channel_idx]
            range_lower = range[0]
            range_upper = range[1]
        # don't know what version is more efficient
        # range_delta = np.sqrt((range_upper - range_lower)**2) 
        range_delta = np.abs(range_upper - range_lower)
        range_mean = range_lower + range_delta/2.0

        self._steps_mean = range_delta * self._steps_scaling
        self._steps_delta = self._steps_mean * 2
        pos = imageview.mapToGlobal( event.pos() )
        dx =  pos.x() - self._current_position.x()
        dy =  self._current_position.y() - pos.y()

        if dx > 0.0:
            # move mean to right
            range_mean += self._steps_mean
        elif dx < 0.0:
            # move mean to left
            range_mean -= self._steps_mean
        
        if dy > 0.0:
            # increase delta
            range_delta += self._steps_delta
        elif dy < 0.0:
            # decrease delta
            range_delta -= self._steps_delta

        # check the bounds, ugly use min max values actually present
        if range_mean < self._range_min:
            range_mean = self._range_min
        elif range_mean > self._range_max:
            range_mean = self._range_max
        
        if range_delta < 1:
            range_delta = 1
        elif range_delta > self._range: 
            range_delta = self._range

        a = range_mean - range_delta/2.0
        b = range_mean + range_delta/2.0

        if a < self._range_min:
            a = self._range_min
        elif a > self._range_max:
            a = self._range_max
        
        if b < self._range_min:
            b = self._range_min
        elif b > self._range_max:
            b = self._range_max

        assert a <= b 

        # TODO test if in allowed range (i.e. max and min of data)
        self._active_layer.set_normalize(0, (a,b))
        self._channel_range[self._active_channel_idx] = (a,b)
        self._current_position = pos
Esempio n. 6
0
    def __init__(self,
                 layerStackModel,
                 parent,
                 labelsink=None,
                 crosshair=True,
                 syncAlongAxes=(0, 1)):
        super(VolumeEditor, self).__init__(parent=parent)
        self._sync_along = tuple(syncAlongAxes)

        ##
        ## properties
        ##
        self._showDebugPatches = False
        self._showTileProgress = True

        ##
        ## base components
        ##
        self.layerStack = layerStackModel
        self.posModel = PositionModel(self)
        self.brushingModel = BrushingModel()
        self.cropModel = CropExtentsModel(self)

        self.imageScenes = [
            ImageScene2D(self.posModel, (0, 1, 4), swapped_default=True),
            ImageScene2D(self.posModel, (0, 2, 4)),
            ImageScene2D(self.posModel, (0, 3, 4))
        ]
        self.imageViews = [
            ImageView2D(parent, self.cropModel, self.imageScenes[i])
            for i in [0, 1, 2]
        ]
        self.imageViews[0].focusChanged.connect(
            lambda arg=0: self.lastImageViewFocus(arg))
        self.imageViews[1].focusChanged.connect(
            lambda arg=1: self.lastImageViewFocus(arg))
        self.imageViews[2].focusChanged.connect(
            lambda arg=2: self.lastImageViewFocus(arg))
        self._lastImageViewFocus = 0

        if not crosshair:
            for view in self.imageViews:
                view._crossHairCursor.enabled = False

        self.imagepumps = self._initImagePumps()

        self.view3d = self._initView3d() if useVTK else QWidget()

        names = ['x', 'y', 'z']
        for scene, name, pump in zip(self.imageScenes, names, self.imagepumps):
            scene.setObjectName(name)
            scene.stackedImageSources = pump.stackedImageSources

        self.cacheSize = 50

        ##
        ## interaction
        ##
        # event switch
        self.eventSwitch = EventSwitch(self.imageViews)

        # navigation control
        v3d = self.view3d if useVTK else None
        self.navCtrl = NavigationController(self.imageViews,
                                            self.imagepumps,
                                            self.posModel,
                                            view3d=v3d)
        self.navInterpret = NavigationInterpreter(self.navCtrl)

        # brushing control
        if crosshair:
            self.crosshairController = CrosshairController(
                self.brushingModel, self.imageViews)
        self.brushingController = BrushingController(self.brushingModel,
                                                     self.posModel, labelsink)
        self.brushingInterpreter = BrushingInterpreter(self.navCtrl,
                                                       self.brushingController)

        for v in self.imageViews:
            self.brushingController._brushingModel.brushSizeChanged.connect(
                v._sliceIntersectionMarker._set_diameter)

        # thresholding control
        self.thresInterpreter = ThresholdingInterpreter(
            self.navCtrl, self.layerStack, self.posModel)
        # initial interaction mode
        self.eventSwitch.interpreter = self.navInterpret

        # By default, don't show cropping controls
        self.showCropLines(False)

        ##
        ## connect
        ##
        self.posModel.timeChanged.connect(self.navCtrl.changeTime)
        self.posModel.slicingPositionChanged.connect(
            self.navCtrl.moveSlicingPosition)
        if crosshair:
            self.posModel.cursorPositionChanged.connect(
                self.navCtrl.moveCrosshair)
        self.posModel.slicingPositionSettled.connect(
            self.navCtrl.settleSlicingPosition)

        self.layerStack.layerAdded.connect(self._onLayerAdded)
        self.parent = parent
Esempio n. 7
0
class BrushingInterpreter( QObject ):
    # states
    FINAL           = 0
    DEFAULT_MODE    = 1

    # FIXME: This state isn't really needed, now that we use a QTimer to manage the double-click case.
    #        (The state machine should be rewritten.)
    MAYBE_DRAW_MODE = 2 #received a single left-click; however, the next event
                        #might be a double-click event; therefore the state has
                        #not been decided yet
    DRAW_MODE       = 3

    @property
    def state( self ):
        return self._current_state

    def __init__( self, navigationController, brushingController ):
        QObject.__init__( self )
        self._navCtrl = navigationController
        self._navIntr = NavigationInterpreter( navigationController )
        self._brushingCtrl = brushingController
        self._current_state = self.FINAL
        self._temp_erasing = False # indicates, if user pressed shift
                                   # for temporary erasing (in
                                   # contrast to selecting the eraser brush)

        self._lineItems = [] # list of line items that have been
                            # added to the qgraphicsscene for drawing indication
                            
        self._lastEvent = None
        self._doubleClickTimer = None

        # clear the temporary line items once they
        # have been pushed to the sink
        self._brushingCtrl.wroteToSink.connect(self.clearLines)

    def start( self ):
        if self._current_state == self.FINAL:
            self._navIntr.start()
            self._current_state = self.DEFAULT_MODE
        else:
            pass # ignore

    def stop( self ):
        if self._brushingCtrl._isDrawing:
            for imageview in self._navCtrl._views:
                self._brushingCtrl.endDrawing(imageview.mousePos)
        self._current_state = self.FINAL
        self._navIntr.stop()

    def eventFilter( self, watched, event ):
        etype = event.type()

        # Before we steal this event from the scene, check that it is allowing brush strokes 
        allow_brushing = True
        for view in self._navCtrl._views:
            allow_brushing &= view.scene().allow_brushing
        if not allow_brushing:
            return self._navIntr.eventFilter( watched, event )
        
        if etype == QEvent.MouseButtonDblClick and self._doubleClickTimer is not None:
            # On doubleclick, cancel release handler that normally draws the stroke.
            self._doubleClickTimer.stop()
            self._doubleClickTimer = None
            self._current_state = self.DEFAULT_MODE
            self.onEntry_default( watched, event )
        
        if self._current_state == self.DEFAULT_MODE:
            if etype == QEvent.MouseButtonPress \
                and event.button() == Qt.LeftButton \
                and event.modifiers() == Qt.NoModifier \
                and self._navIntr.mousePositionValid(watched, event):
                
                ### default mode -> maybe draw mode
                self._current_state = self.MAYBE_DRAW_MODE

                # event will not be valid to use after this function exits,
                # so we must make a copy of it instead of just saving the pointer
                self._lastEvent = QMouseEvent( event.type(), event.pos(), event.globalPos(), event.button(), event.buttons(), event.modifiers() )
                
        elif self._current_state == self.MAYBE_DRAW_MODE:
            if etype == QEvent.MouseMove:
                # navigation interpreter also has to be in
                # default mode to avoid inconsistencies
                if self._navIntr.state == self._navIntr.DEFAULT_MODE:
                    ### maybe draw mode -> maybe draw mode
                    self._current_state = self.DRAW_MODE
                    self.onEntry_draw( watched, self._lastEvent )
                    self.onMouseMove_draw( watched, event )
                    return True
                else:
                    self._navIntr.eventFilter( watched, self._lastEvent )
                    return self._navIntr.eventFilter( watched, event )
            elif etype == QEvent.MouseButtonDblClick:
                ### maybe draw mode -> default mode
                self._current_state = self.DEFAULT_MODE
                return self._navIntr.eventFilter( watched, event )
            elif etype == QEvent.MouseButtonRelease:
                def handleRelease(releaseEvent):
                    self._current_state = self.DRAW_MODE
                    self.onEntry_draw( watched, self._lastEvent )
                    self.onExit_draw( watched, releaseEvent)
                    self._current_state = self.DEFAULT_MODE
                    self.onEntry_default( watched, releaseEvent )

                # If this event is part of a double-click, we don't really want to handle it.
                # Typical event sequence is press, release, double-click (not two presses).
                # Instead of handling this right away, set a timer to do the work.
                # We'll cancel the timer if we see a double-click event (see above).
                self._doubleClickTimer = QTimer(self)
                self._doubleClickTimer.setInterval(200)
                self._doubleClickTimer.setSingleShot(True)
                # event will not be valid to use after this function exits,
                # so we must make a copy of it instead of just saving the pointer
                eventCopy = QMouseEvent( event.type(), event.pos(), event.button(), event.buttons(), event.modifiers() )
                self._doubleClickTimer.timeout.connect( partial(handleRelease, eventCopy ) )
                self._doubleClickTimer.start()

                return True

        elif self._current_state == self.DRAW_MODE:
            if etype == QEvent.MouseButtonRelease and event.button() == Qt.LeftButton:
                self.onExit_draw( watched, event )
                ### draw mode -> default mode
                self._current_state = self.DEFAULT_MODE
                self.onEntry_default( watched, event )
                return True

            elif etype == QEvent.MouseMove and event.buttons() & Qt.LeftButton:
                if self._navIntr.mousePositionValid(watched, event):
                    self.onMouseMove_draw( watched, event )
                    return True
                else:
                    self.onExit_draw( watched, event )
                    ### draw mode -> default mode
                    self._current_state = self.DEFAULT_MODE
                    self.onEntry_default( watched, event )

        # let the navigation interpreter handle common events
        return self._navIntr.eventFilter( watched, event )

    ###
    ### Default Mode
    ###
    def onEntry_default( self, imageview, event ):
        pass

    ###
    ### Draw Mode
    ###
    def onEntry_draw( self, imageview, event ):
        if QApplication.keyboardModifiers() == Qt.ShiftModifier:
            self._brushingCtrl._brushingModel.setErasing()
            self._temp_erasing = True
        imageview.mousePos = imageview.mapScene2Data(imageview.mapToScene(event.pos()))
        self._brushingCtrl.beginDrawing(imageview, imageview.mousePos)

    def onExit_draw( self, imageview, event ):
        self._brushingCtrl.endDrawing(imageview.mousePos)
        if self._temp_erasing:
            self._brushingCtrl._brushingModel.disableErasing()
            self._temp_erasing = False

    def onMouseMove_draw( self, imageview, event ):
        self._navIntr.onMouseMove_default( imageview, event )

        o = imageview.scene().data2scene.map(QPointF(imageview.oldX,imageview.oldY))
        n = imageview.scene().data2scene.map(QPointF(imageview.x,imageview.y))

        # Draw temporary line for the brush stroke so the user gets feedback before the data is really updated.
        pen = QPen( QBrush(self._brushingCtrl._brushingModel.drawColor), self._brushingCtrl._brushingModel.brushSize, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)
        line = QGraphicsLineItem(o.x(), o.y(), n.x(), n.y())
        line.setPen(pen)
        
        imageview.scene().addItem(line)
        line.setParentItem(imageview.scene().dataRectItem)

        self._lineItems.append(line)
        self._brushingCtrl._brushingModel.moveTo(imageview.mousePos)

    def clearLines(self):
        # This is called after the brush stroke is stored to the data.
        # Our temporary line object is no longer needed because the data provides the true pixel labels that were stored.
        lines = self._lineItems
        self._lineItems = []
        for l in lines:
            l.hide()

    def updateCursorPosition(self, *args, **kwargs):
        self._navIntr.updateCursorPosition(*args, **kwargs)