The use case is adjusting shapes by moving their vertexes, even if they are adjacent or overlapping another shape. Currently always the "later drawn" shape wins, making it impossible to adjust an earlier shape without re-creating it. Adjusting existing shapes is common for review scenarios. I have experimented with preferring the highlighted shape, but I found it finicky and less clear as to how the vertex preference works. JOSM (and OpenStreetMap editor) also uses the "prefer selected" approach and it works well there.
746 lines
28 KiB
Python
746 lines
28 KiB
Python
|
|
try:
|
|
from PyQt5.QtGui import *
|
|
from PyQt5.QtCore import *
|
|
from PyQt5.QtWidgets import *
|
|
except ImportError:
|
|
from PyQt4.QtGui import *
|
|
from PyQt4.QtCore import *
|
|
|
|
# from PyQt4.QtOpenGL import *
|
|
|
|
from libs.shape import Shape
|
|
from libs.utils import distance
|
|
|
|
CURSOR_DEFAULT = Qt.ArrowCursor
|
|
CURSOR_POINT = Qt.PointingHandCursor
|
|
CURSOR_DRAW = Qt.CrossCursor
|
|
CURSOR_MOVE = Qt.ClosedHandCursor
|
|
CURSOR_GRAB = Qt.OpenHandCursor
|
|
|
|
# class Canvas(QGLWidget):
|
|
|
|
|
|
class Canvas(QWidget):
|
|
zoomRequest = pyqtSignal(int)
|
|
lightRequest = pyqtSignal(int)
|
|
scrollRequest = pyqtSignal(int, int)
|
|
newShape = pyqtSignal()
|
|
selectionChanged = pyqtSignal(bool)
|
|
shapeMoved = pyqtSignal()
|
|
drawingPolygon = pyqtSignal(bool)
|
|
|
|
CREATE, EDIT = list(range(2))
|
|
|
|
epsilon = 24.0
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(Canvas, self).__init__(*args, **kwargs)
|
|
# Initialise local state.
|
|
self.mode = self.EDIT
|
|
self.shapes = []
|
|
self.current = None
|
|
self.selected_shape = None # save the selected shape here
|
|
self.selected_shape_copy = None
|
|
self.drawing_line_color = QColor(0, 0, 255)
|
|
self.drawing_rect_color = QColor(0, 0, 255)
|
|
self.line = Shape(line_color=self.drawing_line_color)
|
|
self.prev_point = QPointF()
|
|
self.offsets = QPointF(), QPointF()
|
|
self.scale = 1.0
|
|
self.overlay_color = None
|
|
self.label_font_size = 8
|
|
self.pixmap = QPixmap()
|
|
self.visible = {}
|
|
self._hide_background = False
|
|
self.hide_background = False
|
|
self.h_shape = None
|
|
self.h_vertex = None
|
|
self._painter = QPainter()
|
|
self._cursor = CURSOR_DEFAULT
|
|
# Menus:
|
|
self.menus = (QMenu(), QMenu())
|
|
# Set widget options.
|
|
self.setMouseTracking(True)
|
|
self.setFocusPolicy(Qt.WheelFocus)
|
|
self.verified = False
|
|
self.draw_square = False
|
|
|
|
# initialisation for panning
|
|
self.pan_initial_pos = QPoint()
|
|
|
|
def set_drawing_color(self, qcolor):
|
|
self.drawing_line_color = qcolor
|
|
self.drawing_rect_color = qcolor
|
|
|
|
def enterEvent(self, ev):
|
|
self.override_cursor(self._cursor)
|
|
|
|
def leaveEvent(self, ev):
|
|
self.restore_cursor()
|
|
|
|
def focusOutEvent(self, ev):
|
|
self.restore_cursor()
|
|
|
|
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 set_editing(self, value=True):
|
|
self.mode = self.EDIT if value else self.CREATE
|
|
if not value: # Create
|
|
self.un_highlight()
|
|
self.de_select_shape()
|
|
self.prev_point = QPointF()
|
|
self.repaint()
|
|
|
|
def un_highlight(self, shape=None):
|
|
if shape == None or shape == self.h_shape:
|
|
if self.h_shape:
|
|
self.h_shape.highlight_clear()
|
|
self.h_vertex = self.h_shape = None
|
|
|
|
def selected_vertex(self):
|
|
return self.h_vertex is not None
|
|
|
|
def mouseMoveEvent(self, ev):
|
|
"""Update line with last point and current coordinates."""
|
|
pos = self.transform_pos(ev.pos())
|
|
|
|
# Update coordinates in status bar if image is opened
|
|
window = self.parent().window()
|
|
if window.file_path is not None:
|
|
self.parent().window().label_coordinates.setText(
|
|
'X: %d; Y: %d' % (pos.x(), pos.y()))
|
|
|
|
# Polygon drawing.
|
|
if self.drawing():
|
|
self.override_cursor(CURSOR_DRAW)
|
|
if self.current:
|
|
# Display annotation width and height while drawing
|
|
current_width = abs(self.current[0].x() - pos.x())
|
|
current_height = abs(self.current[0].y() - pos.y())
|
|
self.parent().window().label_coordinates.setText(
|
|
'Width: %d, Height: %d / X: %d; Y: %d' % (current_width, current_height, pos.x(), pos.y()))
|
|
|
|
color = self.drawing_line_color
|
|
if self.out_of_pixmap(pos):
|
|
# Don't allow the user to draw outside the pixmap.
|
|
# Clip the coordinates to 0 or max,
|
|
# if they are outside the range [0, max]
|
|
size = self.pixmap.size()
|
|
clipped_x = min(max(0, pos.x()), size.width())
|
|
clipped_y = min(max(0, pos.y()), size.height())
|
|
pos = QPointF(clipped_x, clipped_y)
|
|
elif len(self.current) > 1 and self.close_enough(pos, self.current[0]):
|
|
# Attract line to starting point and colorise to alert the
|
|
# user:
|
|
pos = self.current[0]
|
|
color = self.current.line_color
|
|
self.override_cursor(CURSOR_POINT)
|
|
self.current.highlight_vertex(0, Shape.NEAR_VERTEX)
|
|
|
|
if self.draw_square:
|
|
init_pos = self.current[0]
|
|
min_x = init_pos.x()
|
|
min_y = init_pos.y()
|
|
min_size = min(abs(pos.x() - min_x), abs(pos.y() - min_y))
|
|
direction_x = -1 if pos.x() - min_x < 0 else 1
|
|
direction_y = -1 if pos.y() - min_y < 0 else 1
|
|
self.line[1] = QPointF(min_x + direction_x * min_size, min_y + direction_y * min_size)
|
|
else:
|
|
self.line[1] = pos
|
|
|
|
self.line.line_color = color
|
|
self.prev_point = QPointF()
|
|
self.current.highlight_clear()
|
|
else:
|
|
self.prev_point = pos
|
|
self.repaint()
|
|
return
|
|
|
|
# Polygon copy moving.
|
|
if Qt.RightButton & ev.buttons():
|
|
if self.selected_shape_copy and self.prev_point:
|
|
self.override_cursor(CURSOR_MOVE)
|
|
self.bounded_move_shape(self.selected_shape_copy, pos)
|
|
self.repaint()
|
|
elif self.selected_shape:
|
|
self.selected_shape_copy = self.selected_shape.copy()
|
|
self.repaint()
|
|
return
|
|
|
|
# Polygon/Vertex moving.
|
|
if Qt.LeftButton & ev.buttons():
|
|
if self.selected_vertex():
|
|
self.bounded_move_vertex(pos)
|
|
self.shapeMoved.emit()
|
|
self.repaint()
|
|
|
|
# Display annotation width and height while moving vertex
|
|
point1 = self.h_shape[1]
|
|
point3 = self.h_shape[3]
|
|
current_width = abs(point1.x() - point3.x())
|
|
current_height = abs(point1.y() - point3.y())
|
|
self.parent().window().label_coordinates.setText(
|
|
'Width: %d, Height: %d / X: %d; Y: %d' % (current_width, current_height, pos.x(), pos.y()))
|
|
elif self.selected_shape and self.prev_point:
|
|
self.override_cursor(CURSOR_MOVE)
|
|
self.bounded_move_shape(self.selected_shape, pos)
|
|
self.shapeMoved.emit()
|
|
self.repaint()
|
|
|
|
# Display annotation width and height while moving shape
|
|
point1 = self.selected_shape[1]
|
|
point3 = self.selected_shape[3]
|
|
current_width = abs(point1.x() - point3.x())
|
|
current_height = abs(point1.y() - point3.y())
|
|
self.parent().window().label_coordinates.setText(
|
|
'Width: %d, Height: %d / X: %d; Y: %d' % (current_width, current_height, pos.x(), pos.y()))
|
|
else:
|
|
# pan
|
|
delta_x = pos.x() - self.pan_initial_pos.x()
|
|
delta_y = pos.y() - self.pan_initial_pos.y()
|
|
self.scrollRequest.emit(delta_x, Qt.Horizontal)
|
|
self.scrollRequest.emit(delta_y, Qt.Vertical)
|
|
self.update()
|
|
return
|
|
|
|
# Just hovering over the canvas, 2 possibilities:
|
|
# - Highlight shapes
|
|
# - Highlight vertex
|
|
# Update shape/vertex fill and tooltip value accordingly.
|
|
self.setToolTip("Image")
|
|
priority_list = self.shapes + ([self.selected_shape] if self.selected_shape else [])
|
|
for shape in reversed([s for s in priority_list 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.nearest_vertex(pos, self.epsilon)
|
|
if index is not None:
|
|
if self.selected_vertex():
|
|
self.h_shape.highlight_clear()
|
|
self.h_vertex, self.h_shape = index, shape
|
|
shape.highlight_vertex(index, shape.MOVE_VERTEX)
|
|
self.override_cursor(CURSOR_POINT)
|
|
self.setToolTip("Click & drag to move point")
|
|
self.setStatusTip(self.toolTip())
|
|
self.update()
|
|
break
|
|
elif shape.contains_point(pos):
|
|
if self.selected_vertex():
|
|
self.h_shape.highlight_clear()
|
|
self.h_vertex, self.h_shape = None, shape
|
|
self.setToolTip(
|
|
"Click & drag to move shape '%s'" % shape.label)
|
|
self.setStatusTip(self.toolTip())
|
|
self.override_cursor(CURSOR_GRAB)
|
|
self.update()
|
|
|
|
# Display annotation width and height while hovering inside
|
|
point1 = self.h_shape[1]
|
|
point3 = self.h_shape[3]
|
|
current_width = abs(point1.x() - point3.x())
|
|
current_height = abs(point1.y() - point3.y())
|
|
self.parent().window().label_coordinates.setText(
|
|
'Width: %d, Height: %d / X: %d; Y: %d' % (current_width, current_height, pos.x(), pos.y()))
|
|
break
|
|
else: # Nothing found, clear highlights, reset state.
|
|
if self.h_shape:
|
|
self.h_shape.highlight_clear()
|
|
self.update()
|
|
self.h_vertex, self.h_shape = None, None
|
|
self.override_cursor(CURSOR_DEFAULT)
|
|
|
|
def mousePressEvent(self, ev):
|
|
pos = self.transform_pos(ev.pos())
|
|
|
|
if ev.button() == Qt.LeftButton:
|
|
if self.drawing():
|
|
self.handle_drawing(pos)
|
|
else:
|
|
selection = self.select_shape_point(pos)
|
|
self.prev_point = pos
|
|
|
|
if selection is None:
|
|
# pan
|
|
QApplication.setOverrideCursor(QCursor(Qt.OpenHandCursor))
|
|
self.pan_initial_pos = pos
|
|
|
|
elif ev.button() == Qt.RightButton and self.editing():
|
|
self.select_shape_point(pos)
|
|
self.prev_point = pos
|
|
self.update()
|
|
|
|
def mouseReleaseEvent(self, ev):
|
|
if ev.button() == Qt.RightButton:
|
|
menu = self.menus[bool(self.selected_shape_copy)]
|
|
self.restore_cursor()
|
|
if not menu.exec_(self.mapToGlobal(ev.pos()))\
|
|
and self.selected_shape_copy:
|
|
# Cancel the move by deleting the shadow copy.
|
|
self.selected_shape_copy = None
|
|
self.repaint()
|
|
elif ev.button() == Qt.LeftButton and self.selected_shape:
|
|
if self.selected_vertex():
|
|
self.override_cursor(CURSOR_POINT)
|
|
else:
|
|
self.override_cursor(CURSOR_GRAB)
|
|
elif ev.button() == Qt.LeftButton:
|
|
pos = self.transform_pos(ev.pos())
|
|
if self.drawing():
|
|
self.handle_drawing(pos)
|
|
else:
|
|
# pan
|
|
QApplication.restoreOverrideCursor()
|
|
|
|
def end_move(self, copy=False):
|
|
assert self.selected_shape and self.selected_shape_copy
|
|
shape = self.selected_shape_copy
|
|
# del shape.fill_color
|
|
# del shape.line_color
|
|
if copy:
|
|
self.shapes.append(shape)
|
|
self.selected_shape.selected = False
|
|
self.selected_shape = shape
|
|
self.repaint()
|
|
else:
|
|
self.selected_shape.points = [p for p in shape.points]
|
|
self.selected_shape_copy = None
|
|
|
|
def hide_background_shapes(self, value):
|
|
self.hide_background = value
|
|
if self.selected_shape:
|
|
# Only hide other shapes if there is a current selection.
|
|
# Otherwise the user will not be able to select a shape.
|
|
self.set_hiding(True)
|
|
self.repaint()
|
|
|
|
def handle_drawing(self, pos):
|
|
if self.current and self.current.reach_max_points() is False:
|
|
init_pos = self.current[0]
|
|
min_x = init_pos.x()
|
|
min_y = init_pos.y()
|
|
target_pos = self.line[1]
|
|
max_x = target_pos.x()
|
|
max_y = target_pos.y()
|
|
self.current.add_point(QPointF(max_x, min_y))
|
|
self.current.add_point(target_pos)
|
|
self.current.add_point(QPointF(min_x, max_y))
|
|
self.finalise()
|
|
elif not self.out_of_pixmap(pos):
|
|
self.current = Shape()
|
|
self.current.add_point(pos)
|
|
self.line.points = [pos, pos]
|
|
self.set_hiding()
|
|
self.drawingPolygon.emit(True)
|
|
self.update()
|
|
|
|
def set_hiding(self, enable=True):
|
|
self._hide_background = self.hide_background if enable else False
|
|
|
|
def can_close_shape(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.can_close_shape() and len(self.current) > 3:
|
|
self.current.pop_point()
|
|
self.finalise()
|
|
|
|
def select_shape(self, shape):
|
|
self.de_select_shape()
|
|
shape.selected = True
|
|
self.selected_shape = shape
|
|
self.set_hiding()
|
|
self.selectionChanged.emit(True)
|
|
self.update()
|
|
|
|
def select_shape_point(self, point):
|
|
"""Select the first shape created which contains this point."""
|
|
self.de_select_shape()
|
|
if self.selected_vertex(): # A vertex is marked for selection.
|
|
index, shape = self.h_vertex, self.h_shape
|
|
shape.highlight_vertex(index, shape.MOVE_VERTEX)
|
|
self.select_shape(shape)
|
|
return self.h_vertex
|
|
for shape in reversed(self.shapes):
|
|
if self.isVisible(shape) and shape.contains_point(point):
|
|
self.select_shape(shape)
|
|
self.calculate_offsets(shape, point)
|
|
return self.selected_shape
|
|
return None
|
|
|
|
def calculate_offsets(self, shape, point):
|
|
rect = shape.bounding_rect()
|
|
x1 = rect.x() - point.x()
|
|
y1 = rect.y() - point.y()
|
|
x2 = (rect.x() + rect.width()) - point.x()
|
|
y2 = (rect.y() + rect.height()) - point.y()
|
|
self.offsets = QPointF(x1, y1), QPointF(x2, y2)
|
|
|
|
def snap_point_to_canvas(self, x, y):
|
|
"""
|
|
Moves a point x,y to within the boundaries of the canvas.
|
|
:return: (x,y,snapped) where snapped is True if x or y were changed, False if not.
|
|
"""
|
|
if x < 0 or x > self.pixmap.width() or y < 0 or y > self.pixmap.height():
|
|
x = max(x, 0)
|
|
y = max(y, 0)
|
|
x = min(x, self.pixmap.width())
|
|
y = min(y, self.pixmap.height())
|
|
return x, y, True
|
|
|
|
return x, y, False
|
|
|
|
def bounded_move_vertex(self, pos):
|
|
index, shape = self.h_vertex, self.h_shape
|
|
point = shape[index]
|
|
if self.out_of_pixmap(pos):
|
|
size = self.pixmap.size()
|
|
clipped_x = min(max(0, pos.x()), size.width())
|
|
clipped_y = min(max(0, pos.y()), size.height())
|
|
pos = QPointF(clipped_x, clipped_y)
|
|
|
|
if self.draw_square:
|
|
opposite_point_index = (index + 2) % 4
|
|
opposite_point = shape[opposite_point_index]
|
|
|
|
min_size = min(abs(pos.x() - opposite_point.x()), abs(pos.y() - opposite_point.y()))
|
|
direction_x = -1 if pos.x() - opposite_point.x() < 0 else 1
|
|
direction_y = -1 if pos.y() - opposite_point.y() < 0 else 1
|
|
shift_pos = QPointF(opposite_point.x() + direction_x * min_size - point.x(),
|
|
opposite_point.y() + direction_y * min_size - point.y())
|
|
else:
|
|
shift_pos = pos - point
|
|
|
|
shape.move_vertex_by(index, shift_pos)
|
|
|
|
left_index = (index + 1) % 4
|
|
right_index = (index + 3) % 4
|
|
left_shift = None
|
|
right_shift = None
|
|
if index % 2 == 0:
|
|
right_shift = QPointF(shift_pos.x(), 0)
|
|
left_shift = QPointF(0, shift_pos.y())
|
|
else:
|
|
left_shift = QPointF(shift_pos.x(), 0)
|
|
right_shift = QPointF(0, shift_pos.y())
|
|
shape.move_vertex_by(right_index, right_shift)
|
|
shape.move_vertex_by(left_index, left_shift)
|
|
|
|
def bounded_move_shape(self, shape, pos):
|
|
if self.out_of_pixmap(pos):
|
|
return False # No need to move
|
|
o1 = pos + self.offsets[0]
|
|
if self.out_of_pixmap(o1):
|
|
pos -= QPointF(min(0, o1.x()), min(0, o1.y()))
|
|
o2 = pos + self.offsets[1]
|
|
if self.out_of_pixmap(o2):
|
|
pos += QPointF(min(0, self.pixmap.width() - o2.x()),
|
|
min(0, self.pixmap.height() - o2.y()))
|
|
# 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. XXX
|
|
# self.calculateOffsets(self.selectedShape, pos)
|
|
dp = pos - self.prev_point
|
|
if dp:
|
|
shape.move_by(dp)
|
|
self.prev_point = pos
|
|
return True
|
|
return False
|
|
|
|
def de_select_shape(self):
|
|
if self.selected_shape:
|
|
self.selected_shape.selected = False
|
|
self.selected_shape = None
|
|
self.set_hiding(False)
|
|
self.selectionChanged.emit(False)
|
|
self.update()
|
|
|
|
def delete_selected(self):
|
|
if self.selected_shape:
|
|
shape = self.selected_shape
|
|
self.un_highlight(shape)
|
|
self.shapes.remove(self.selected_shape)
|
|
self.selected_shape = None
|
|
self.update()
|
|
return shape
|
|
|
|
def copy_selected_shape(self):
|
|
if self.selected_shape:
|
|
shape = self.selected_shape.copy()
|
|
self.de_select_shape()
|
|
self.shapes.append(shape)
|
|
shape.selected = True
|
|
self.selected_shape = shape
|
|
self.bounded_shift_shape(shape)
|
|
return shape
|
|
|
|
def bounded_shift_shape(self, shape):
|
|
# Try to move in one direction, and if it fails in another.
|
|
# Give up if both fail.
|
|
point = shape[0]
|
|
offset = QPointF(2.0, 2.0)
|
|
self.calculate_offsets(shape, point)
|
|
self.prev_point = point
|
|
if not self.bounded_move_shape(shape, point - offset):
|
|
self.bounded_move_shape(shape, point + offset)
|
|
|
|
def paintEvent(self, event):
|
|
if not self.pixmap:
|
|
return super(Canvas, self).paintEvent(event)
|
|
|
|
p = self._painter
|
|
p.begin(self)
|
|
p.setRenderHint(QPainter.Antialiasing)
|
|
p.setRenderHint(QPainter.HighQualityAntialiasing)
|
|
p.setRenderHint(QPainter.SmoothPixmapTransform)
|
|
|
|
p.scale(self.scale, self.scale)
|
|
p.translate(self.offset_to_center())
|
|
|
|
temp = self.pixmap
|
|
if self.overlay_color:
|
|
temp = QPixmap(self.pixmap)
|
|
painter = QPainter(temp)
|
|
painter.setCompositionMode(painter.CompositionMode_Overlay)
|
|
painter.fillRect(temp.rect(), self.overlay_color)
|
|
painter.end()
|
|
|
|
p.drawPixmap(0, 0, temp)
|
|
Shape.scale = self.scale
|
|
Shape.label_font_size = self.label_font_size
|
|
for shape in self.shapes:
|
|
if (shape.selected or not self._hide_background) and self.isVisible(shape):
|
|
shape.fill = shape.selected or shape == self.h_shape
|
|
shape.paint(p)
|
|
if self.current:
|
|
self.current.paint(p)
|
|
self.line.paint(p)
|
|
if self.selected_shape_copy:
|
|
self.selected_shape_copy.paint(p)
|
|
|
|
# Paint rect
|
|
if self.current is not None and len(self.line) == 2:
|
|
left_top = self.line[0]
|
|
right_bottom = self.line[1]
|
|
rect_width = right_bottom.x() - left_top.x()
|
|
rect_height = right_bottom.y() - left_top.y()
|
|
p.setPen(self.drawing_rect_color)
|
|
brush = QBrush(Qt.BDiagPattern)
|
|
p.setBrush(brush)
|
|
p.drawRect(int(left_top.x()), int(left_top.y()), int(rect_width), int(rect_height))
|
|
|
|
if self.drawing() and not self.prev_point.isNull() and not self.out_of_pixmap(self.prev_point):
|
|
p.setPen(QColor(0, 0, 0))
|
|
p.drawLine(int(self.prev_point.x()), 0, int(self.prev_point.x()), int(self.pixmap.height()))
|
|
p.drawLine(0, int(self.prev_point.y()), int(self.pixmap.width()), int(self.prev_point.y()))
|
|
|
|
self.setAutoFillBackground(True)
|
|
if self.verified:
|
|
pal = self.palette()
|
|
pal.setColor(self.backgroundRole(), QColor(184, 239, 38, 128))
|
|
self.setPalette(pal)
|
|
else:
|
|
pal = self.palette()
|
|
pal.setColor(self.backgroundRole(), QColor(232, 232, 232, 255))
|
|
self.setPalette(pal)
|
|
|
|
p.end()
|
|
|
|
def transform_pos(self, point):
|
|
"""Convert from widget-logical coordinates to painter-logical coordinates."""
|
|
return point / self.scale - self.offset_to_center()
|
|
|
|
def offset_to_center(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 QPointF(x, y)
|
|
|
|
def out_of_pixmap(self, p):
|
|
w, h = self.pixmap.width(), self.pixmap.height()
|
|
return not (0 <= p.x() <= w and 0 <= p.y() <= h)
|
|
|
|
def finalise(self):
|
|
assert self.current
|
|
if self.current.points[0] == self.current.points[-1]:
|
|
self.current = None
|
|
self.drawingPolygon.emit(False)
|
|
self.update()
|
|
return
|
|
|
|
self.current.close()
|
|
self.shapes.append(self.current)
|
|
self.current = None
|
|
self.set_hiding(False)
|
|
self.newShape.emit()
|
|
self.update()
|
|
|
|
def close_enough(self, p1, p2):
|
|
# d = distance(p1 - p2)
|
|
# m = (p1-p2).manhattanLength()
|
|
# print "d %.2f, m %d, %.2f" % (d, m, d - m)
|
|
return distance(p1 - p2) < self.epsilon
|
|
|
|
# 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):
|
|
qt_version = 4 if hasattr(ev, "delta") else 5
|
|
if qt_version == 4:
|
|
if ev.orientation() == Qt.Vertical:
|
|
v_delta = ev.delta()
|
|
h_delta = 0
|
|
else:
|
|
h_delta = ev.delta()
|
|
v_delta = 0
|
|
else:
|
|
delta = ev.angleDelta()
|
|
h_delta = delta.x()
|
|
v_delta = delta.y()
|
|
|
|
mods = ev.modifiers()
|
|
if int(Qt.ControlModifier) | int(Qt.ShiftModifier) == int(mods) and v_delta:
|
|
self.lightRequest.emit(v_delta)
|
|
elif Qt.ControlModifier == int(mods) and v_delta:
|
|
self.zoomRequest.emit(v_delta)
|
|
else:
|
|
v_delta and self.scrollRequest.emit(v_delta, Qt.Vertical)
|
|
h_delta and self.scrollRequest.emit(h_delta, Qt.Horizontal)
|
|
ev.accept()
|
|
|
|
def keyPressEvent(self, ev):
|
|
key = ev.key()
|
|
if key == Qt.Key_Escape and self.current:
|
|
print('ESC press')
|
|
self.current = None
|
|
self.drawingPolygon.emit(False)
|
|
self.update()
|
|
elif key == Qt.Key_Return and self.can_close_shape():
|
|
self.finalise()
|
|
elif key == Qt.Key_Left and self.selected_shape:
|
|
self.move_one_pixel('Left')
|
|
elif key == Qt.Key_Right and self.selected_shape:
|
|
self.move_one_pixel('Right')
|
|
elif key == Qt.Key_Up and self.selected_shape:
|
|
self.move_one_pixel('Up')
|
|
elif key == Qt.Key_Down and self.selected_shape:
|
|
self.move_one_pixel('Down')
|
|
|
|
def move_one_pixel(self, direction):
|
|
# print(self.selectedShape.points)
|
|
if direction == 'Left' and not self.move_out_of_bound(QPointF(-1.0, 0)):
|
|
# print("move Left one pixel")
|
|
self.selected_shape.points[0] += QPointF(-1.0, 0)
|
|
self.selected_shape.points[1] += QPointF(-1.0, 0)
|
|
self.selected_shape.points[2] += QPointF(-1.0, 0)
|
|
self.selected_shape.points[3] += QPointF(-1.0, 0)
|
|
elif direction == 'Right' and not self.move_out_of_bound(QPointF(1.0, 0)):
|
|
# print("move Right one pixel")
|
|
self.selected_shape.points[0] += QPointF(1.0, 0)
|
|
self.selected_shape.points[1] += QPointF(1.0, 0)
|
|
self.selected_shape.points[2] += QPointF(1.0, 0)
|
|
self.selected_shape.points[3] += QPointF(1.0, 0)
|
|
elif direction == 'Up' and not self.move_out_of_bound(QPointF(0, -1.0)):
|
|
# print("move Up one pixel")
|
|
self.selected_shape.points[0] += QPointF(0, -1.0)
|
|
self.selected_shape.points[1] += QPointF(0, -1.0)
|
|
self.selected_shape.points[2] += QPointF(0, -1.0)
|
|
self.selected_shape.points[3] += QPointF(0, -1.0)
|
|
elif direction == 'Down' and not self.move_out_of_bound(QPointF(0, 1.0)):
|
|
# print("move Down one pixel")
|
|
self.selected_shape.points[0] += QPointF(0, 1.0)
|
|
self.selected_shape.points[1] += QPointF(0, 1.0)
|
|
self.selected_shape.points[2] += QPointF(0, 1.0)
|
|
self.selected_shape.points[3] += QPointF(0, 1.0)
|
|
self.shapeMoved.emit()
|
|
self.repaint()
|
|
|
|
def move_out_of_bound(self, step):
|
|
points = [p1 + p2 for p1, p2 in zip(self.selected_shape.points, [step] * 4)]
|
|
return True in map(self.out_of_pixmap, points)
|
|
|
|
def set_last_label(self, text, line_color=None, fill_color=None):
|
|
assert text
|
|
self.shapes[-1].label = text
|
|
if line_color:
|
|
self.shapes[-1].line_color = line_color
|
|
|
|
if fill_color:
|
|
self.shapes[-1].fill_color = fill_color
|
|
|
|
return self.shapes[-1]
|
|
|
|
def undo_last_line(self):
|
|
assert self.shapes
|
|
self.current = self.shapes.pop()
|
|
self.current.set_open()
|
|
self.line.points = [self.current[-1], self.current[0]]
|
|
self.drawingPolygon.emit(True)
|
|
|
|
def reset_all_lines(self):
|
|
assert self.shapes
|
|
self.current = self.shapes.pop()
|
|
self.current.set_open()
|
|
self.line.points = [self.current[-1], self.current[0]]
|
|
self.drawingPolygon.emit(True)
|
|
self.current = None
|
|
self.drawingPolygon.emit(False)
|
|
self.update()
|
|
|
|
def load_pixmap(self, pixmap):
|
|
self.pixmap = pixmap
|
|
self.shapes = []
|
|
self.repaint()
|
|
|
|
def load_shapes(self, shapes):
|
|
self.shapes = list(shapes)
|
|
self.current = None
|
|
self.repaint()
|
|
|
|
def set_shape_visible(self, shape, value):
|
|
self.visible[shape] = value
|
|
self.repaint()
|
|
|
|
def current_cursor(self):
|
|
cursor = QApplication.overrideCursor()
|
|
if cursor is not None:
|
|
cursor = cursor.shape()
|
|
return cursor
|
|
|
|
def override_cursor(self, cursor):
|
|
self._cursor = cursor
|
|
if self.current_cursor() is None:
|
|
QApplication.setOverrideCursor(cursor)
|
|
else:
|
|
QApplication.changeOverrideCursor(cursor)
|
|
|
|
def restore_cursor(self):
|
|
QApplication.restoreOverrideCursor()
|
|
|
|
def reset_state(self):
|
|
self.restore_cursor()
|
|
self.pixmap = None
|
|
self.update()
|
|
|
|
def set_drawing_shape_to_square(self, status):
|
|
self.draw_square = status
|