canvas.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679
  1. from qtpy import QtCore
  2. from qtpy import QtGui
  3. from qtpy import QtWidgets
  4. from labelme.lib import distance
  5. from labelme import QT5
  6. from labelme.shape import Shape
  7. # TODO(unknown):
  8. # - [maybe] Find optimal epsilon value.
  9. CURSOR_DEFAULT = QtCore.Qt.ArrowCursor
  10. CURSOR_POINT = QtCore.Qt.PointingHandCursor
  11. CURSOR_DRAW = QtCore.Qt.CrossCursor
  12. CURSOR_MOVE = QtCore.Qt.ClosedHandCursor
  13. CURSOR_GRAB = QtCore.Qt.OpenHandCursor
  14. class Canvas(QtWidgets.QWidget):
  15. zoomRequest = QtCore.Signal(int, QtCore.QPoint)
  16. scrollRequest = QtCore.Signal(int, int)
  17. newShape = QtCore.Signal()
  18. selectionChanged = QtCore.Signal(bool)
  19. shapeMoved = QtCore.Signal()
  20. drawingPolygon = QtCore.Signal(bool)
  21. edgeSelected = QtCore.Signal(bool)
  22. CREATE, EDIT = 0, 1
  23. # polygon, rectangle, line, or point
  24. _createMode = 'polygon'
  25. def __init__(self, *args, **kwargs):
  26. self.epsilon = kwargs.pop('epsilon', 11.0)
  27. super(Canvas, self).__init__(*args, **kwargs)
  28. # Initialise local state.
  29. self.mode = self.EDIT
  30. self.shapes = []
  31. self.shapesBackups = []
  32. self.current = None
  33. self.selectedShape = None # save the selected shape here
  34. self.selectedShapeCopy = None
  35. self.lineColor = QtGui.QColor(0, 0, 255)
  36. # self.line represents:
  37. # - createMode == 'polygon': edge from last point to current
  38. # - createMode == 'rectangle': diagonal line of the rectangle
  39. # - createMode == 'line': the line
  40. # - createMode == 'point': the point
  41. self.line = Shape(line_color=self.lineColor)
  42. self.prevPoint = QtCore.QPoint()
  43. self.prevMovePoint = QtCore.QPoint()
  44. self.offsets = QtCore.QPoint(), QtCore.QPoint()
  45. self.scale = 1.0
  46. self.pixmap = QtGui.QPixmap()
  47. self.visible = {}
  48. self._hideBackround = False
  49. self.hideBackround = False
  50. self.hShape = None
  51. self.hVertex = None
  52. self.hEdge = None
  53. self.movingShape = False
  54. self._painter = QtGui.QPainter()
  55. self._cursor = CURSOR_DEFAULT
  56. # Menus:
  57. self.menus = (QtWidgets.QMenu(), QtWidgets.QMenu())
  58. # Set widget options.
  59. self.setMouseTracking(True)
  60. self.setFocusPolicy(QtCore.Qt.WheelFocus)
  61. @property
  62. def createMode(self):
  63. return self._createMode
  64. @createMode.setter
  65. def createMode(self, value):
  66. if value not in ['polygon', 'rectangle', 'line', 'point']:
  67. raise ValueError('Unsupported createMode: %s' % value)
  68. self._createMode = value
  69. def storeShapes(self):
  70. shapesBackup = []
  71. for shape in self.shapes:
  72. shapesBackup.append(shape.copy())
  73. if len(self.shapesBackups) >= 10:
  74. self.shapesBackups = self.shapesBackups[-9:]
  75. self.shapesBackups.append(shapesBackup)
  76. @property
  77. def isShapeRestorable(self):
  78. if len(self.shapesBackups) < 2:
  79. return False
  80. return True
  81. def restoreShape(self):
  82. if not self.isShapeRestorable:
  83. return
  84. self.shapesBackups.pop() # latest
  85. shapesBackup = self.shapesBackups.pop()
  86. self.shapes = shapesBackup
  87. self.storeShapes()
  88. self.repaint()
  89. def enterEvent(self, ev):
  90. self.overrideCursor(self._cursor)
  91. def leaveEvent(self, ev):
  92. self.restoreCursor()
  93. def focusOutEvent(self, ev):
  94. self.restoreCursor()
  95. def isVisible(self, shape):
  96. return self.visible.get(shape, True)
  97. def drawing(self):
  98. return self.mode == self.CREATE
  99. def editing(self):
  100. return self.mode == self.EDIT
  101. def setEditing(self, value=True):
  102. self.mode = self.EDIT if value else self.CREATE
  103. if not value: # Create
  104. self.unHighlight()
  105. self.deSelectShape()
  106. def unHighlight(self):
  107. if self.hShape:
  108. self.hShape.highlightClear()
  109. self.hVertex = self.hShape = None
  110. def selectedVertex(self):
  111. return self.hVertex is not None
  112. def mouseMoveEvent(self, ev):
  113. """Update line with last point and current coordinates."""
  114. if QT5:
  115. pos = self.transformPos(ev.pos())
  116. else:
  117. pos = self.transformPos(ev.posF())
  118. self.prevMovePoint = pos
  119. self.restoreCursor()
  120. # Polygon drawing.
  121. if self.drawing():
  122. self.overrideCursor(CURSOR_DRAW)
  123. if not self.current:
  124. return
  125. color = self.lineColor
  126. if self.outOfPixmap(pos):
  127. # Don't allow the user to draw outside the pixmap.
  128. # Project the point to the pixmap's edges.
  129. pos = self.intersectionPoint(self.current[-1], pos)
  130. elif len(self.current) > 1 and \
  131. self.closeEnough(pos, self.current[0]):
  132. # Attract line to starting point and
  133. # colorise to alert the user.
  134. pos = self.current[0]
  135. color = self.current.line_color
  136. self.overrideCursor(CURSOR_POINT)
  137. self.current.highlightVertex(0, Shape.NEAR_VERTEX)
  138. if self.createMode == 'polygon':
  139. self.line[0] = self.current[-1]
  140. self.line[1] = pos
  141. elif self.createMode == 'rectangle':
  142. self.line.points = list(self.getRectangleFromLine(
  143. (self.current[0], pos)
  144. ))
  145. self.line.close()
  146. elif self.createMode == 'line':
  147. self.line.points = [self.current[0], pos]
  148. self.line.close()
  149. elif self.createMode == 'point':
  150. self.line.points = [self.current[0]]
  151. self.line.close()
  152. self.line.line_color = color
  153. self.repaint()
  154. self.current.highlightClear()
  155. return
  156. # Polygon copy moving.
  157. if QtCore.Qt.RightButton & ev.buttons():
  158. if self.selectedShapeCopy and self.prevPoint:
  159. self.overrideCursor(CURSOR_MOVE)
  160. self.boundedMoveShape(self.selectedShapeCopy, pos)
  161. self.repaint()
  162. elif self.selectedShape:
  163. self.selectedShapeCopy = self.selectedShape.copy()
  164. self.repaint()
  165. return
  166. # Polygon/Vertex moving.
  167. self.movingShape = False
  168. if QtCore.Qt.LeftButton & ev.buttons():
  169. if self.selectedVertex():
  170. self.boundedMoveVertex(pos)
  171. self.repaint()
  172. self.movingShape = True
  173. elif self.selectedShape and self.prevPoint:
  174. self.overrideCursor(CURSOR_MOVE)
  175. self.boundedMoveShape(self.selectedShape, pos)
  176. self.repaint()
  177. self.movingShape = True
  178. return
  179. # Just hovering over the canvas, 2 posibilities:
  180. # - Highlight shapes
  181. # - Highlight vertex
  182. # Update shape/vertex fill and tooltip value accordingly.
  183. self.setToolTip("Image")
  184. for shape in reversed([s for s in self.shapes if self.isVisible(s)]):
  185. # Look for a nearby vertex to highlight. If that fails,
  186. # check if we happen to be inside a shape.
  187. index = shape.nearestVertex(pos, self.epsilon)
  188. index_edge = shape.nearestEdge(pos, self.epsilon)
  189. if index is not None:
  190. if self.selectedVertex():
  191. self.hShape.highlightClear()
  192. self.hVertex = index
  193. self.hShape = shape
  194. self.hEdge = index_edge
  195. shape.highlightVertex(index, shape.MOVE_VERTEX)
  196. self.overrideCursor(CURSOR_POINT)
  197. self.setToolTip("Click & drag to move point")
  198. self.setStatusTip(self.toolTip())
  199. self.update()
  200. break
  201. elif shape.containsPoint(pos):
  202. if self.selectedVertex():
  203. self.hShape.highlightClear()
  204. self.hVertex = None
  205. self.hShape = shape
  206. self.hEdge = index_edge
  207. self.setToolTip(
  208. "Click & drag to move shape '%s'" % shape.label)
  209. self.setStatusTip(self.toolTip())
  210. self.overrideCursor(CURSOR_GRAB)
  211. self.update()
  212. break
  213. else: # Nothing found, clear highlights, reset state.
  214. if self.hShape:
  215. self.hShape.highlightClear()
  216. self.update()
  217. self.hVertex, self.hShape, self.hEdge = None, None, None
  218. self.edgeSelected.emit(self.hEdge is not None)
  219. def addPointToEdge(self):
  220. if (self.hShape is None and
  221. self.hEdge is None and
  222. self.prevMovePoint is None):
  223. return
  224. shape = self.hShape
  225. index = self.hEdge
  226. point = self.prevMovePoint
  227. shape.insertPoint(index, point)
  228. shape.highlightVertex(index, shape.MOVE_VERTEX)
  229. self.hShape = shape
  230. self.hVertex = index
  231. self.hEdge = None
  232. def getRectangleFromLine(self, line):
  233. pt1 = line[0]
  234. pt3 = line[1]
  235. pt2 = QtCore.QPoint(pt3.x(), pt1.y())
  236. pt4 = QtCore.QPoint(pt1.x(), pt3.y())
  237. return pt1, pt2, pt3, pt4
  238. def mousePressEvent(self, ev):
  239. if QT5:
  240. pos = self.transformPos(ev.pos())
  241. else:
  242. pos = self.transformPos(ev.posF())
  243. if ev.button() == QtCore.Qt.LeftButton:
  244. if self.drawing():
  245. if self.current:
  246. # Add point to existing shape.
  247. if self.createMode == 'polygon':
  248. self.current.addPoint(self.line[1])
  249. self.line[0] = self.current[-1]
  250. if self.current.isClosed():
  251. self.finalise()
  252. elif self.createMode in ['rectangle', 'line']:
  253. assert len(self.current.points) == 1
  254. self.current.points = self.line.points
  255. self.finalise()
  256. elif not self.outOfPixmap(pos):
  257. # Create new shape.
  258. self.current = Shape()
  259. self.current.addPoint(pos)
  260. if self.createMode == 'point':
  261. self.finalise()
  262. else:
  263. self.line.points = [pos, pos]
  264. self.setHiding()
  265. self.drawingPolygon.emit(True)
  266. self.update()
  267. else:
  268. self.selectShapePoint(pos)
  269. self.prevPoint = pos
  270. self.repaint()
  271. elif ev.button() == QtCore.Qt.RightButton and self.editing():
  272. self.selectShapePoint(pos)
  273. self.prevPoint = pos
  274. self.repaint()
  275. def mouseReleaseEvent(self, ev):
  276. if ev.button() == QtCore.Qt.RightButton:
  277. menu = self.menus[bool(self.selectedShapeCopy)]
  278. self.restoreCursor()
  279. if not menu.exec_(self.mapToGlobal(ev.pos()))\
  280. and self.selectedShapeCopy:
  281. # Cancel the move by deleting the shadow copy.
  282. self.selectedShapeCopy = None
  283. self.repaint()
  284. elif ev.button() == QtCore.Qt.LeftButton and self.selectedShape:
  285. self.overrideCursor(CURSOR_GRAB)
  286. if self.movingShape:
  287. self.storeShapes()
  288. self.shapeMoved.emit()
  289. def endMove(self, copy=False):
  290. assert self.selectedShape and self.selectedShapeCopy
  291. shape = self.selectedShapeCopy
  292. # del shape.fill_color
  293. # del shape.line_color
  294. if copy:
  295. self.shapes.append(shape)
  296. self.selectedShape.selected = False
  297. self.selectedShape = shape
  298. self.repaint()
  299. else:
  300. shape.label = self.selectedShape.label
  301. self.deleteSelected()
  302. self.shapes.append(shape)
  303. self.storeShapes()
  304. self.selectedShapeCopy = None
  305. def hideBackroundShapes(self, value):
  306. self.hideBackround = value
  307. if self.selectedShape:
  308. # Only hide other shapes if there is a current selection.
  309. # Otherwise the user will not be able to select a shape.
  310. self.setHiding(True)
  311. self.repaint()
  312. def setHiding(self, enable=True):
  313. self._hideBackround = self.hideBackround if enable else False
  314. def canCloseShape(self):
  315. return self.drawing() and self.current and len(self.current) > 2
  316. def mouseDoubleClickEvent(self, ev):
  317. # We need at least 4 points here, since the mousePress handler
  318. # adds an extra one before this handler is called.
  319. if self.canCloseShape() and len(self.current) > 3:
  320. self.current.popPoint()
  321. self.finalise()
  322. def selectShape(self, shape):
  323. self.deSelectShape()
  324. shape.selected = True
  325. self.selectedShape = shape
  326. self.setHiding()
  327. self.selectionChanged.emit(True)
  328. self.update()
  329. def selectShapePoint(self, point):
  330. """Select the first shape created which contains this point."""
  331. self.deSelectShape()
  332. if self.selectedVertex(): # A vertex is marked for selection.
  333. index, shape = self.hVertex, self.hShape
  334. shape.highlightVertex(index, shape.MOVE_VERTEX)
  335. return
  336. for shape in reversed(self.shapes):
  337. if self.isVisible(shape) and shape.containsPoint(point):
  338. shape.selected = True
  339. self.selectedShape = shape
  340. self.calculateOffsets(shape, point)
  341. self.setHiding()
  342. self.selectionChanged.emit(True)
  343. return
  344. def calculateOffsets(self, shape, point):
  345. rect = shape.boundingRect()
  346. x1 = rect.x() - point.x()
  347. y1 = rect.y() - point.y()
  348. x2 = (rect.x() + rect.width()) - point.x()
  349. y2 = (rect.y() + rect.height()) - point.y()
  350. self.offsets = QtCore.QPoint(x1, y1), QtCore.QPoint(x2, y2)
  351. def boundedMoveVertex(self, pos):
  352. index, shape = self.hVertex, self.hShape
  353. point = shape[index]
  354. if self.outOfPixmap(pos):
  355. pos = self.intersectionPoint(point, pos)
  356. shape.moveVertexBy(index, pos - point)
  357. def boundedMoveShape(self, shape, pos):
  358. if self.outOfPixmap(pos):
  359. return False # No need to move
  360. o1 = pos + self.offsets[0]
  361. if self.outOfPixmap(o1):
  362. pos -= QtCore.QPoint(min(0, o1.x()), min(0, o1.y()))
  363. o2 = pos + self.offsets[1]
  364. if self.outOfPixmap(o2):
  365. pos += QtCore.QPoint(min(0, self.pixmap.width() - o2.x()),
  366. min(0, self.pixmap.height() - o2.y()))
  367. # XXX: The next line tracks the new position of the cursor
  368. # relative to the shape, but also results in making it
  369. # a bit "shaky" when nearing the border and allows it to
  370. # go outside of the shape's area for some reason.
  371. # self.calculateOffsets(self.selectedShape, pos)
  372. dp = pos - self.prevPoint
  373. if dp:
  374. shape.moveBy(dp)
  375. self.prevPoint = pos
  376. return True
  377. return False
  378. def deSelectShape(self):
  379. if self.selectedShape:
  380. self.selectedShape.selected = False
  381. self.selectedShape = None
  382. self.setHiding(False)
  383. self.selectionChanged.emit(False)
  384. self.update()
  385. def deleteSelected(self):
  386. if self.selectedShape:
  387. shape = self.selectedShape
  388. self.shapes.remove(self.selectedShape)
  389. self.storeShapes()
  390. self.selectedShape = None
  391. self.update()
  392. return shape
  393. def copySelectedShape(self):
  394. if self.selectedShape:
  395. shape = self.selectedShape.copy()
  396. self.deSelectShape()
  397. self.shapes.append(shape)
  398. self.storeShapes()
  399. shape.selected = True
  400. self.selectedShape = shape
  401. self.boundedShiftShape(shape)
  402. return shape
  403. def boundedShiftShape(self, shape):
  404. # Try to move in one direction, and if it fails in another.
  405. # Give up if both fail.
  406. point = shape[0]
  407. offset = QtCore.QPoint(2.0, 2.0)
  408. self.calculateOffsets(shape, point)
  409. self.prevPoint = point
  410. if not self.boundedMoveShape(shape, point - offset):
  411. self.boundedMoveShape(shape, point + offset)
  412. def paintEvent(self, event):
  413. if not self.pixmap:
  414. return super(Canvas, self).paintEvent(event)
  415. p = self._painter
  416. p.begin(self)
  417. p.setRenderHint(QtGui.QPainter.Antialiasing)
  418. p.setRenderHint(QtGui.QPainter.HighQualityAntialiasing)
  419. p.setRenderHint(QtGui.QPainter.SmoothPixmapTransform)
  420. p.scale(self.scale, self.scale)
  421. p.translate(self.offsetToCenter())
  422. p.drawPixmap(0, 0, self.pixmap)
  423. Shape.scale = self.scale
  424. for shape in self.shapes:
  425. if (shape.selected or not self._hideBackround) and \
  426. self.isVisible(shape):
  427. shape.fill = shape.selected or shape == self.hShape
  428. shape.paint(p)
  429. if self.current:
  430. self.current.paint(p)
  431. self.line.paint(p)
  432. if self.selectedShapeCopy:
  433. self.selectedShapeCopy.paint(p)
  434. p.end()
  435. def transformPos(self, point):
  436. """Convert from widget-logical coordinates to painter-logical ones."""
  437. return point / self.scale - self.offsetToCenter()
  438. def offsetToCenter(self):
  439. s = self.scale
  440. area = super(Canvas, self).size()
  441. w, h = self.pixmap.width() * s, self.pixmap.height() * s
  442. aw, ah = area.width(), area.height()
  443. x = (aw - w) / (2 * s) if aw > w else 0
  444. y = (ah - h) / (2 * s) if ah > h else 0
  445. return QtCore.QPoint(x, y)
  446. def outOfPixmap(self, p):
  447. w, h = self.pixmap.width(), self.pixmap.height()
  448. return not (0 <= p.x() <= w and 0 <= p.y() <= h)
  449. def finalise(self):
  450. assert self.current
  451. self.current.close()
  452. self.shapes.append(self.current)
  453. self.storeShapes()
  454. self.current = None
  455. self.setHiding(False)
  456. self.newShape.emit()
  457. self.update()
  458. def closeEnough(self, p1, p2):
  459. # d = distance(p1 - p2)
  460. # m = (p1-p2).manhattanLength()
  461. # print "d %.2f, m %d, %.2f" % (d, m, d - m)
  462. return distance(p1 - p2) < self.epsilon
  463. def intersectionPoint(self, p1, p2):
  464. # Cycle through each image edge in clockwise fashion,
  465. # and find the one intersecting the current line segment.
  466. # http://paulbourke.net/geometry/lineline2d/
  467. size = self.pixmap.size()
  468. points = [(0, 0),
  469. (size.width(), 0),
  470. (size.width(), size.height()),
  471. (0, size.height())]
  472. x1, y1 = p1.x(), p1.y()
  473. x2, y2 = p2.x(), p2.y()
  474. d, i, (x, y) = min(self.intersectingEdges((x1, y1), (x2, y2), points))
  475. x3, y3 = points[i]
  476. x4, y4 = points[(i + 1) % 4]
  477. if (x, y) == (x1, y1):
  478. # Handle cases where previous point is on one of the edges.
  479. if x3 == x4:
  480. return QtCore.QPoint(x3, min(max(0, y2), max(y3, y4)))
  481. else: # y3 == y4
  482. return QtCore.QPoint(min(max(0, x2), max(x3, x4)), y3)
  483. return QtCore.QPoint(x, y)
  484. def intersectingEdges(self, point1, point2, points):
  485. """Find intersecting edges.
  486. For each edge formed by `points', yield the intersection
  487. with the line segment `(x1,y1) - (x2,y2)`, if it exists.
  488. Also return the distance of `(x2,y2)' to the middle of the
  489. edge along with its index, so that the one closest can be chosen.
  490. """
  491. (x1, y1) = point1
  492. (x2, y2) = point2
  493. for i in range(4):
  494. x3, y3 = points[i]
  495. x4, y4 = points[(i + 1) % 4]
  496. denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)
  497. nua = (x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)
  498. nub = (x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)
  499. if denom == 0:
  500. # This covers two cases:
  501. # nua == nub == 0: Coincident
  502. # otherwise: Parallel
  503. continue
  504. ua, ub = nua / denom, nub / denom
  505. if 0 <= ua <= 1 and 0 <= ub <= 1:
  506. x = x1 + ua * (x2 - x1)
  507. y = y1 + ua * (y2 - y1)
  508. m = QtCore.QPoint((x3 + x4) / 2, (y3 + y4) / 2)
  509. d = distance(m - QtCore.QPoint(x2, y2))
  510. yield d, i, (x, y)
  511. # These two, along with a call to adjustSize are required for the
  512. # scroll area.
  513. def sizeHint(self):
  514. return self.minimumSizeHint()
  515. def minimumSizeHint(self):
  516. if self.pixmap:
  517. return self.scale * self.pixmap.size()
  518. return super(Canvas, self).minimumSizeHint()
  519. def wheelEvent(self, ev):
  520. if QT5:
  521. mods = ev.modifiers()
  522. delta = ev.angleDelta()
  523. if QtCore.Qt.ControlModifier == int(mods):
  524. # with Ctrl/Command key
  525. # zoom
  526. self.zoomRequest.emit(delta.y(), ev.pos())
  527. else:
  528. # scroll
  529. self.scrollRequest.emit(delta.x(), QtCore.Qt.Horizontal)
  530. self.scrollRequest.emit(delta.y(), QtCore.Qt.Vertical)
  531. else:
  532. if ev.orientation() == QtCore.Qt.Vertical:
  533. mods = ev.modifiers()
  534. if QtCore.Qt.ControlModifier == int(mods):
  535. # with Ctrl/Command key
  536. self.zoomRequest.emit(ev.delta(), ev.pos())
  537. else:
  538. self.scrollRequest.emit(
  539. ev.delta(),
  540. QtCore.Qt.Horizontal
  541. if (QtCore.Qt.ShiftModifier == int(mods))
  542. else QtCore.Qt.Vertical)
  543. else:
  544. self.scrollRequest.emit(ev.delta(), QtCore.Qt.Horizontal)
  545. ev.accept()
  546. def keyPressEvent(self, ev):
  547. key = ev.key()
  548. if key == QtCore.Qt.Key_Escape and self.current:
  549. self.current = None
  550. self.drawingPolygon.emit(False)
  551. self.update()
  552. elif key == QtCore.Qt.Key_Return and self.canCloseShape():
  553. self.finalise()
  554. def setLastLabel(self, text):
  555. assert text
  556. self.shapes[-1].label = text
  557. self.shapesBackups.pop()
  558. self.storeShapes()
  559. return self.shapes[-1]
  560. def undoLastLine(self):
  561. assert self.shapes
  562. self.current = self.shapes.pop()
  563. self.current.setOpen()
  564. if self.createMode == 'polygon':
  565. self.line.points = [self.current[-1], self.current[0]]
  566. elif self.createMode in ['rectangle', 'line']:
  567. self.current.points = self.current.points[0:1]
  568. elif self.createMode == 'point':
  569. self.current = None
  570. self.drawingPolygon.emit(True)
  571. def undoLastPoint(self):
  572. if not self.current or self.current.isClosed():
  573. return
  574. self.current.popPoint()
  575. if len(self.current) > 0:
  576. self.line[0] = self.current[-1]
  577. else:
  578. self.current = None
  579. self.drawingPolygon.emit(False)
  580. self.repaint()
  581. def loadPixmap(self, pixmap):
  582. self.pixmap = pixmap
  583. self.shapes = []
  584. self.repaint()
  585. def loadShapes(self, shapes):
  586. self.shapes = list(shapes)
  587. self.storeShapes()
  588. self.current = None
  589. self.repaint()
  590. def setShapeVisible(self, shape, value):
  591. self.visible[shape] = value
  592. self.repaint()
  593. def overrideCursor(self, cursor):
  594. self.restoreCursor()
  595. self._cursor = cursor
  596. QtWidgets.QApplication.setOverrideCursor(cursor)
  597. def restoreCursor(self):
  598. QtWidgets.QApplication.restoreOverrideCursor()
  599. def resetState(self):
  600. self.restoreCursor()
  601. self.pixmap = None
  602. self.shapesBackups = []
  603. self.update()