class SelectionScene(QGraphicsScene): selection_drawing_finished = pyqtSignal(QRectF) def __init__(self, scene_rect: QRectF, parent: QObject = None): super(SelectionScene, self).__init__(scene_rect, parent) rectangle_color = QColor(Qt.red) self.default_border_pen = QPen(Qt.red, 3, Qt.SolidLine, Qt.SquareCap, Qt.MiterJoin) self.default_fill_brush = QBrush(rectangle_color, Qt.BDiagPattern) self.new_selection_view: QGraphicsRectItem = None self.new_selection_origin: QPointF = None self.mode = EditorMode.DRAW_MODE def mousePressEvent(self, event: QGraphicsSceneMouseEvent): if self.mode == EditorMode.DRAW_MODE: self._handle_mouse_press_draw_mode(event) elif self.mode == EditorMode.MOVE_MODE: raise NotImplementedError("Move mode is not implemented!") def _handle_mouse_press_draw_mode(self, event: QGraphicsSceneMouseEvent): if event.button() == Qt.LeftButton and self.new_selection_view is None: self.new_selection_origin = event.scenePos() self._restrict_to_scene_space(self.new_selection_origin) scene_logger.info( f"Beginning to draw a new selection: " f"X={self.new_selection_origin.x()}, Y={self.new_selection_origin.y()}" ) self.new_selection_view = QGraphicsRectItem( self.new_selection_origin.x(), self.new_selection_origin.y(), 0, 0) self.new_selection_view.setPen(self.default_border_pen) self.new_selection_view.setBrush(self.default_fill_brush) self.addItem(self.new_selection_view) event.accept() def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent): if self.mode == EditorMode.DRAW_MODE: self._handle_mouse_move_draw_mode(event) elif self.mode == EditorMode.MOVE_MODE: raise NotImplementedError("Move mode is not implemented!") def _handle_mouse_move_draw_mode(self, event: QGraphicsSceneMouseEvent): point2 = event.scenePos() self._restrict_to_scene_space(point2) if self.new_selection_origin is None: scene_logger.warning("Move event: Selection origin is None!") event.accept() return rectangle = QRectF( QPointF(min(self.new_selection_origin.x(), point2.x()), min(self.new_selection_origin.y(), point2.y())), QPointF(max(self.new_selection_origin.x(), point2.x()), max(self.new_selection_origin.y(), point2.y()))) self.new_selection_view.setRect(rectangle) event.accept() def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent): if self.mode == EditorMode.DRAW_MODE: self._handle_mouse_release_draw_mode(event) elif self.mode == EditorMode.MOVE_MODE: raise NotImplementedError("Move mode is not implemented!") def _handle_mouse_release_draw_mode(self, event: QGraphicsSceneMouseEvent): self.new_selection_view: QGraphicsRectItem if event.button( ) == Qt.LeftButton and self.new_selection_view is not None: absolute_rectangle = self.new_selection_view.mapRectFromScene( self.new_selection_view.rect()) if self.is_rectangle_valid_selection(absolute_rectangle): self.selection_drawing_finished.emit(absolute_rectangle) else: scene_logger.info( f"Discarding invalid selection: " f"x={absolute_rectangle.x()}, y={absolute_rectangle.y()}, " f"width={absolute_rectangle.width()}, height={absolute_rectangle.height()}" ) self.removeItem(self.new_selection_view) self.new_selection_origin = None self.new_selection_view = None event.accept() def load_selections(self, current: QModelIndex): selection_count: int = current.model().rowCount( current) # The number of child nodes, which are selections current_first_column = current.sibling( current.row(), 0) # Selections are below the first column selections: typing.List[Selection] = [ current_first_column.child(index, 0).data(Qt.UserRole) for index in range(selection_count) ] editor_logger.debug(f"Loading selection list: {selections}") for selection in selections: self._draw_rectangle(current, selection) def _restrict_to_scene_space(self, point: QPointF): """Restrict rectangle drawing to the screen space. This prevents drawing out of the source image bounds.""" point.setX(min(max(point.x(), 0), self.sceneRect().width())) point.setY(min(max(point.y(), 0), self.sceneRect().height())) def _draw_rectangle(self, current: QModelIndex, rectangle: Selection): self.addRect(self._to_local_coordinates(current, rectangle), self.default_border_pen, self.default_fill_brush) def _to_local_coordinates(self, current: QModelIndex, rectangle: Selection) -> QRectF: """ Scales a model Selection to local coordinates. Large images are scaled down, so the rectangles need to be scaled, too. This function performs the scaling and conversion to floating point based rectangles, as expected by QGraphicsView. """ scaling_factor: float = self.width() / current.sibling( current.row(), 0).data(Qt.UserRole).width if scaling_factor >= 1: result = rectangle.as_qrectf else: result = QRectF(rectangle.top_left.x * scaling_factor, rectangle.top_left.y * scaling_factor, rectangle.width * scaling_factor, rectangle.height * scaling_factor) scene_logger.debug( f"Scaled {rectangle} to {result.topLeft(), result.bottomRight()}" ) return result def is_rectangle_valid_selection(self, selection: QRectF) -> bool: """ Returns True, if the given rectangle is a valid selection. A selection is determined to be valid if its width and height is at least 0.5% of the source image or 20 pixels large, whichever comes first """ return (selection.width() > self.sceneRect().width() * 0.01 or selection.width() > 20) \ and (selection.height() > self.sceneRect().height() * 0.01 or selection.height() > 20)