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)