| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842 | from qtpy import QtCorefrom qtpy import QtGuifrom qtpy import QtWidgetsfrom labelme import QT5from labelme.shape import Shapeimport labelme.utils# TODO(unknown):# - [maybe] Find optimal epsilon value.CURSOR_DEFAULT = QtCore.Qt.ArrowCursorCURSOR_POINT = QtCore.Qt.PointingHandCursorCURSOR_DRAW = QtCore.Qt.CrossCursorCURSOR_MOVE = QtCore.Qt.ClosedHandCursorCURSOR_GRAB = QtCore.Qt.OpenHandCursorclass Canvas(QtWidgets.QWidget):    zoomRequest = QtCore.Signal(int, QtCore.QPoint)    scrollRequest = QtCore.Signal(int, int)    newShape = QtCore.Signal()    selectionChanged = QtCore.Signal(list)    shapeMoved = QtCore.Signal()    drawingPolygon = QtCore.Signal(bool)    vertexSelected = QtCore.Signal(bool)    CREATE, EDIT = 0, 1    CREATE, EDIT = 0, 1    # polygon, rectangle, line, or point    _createMode = "polygon"    _fill_drawing = False    def __init__(self, *args, **kwargs):        self.epsilon = kwargs.pop("epsilon", 10.0)        self.double_click = kwargs.pop("double_click", "close")        if self.double_click not in [None, "close"]:            raise ValueError(                "Unexpected value for double_click event: {}".format(                    self.double_click                )            )        self.num_backups = kwargs.pop("num_backups", 10)        super(Canvas, self).__init__(*args, **kwargs)        # Initialise local state.        self.mode = self.EDIT        self.shapes = []        self.shapesBackups = []        self.current = None        self.selectedShapes = []  # save the selected shapes here        self.selectedShapesCopy = []        # self.line represents:        #   - createMode == 'polygon': edge from last point to current        #   - createMode == 'rectangle': diagonal line of the rectangle        #   - createMode == 'line': the line        #   - createMode == 'point': the point        self.line = Shape()        self.prevPoint = QtCore.QPoint()        self.prevMovePoint = QtCore.QPoint()        self.offsets = QtCore.QPoint(), QtCore.QPoint()        self.scale = 1.0        self.pixmap = QtGui.QPixmap()        self.visible = {}        self._hideBackround = False        self.hideBackround = False        self.hShape = None        self.prevhShape = None        self.hVertex = None        self.prevhVertex = None        self.hEdge = None        self.prevhEdge = None        self.movingShape = False        self.snapping = True        self.hShapeIsSelected = False        self._painter = QtGui.QPainter()        self._cursor = CURSOR_DEFAULT        # Menus:        # 0: right-click without selection and dragging of shapes        # 1: right-click with selection and dragging of shapes        self.menus = (QtWidgets.QMenu(), QtWidgets.QMenu())        # Set widget options.        self.setMouseTracking(True)        self.setFocusPolicy(QtCore.Qt.WheelFocus)    def fillDrawing(self):        return self._fill_drawing    def setFillDrawing(self, value):        self._fill_drawing = value    @property    def createMode(self):        return self._createMode    @createMode.setter    def createMode(self, value):        if value not in [            "polygon",            "rectangle",            "circle",            "line",            "point",            "linestrip",        ]:            raise ValueError("Unsupported createMode: %s" % value)        self._createMode = value    def storeShapes(self):        shapesBackup = []        for shape in self.shapes:            shapesBackup.append(shape.copy())        if len(self.shapesBackups) > self.num_backups:            self.shapesBackups = self.shapesBackups[-self.num_backups - 1 :]        self.shapesBackups.append(shapesBackup)    @property    def isShapeRestorable(self):        # We save the state AFTER each edit (not before) so for an        # edit to be undoable, we expect the CURRENT and the PREVIOUS state        # to be in the undo stack.        if len(self.shapesBackups) < 2:            return False        return True    def restoreShape(self):        # This does _part_ of the job of restoring shapes.        # The complete process is also done in app.py::undoShapeEdit        # and app.py::loadShapes and our own Canvas::loadShapes function.        if not self.isShapeRestorable:            return        self.shapesBackups.pop()  # latest        # The application will eventually call Canvas.loadShapes which will        # push this right back onto the stack.        shapesBackup = self.shapesBackups.pop()        self.shapes = shapesBackup        self.selectedShapes = []        for shape in self.shapes:            shape.selected = False        self.update()    def enterEvent(self, ev):        self.overrideCursor(self._cursor)    def leaveEvent(self, ev):        self.unHighlight()        self.restoreCursor()    def focusOutEvent(self, ev):        self.restoreCursor()    def isVisible(self, shape):        return self.visible.get(shape, True)    def drawing(self):        return self.mode == self.CREATE    def editing(self):        return self.mode == self.EDIT    def setEditing(self, value=True):        self.mode = self.EDIT if value else self.CREATE        if not value:  # Create            self.unHighlight()            self.deSelectShape()    def unHighlight(self):        if self.hShape:            self.hShape.highlightClear()            self.update()        self.prevhShape = self.hShape        self.prevhVertex = self.hVertex        self.prevhEdge = self.hEdge        self.hShape = self.hVertex = self.hEdge = None    def selectedVertex(self):        return self.hVertex is not None    def selectedEdge(self):        return self.hEdge is not None    def mouseMoveEvent(self, ev):        """Update line with last point and current coordinates."""        try:            if QT5:                pos = self.transformPos(ev.localPos())            else:                pos = self.transformPos(ev.posF())        except AttributeError:            return        self.prevMovePoint = pos        self.restoreCursor()        # Polygon drawing.        if self.drawing():            self.line.shape_type = self.createMode            self.overrideCursor(CURSOR_DRAW)            if not self.current:                return            if self.outOfPixmap(pos):                # Don't allow the user to draw outside the pixmap.                # Project the point to the pixmap's edges.                pos = self.intersectionPoint(self.current[-1], pos)            elif (                self.snapping                and len(self.current) > 1                and self.createMode == "polygon"                and self.closeEnough(pos, self.current[0])            ):                # Attract line to starting point and                # colorise to alert the user.                pos = self.current[0]                self.overrideCursor(CURSOR_POINT)                self.current.highlightVertex(0, Shape.NEAR_VERTEX)            if self.createMode in ["polygon", "linestrip"]:                self.line[0] = self.current[-1]                self.line[1] = pos            elif self.createMode == "rectangle":                self.line.points = [self.current[0], pos]                self.line.close()            elif self.createMode == "circle":                self.line.points = [self.current[0], pos]                self.line.shape_type = "circle"            elif self.createMode == "line":                self.line.points = [self.current[0], pos]                self.line.close()            elif self.createMode == "point":                self.line.points = [self.current[0]]                self.line.close()            self.repaint()            self.current.highlightClear()            return        # Polygon copy moving.        if QtCore.Qt.RightButton & ev.buttons():            if self.selectedShapesCopy and self.prevPoint:                self.overrideCursor(CURSOR_MOVE)                self.boundedMoveShapes(self.selectedShapesCopy, pos)                self.repaint()            elif self.selectedShapes:                self.selectedShapesCopy = [                    s.copy() for s in self.selectedShapes                ]                self.repaint()            return        # Polygon/Vertex moving.        if QtCore.Qt.LeftButton & ev.buttons():            if self.selectedVertex():                self.boundedMoveVertex(pos)                self.repaint()                self.movingShape = True            elif self.selectedShapes and self.prevPoint:                self.overrideCursor(CURSOR_MOVE)                self.boundedMoveShapes(self.selectedShapes, pos)                self.repaint()                self.movingShape = True            return        # Just hovering over the canvas, 2 possibilities:        # - Highlight shapes        # - Highlight vertex        # Update shape/vertex fill and tooltip value accordingly.        self.setToolTip(self.tr("Image"))        for shape in reversed([s for s in self.shapes if self.isVisible(s)]):            # Look for a nearby vertex to highlight. If that fails,            # check if we happen to be inside a shape.            index = shape.nearestVertex(pos, self.epsilon / self.scale)            index_edge = shape.nearestEdge(pos, self.epsilon / self.scale)            if index is not None:                if self.selectedVertex():                    self.hShape.highlightClear()                self.prevhVertex = self.hVertex = index                self.prevhShape = self.hShape = shape                self.prevhEdge = self.hEdge                self.hEdge = None                shape.highlightVertex(index, shape.MOVE_VERTEX)                self.overrideCursor(CURSOR_POINT)                self.setToolTip(self.tr("Click & drag to move point"))                self.setStatusTip(self.toolTip())                self.update()                break            elif index_edge is not None and shape.canAddPoint():                if self.selectedVertex():                    self.hShape.highlightClear()                self.prevhVertex = self.hVertex                self.hVertex = None                self.prevhShape = self.hShape = shape                self.prevhEdge = self.hEdge = index_edge                self.overrideCursor(CURSOR_POINT)                self.setToolTip(self.tr("Click to create point"))                self.setStatusTip(self.toolTip())                self.update()                break            elif shape.containsPoint(pos):                if self.selectedVertex():                    self.hShape.highlightClear()                self.prevhVertex = self.hVertex                self.hVertex = None                self.prevhShape = self.hShape = shape                self.prevhEdge = self.hEdge                self.hEdge = None                self.setToolTip(                    self.tr("Click & drag to move shape '%s'") % shape.label                )                self.setStatusTip(self.toolTip())                self.overrideCursor(CURSOR_GRAB)                self.update()                break        else:  # Nothing found, clear highlights, reset state.            self.unHighlight()        self.vertexSelected.emit(self.hVertex is not None)    def addPointToEdge(self):        shape = self.prevhShape        index = self.prevhEdge        point = self.prevMovePoint        if shape is None or index is None or point is None:            return        shape.insertPoint(index, point)        shape.highlightVertex(index, shape.MOVE_VERTEX)        self.hShape = shape        self.hVertex = index        self.hEdge = None        self.movingShape = True    def removeSelectedPoint(self):        shape = self.prevhShape        index = self.prevhVertex        if shape is None or index is None:            return        shape.removePoint(index)        shape.highlightClear()        self.hShape = shape        self.prevhVertex = None        self.movingShape = True  # Save changes    def mousePressEvent(self, ev):        if QT5:            pos = self.transformPos(ev.localPos())        else:            pos = self.transformPos(ev.posF())        if ev.button() == QtCore.Qt.LeftButton:            if self.drawing():                if self.current:                    # Add point to existing shape.                    if self.createMode == "polygon":                        self.current.addPoint(self.line[1])                        self.line[0] = self.current[-1]                        if self.current.isClosed():                            self.finalise()                    elif self.createMode in ["rectangle", "circle", "line"]:                        assert len(self.current.points) == 1                        self.current.points = self.line.points                        self.finalise()                    elif self.createMode == "linestrip":                        self.current.addPoint(self.line[1])                        self.line[0] = self.current[-1]                        if int(ev.modifiers()) == QtCore.Qt.ControlModifier:                            self.finalise()                elif not self.outOfPixmap(pos):                    # Create new shape.                    self.current = Shape(shape_type=self.createMode)                    self.current.addPoint(pos)                    if self.createMode == "point":                        self.finalise()                    else:                        if self.createMode == "circle":                            self.current.shape_type = "circle"                        self.line.points = [pos, pos]                        self.setHiding()                        self.drawingPolygon.emit(True)                        self.update()            elif self.editing():                if self.selectedEdge():                    self.addPointToEdge()                elif (                    self.selectedVertex()                    and int(ev.modifiers()) == QtCore.Qt.ShiftModifier                ):                    # Delete point if: left-click + SHIFT on a point                    self.removeSelectedPoint()                group_mode = int(ev.modifiers()) == QtCore.Qt.ControlModifier                self.selectShapePoint(pos, multiple_selection_mode=group_mode)                self.prevPoint = pos                self.repaint()        elif ev.button() == QtCore.Qt.RightButton and self.editing():            group_mode = int(ev.modifiers()) == QtCore.Qt.ControlModifier            if not self.selectedShapes or (                self.hShape is not None                and self.hShape not in self.selectedShapes            ):                self.selectShapePoint(pos, multiple_selection_mode=group_mode)                self.repaint()            self.prevPoint = pos    def mouseReleaseEvent(self, ev):        if ev.button() == QtCore.Qt.RightButton:            menu = self.menus[len(self.selectedShapesCopy) > 0]            self.restoreCursor()            if (                not menu.exec_(self.mapToGlobal(ev.pos()))                and self.selectedShapesCopy            ):                # Cancel the move by deleting the shadow copy.                self.selectedShapesCopy = []                self.repaint()        elif ev.button() == QtCore.Qt.LeftButton:            if self.editing():                if (                    self.hShape is not None                    and self.hShapeIsSelected                    and not self.movingShape                ):                    self.selectionChanged.emit(                        [x for x in self.selectedShapes if x != self.hShape]                    )        if self.movingShape and self.hShape:            index = self.shapes.index(self.hShape)            if (                self.shapesBackups[-1][index].points                != self.shapes[index].points            ):                self.storeShapes()                self.shapeMoved.emit()            self.movingShape = False    def endMove(self, copy):        assert self.selectedShapes and self.selectedShapesCopy        assert len(self.selectedShapesCopy) == len(self.selectedShapes)        if copy:            for i, shape in enumerate(self.selectedShapesCopy):                self.shapes.append(shape)                self.selectedShapes[i].selected = False                self.selectedShapes[i] = shape        else:            for i, shape in enumerate(self.selectedShapesCopy):                self.selectedShapes[i].points = shape.points        self.selectedShapesCopy = []        self.repaint()        self.storeShapes()        return True    def hideBackroundShapes(self, value):        self.hideBackround = value        if self.selectedShapes:            # Only hide other shapes if there is a current selection.            # Otherwise the user will not be able to select a shape.            self.setHiding(True)            self.update()    def setHiding(self, enable=True):        self._hideBackround = self.hideBackround if enable else False    def canCloseShape(self):        return self.drawing() and self.current and len(self.current) > 2    def mouseDoubleClickEvent(self, ev):        # We need at least 4 points here, since the mousePress handler        # adds an extra one before this handler is called.        if (            self.double_click == "close"            and self.canCloseShape()            and len(self.current) > 3        ):            self.current.popPoint()            self.finalise()    def selectShapes(self, shapes):        self.setHiding()        self.selectionChanged.emit(shapes)        self.update()    def selectShapePoint(self, point, multiple_selection_mode):        """Select the first shape created which contains this point."""        if self.selectedVertex():  # A vertex is marked for selection.            index, shape = self.hVertex, self.hShape            shape.highlightVertex(index, shape.MOVE_VERTEX)        else:            for shape in reversed(self.shapes):                if self.isVisible(shape) and shape.containsPoint(point):                    self.calculateOffsets(shape, point)                    self.setHiding()                    if shape not in self.selectedShapes:                        if multiple_selection_mode:                            self.selectionChanged.emit(                                self.selectedShapes + [shape]                            )                        else:                            self.selectionChanged.emit([shape])                        self.hShapeIsSelected = False                    else:                        self.hShapeIsSelected = True                    return        self.deSelectShape()    def calculateOffsets(self, shape, point):        rect = shape.boundingRect()        x1 = rect.x() - point.x()        y1 = rect.y() - point.y()        x2 = (rect.x() + rect.width() - 1) - point.x()        y2 = (rect.y() + rect.height() - 1) - point.y()        self.offsets = QtCore.QPoint(x1, y1), QtCore.QPoint(x2, y2)    def boundedMoveVertex(self, pos):        index, shape = self.hVertex, self.hShape        point = shape[index]        if self.outOfPixmap(pos):            pos = self.intersectionPoint(point, pos)        shape.moveVertexBy(index, pos - point)    def boundedMoveShapes(self, shapes, pos):        if self.outOfPixmap(pos):            return False  # No need to move        o1 = pos + self.offsets[0]        if self.outOfPixmap(o1):            pos -= QtCore.QPoint(min(0, o1.x()), min(0, o1.y()))        o2 = pos + self.offsets[1]        if self.outOfPixmap(o2):            pos += QtCore.QPoint(                min(0, self.pixmap.width() - o2.x()),                min(0, self.pixmap.height() - o2.y()),            )        # XXX: The next line tracks the new position of the cursor        # relative to the shape, but also results in making it        # a bit "shaky" when nearing the border and allows it to        # go outside of the shape's area for some reason.        # self.calculateOffsets(self.selectedShapes, pos)        dp = pos - self.prevPoint        if dp:            for shape in shapes:                shape.moveBy(dp)            self.prevPoint = pos            return True        return False    def deSelectShape(self):        if self.selectedShapes:            self.setHiding(False)            self.selectionChanged.emit([])            self.hShapeIsSelected = False            self.update()    def deleteSelected(self):        deleted_shapes = []        if self.selectedShapes:            for shape in self.selectedShapes:                self.shapes.remove(shape)                deleted_shapes.append(shape)            self.storeShapes()            self.selectedShapes = []            self.update()        return deleted_shapes    def deleteShape(self, shape):        if shape in self.selectedShapes:            self.selectedShapes.remove(shape)        if shape in self.shapes:            self.shapes.remove(shape)        self.storeShapes()        self.update()    def duplicateSelectedShapes(self):        if self.selectedShapes:            self.selectedShapesCopy = [s.copy() for s in self.selectedShapes]            self.boundedShiftShapes(self.selectedShapesCopy)            self.endMove(copy=True)        return self.selectedShapes    def boundedShiftShapes(self, shapes):        # Try to move in one direction, and if it fails in another.        # Give up if both fail.        point = shapes[0][0]        offset = QtCore.QPoint(2.0, 2.0)        self.offsets = QtCore.QPoint(), QtCore.QPoint()        self.prevPoint = point        if not self.boundedMoveShapes(shapes, point - offset):            self.boundedMoveShapes(shapes, point + offset)    def paintEvent(self, event):        if not self.pixmap:            return super(Canvas, self).paintEvent(event)        p = self._painter        p.begin(self)        p.setRenderHint(QtGui.QPainter.Antialiasing)        p.setRenderHint(QtGui.QPainter.HighQualityAntialiasing)        p.setRenderHint(QtGui.QPainter.SmoothPixmapTransform)        p.scale(self.scale, self.scale)        p.translate(self.offsetToCenter())        p.drawPixmap(0, 0, self.pixmap)        Shape.scale = self.scale        for shape in self.shapes:            if (shape.selected or not self._hideBackround) and self.isVisible(                shape            ):                shape.fill = shape.selected or shape == self.hShape                shape.paint(p)        if self.current:            self.current.paint(p)            self.line.paint(p)        if self.selectedShapesCopy:            for s in self.selectedShapesCopy:                s.paint(p)        if (            self.fillDrawing()            and self.createMode == "polygon"            and self.current is not None            and len(self.current.points) >= 2        ):            drawing_shape = self.current.copy()            drawing_shape.addPoint(self.line[1])            drawing_shape.fill = True            drawing_shape.paint(p)        p.end()    def transformPos(self, point):        """Convert from widget-logical coordinates to painter-logical ones."""        return point / self.scale - self.offsetToCenter()    def offsetToCenter(self):        s = self.scale        area = super(Canvas, self).size()        w, h = self.pixmap.width() * s, self.pixmap.height() * s        aw, ah = area.width(), area.height()        x = (aw - w) / (2 * s) if aw > w else 0        y = (ah - h) / (2 * s) if ah > h else 0        return QtCore.QPoint(x, y)    def outOfPixmap(self, p):        w, h = self.pixmap.width(), self.pixmap.height()        return not (0 <= p.x() <= w - 1 and 0 <= p.y() <= h - 1)    def finalise(self):        assert self.current        self.current.close()        self.shapes.append(self.current)        self.storeShapes()        self.current = None        self.setHiding(False)        self.newShape.emit()        self.update()    def closeEnough(self, p1, p2):        # d = distance(p1 - p2)        # m = (p1-p2).manhattanLength()        # print "d %.2f, m %d, %.2f" % (d, m, d - m)        # divide by scale to allow more precision when zoomed in        return labelme.utils.distance(p1 - p2) < (self.epsilon / self.scale)    def intersectionPoint(self, p1, p2):        # Cycle through each image edge in clockwise fashion,        # and find the one intersecting the current line segment.        # http://paulbourke.net/geometry/lineline2d/        size = self.pixmap.size()        points = [            (0, 0),            (size.width() - 1, 0),            (size.width() - 1, size.height() - 1),            (0, size.height() - 1),        ]        # x1, y1 should be in the pixmap, x2, y2 should be out of the pixmap        x1 = min(max(p1.x(), 0), size.width() - 1)        y1 = min(max(p1.y(), 0), size.height() - 1)        x2, y2 = p2.x(), p2.y()        d, i, (x, y) = min(self.intersectingEdges((x1, y1), (x2, y2), points))        x3, y3 = points[i]        x4, y4 = points[(i + 1) % 4]        if (x, y) == (x1, y1):            # Handle cases where previous point is on one of the edges.            if x3 == x4:                return QtCore.QPoint(x3, min(max(0, y2), max(y3, y4)))            else:  # y3 == y4                return QtCore.QPoint(min(max(0, x2), max(x3, x4)), y3)        return QtCore.QPoint(x, y)    def intersectingEdges(self, point1, point2, points):        """Find intersecting edges.        For each edge formed by `points', yield the intersection        with the line segment `(x1,y1) - (x2,y2)`, if it exists.        Also return the distance of `(x2,y2)' to the middle of the        edge along with its index, so that the one closest can be chosen.        """        (x1, y1) = point1        (x2, y2) = point2        for i in range(4):            x3, y3 = points[i]            x4, y4 = points[(i + 1) % 4]            denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)            nua = (x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)            nub = (x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)            if denom == 0:                # This covers two cases:                #   nua == nub == 0: Coincident                #   otherwise: Parallel                continue            ua, ub = nua / denom, nub / denom            if 0 <= ua <= 1 and 0 <= ub <= 1:                x = x1 + ua * (x2 - x1)                y = y1 + ua * (y2 - y1)                m = QtCore.QPoint((x3 + x4) / 2, (y3 + y4) / 2)                d = labelme.utils.distance(m - QtCore.QPoint(x2, y2))                yield d, i, (x, y)    # These two, along with a call to adjustSize are required for the    # scroll area.    def sizeHint(self):        return self.minimumSizeHint()    def minimumSizeHint(self):        if self.pixmap:            return self.scale * self.pixmap.size()        return super(Canvas, self).minimumSizeHint()    def wheelEvent(self, ev):        if QT5:            mods = ev.modifiers()            delta = ev.angleDelta()            if QtCore.Qt.ControlModifier == int(mods):                # with Ctrl/Command key                # zoom                self.zoomRequest.emit(delta.y(), ev.pos())            else:                # scroll                self.scrollRequest.emit(delta.x(), QtCore.Qt.Horizontal)                self.scrollRequest.emit(delta.y(), QtCore.Qt.Vertical)        else:            if ev.orientation() == QtCore.Qt.Vertical:                mods = ev.modifiers()                if QtCore.Qt.ControlModifier == int(mods):                    # with Ctrl/Command key                    self.zoomRequest.emit(ev.delta(), ev.pos())                else:                    self.scrollRequest.emit(                        ev.delta(),                        QtCore.Qt.Horizontal                        if (QtCore.Qt.ShiftModifier == int(mods))                        else QtCore.Qt.Vertical,                    )            else:                self.scrollRequest.emit(ev.delta(), QtCore.Qt.Horizontal)        ev.accept()    def keyPressEvent(self, ev):        modifiers = ev.modifiers()        key = ev.key()        if key == QtCore.Qt.Key_Escape and self.current:            self.current = None            self.drawingPolygon.emit(False)            self.update()        elif key == QtCore.Qt.Key_Return and self.canCloseShape():            self.finalise()        elif modifiers == QtCore.Qt.AltModifier:            self.snapping = False    def keyReleaseEvent(self, ev):        modifiers = ev.modifiers()        if int(modifiers) == 0:            self.snapping = True    def setLastLabel(self, text, flags):        assert text        self.shapes[-1].label = text        self.shapes[-1].flags = flags        self.shapesBackups.pop()        self.storeShapes()        return self.shapes[-1]    def undoLastLine(self):        assert self.shapes        self.current = self.shapes.pop()        self.current.setOpen()        if self.createMode in ["polygon", "linestrip"]:            self.line.points = [self.current[-1], self.current[0]]        elif self.createMode in ["rectangle", "line", "circle"]:            self.current.points = self.current.points[0:1]        elif self.createMode == "point":            self.current = None        self.drawingPolygon.emit(True)    def undoLastPoint(self):        if not self.current or self.current.isClosed():            return        self.current.popPoint()        if len(self.current) > 0:            self.line[0] = self.current[-1]        else:            self.current = None            self.drawingPolygon.emit(False)        self.update()    def loadPixmap(self, pixmap, clear_shapes=True):        self.pixmap = pixmap        if clear_shapes:            self.shapes = []        self.update()    def loadShapes(self, shapes, replace=True):        if replace:            self.shapes = list(shapes)        else:            self.shapes.extend(shapes)        self.storeShapes()        self.current = None        self.hShape = None        self.hVertex = None        self.hEdge = None        self.update()    def setShapeVisible(self, shape, value):        self.visible[shape] = value        self.update()    def overrideCursor(self, cursor):        self.restoreCursor()        self._cursor = cursor        QtWidgets.QApplication.setOverrideCursor(cursor)    def restoreCursor(self):        QtWidgets.QApplication.restoreOverrideCursor()    def resetState(self):        self.restoreCursor()        self.pixmap = None        self.shapesBackups = []        self.update()
 |