From 667239b8b62b5e21a7ae1b7946371cca0e95b736 Mon Sep 17 00:00:00 2001 From: Rebecca Breu Date: Mon, 29 Mar 2021 13:56:47 +0200 Subject: [PATCH] Rework selection code part 1 --- beeref/fileio/sql.py | 2 + beeref/items.py | 1 + beeref/scene.py | 3 +- beeref/selection.py | 102 +++++++++++++++++++++++++++---------------- beeref/view.py | 13 +++--- tests/test_scene.py | 14 +++++- 6 files changed, 89 insertions(+), 46 deletions(-) diff --git a/beeref/fileio/sql.py b/beeref/fileio/sql.py index 2101b5e..87d4a96 100644 --- a/beeref/fileio/sql.py +++ b/beeref/fileio/sql.py @@ -43,6 +43,8 @@ def handle_sqlite_errors(func): func(self, *args, **kwargs) except sqlite3.Error as e: logger.exception(f'Error while reading/writing {self.filename}') + if self.progress: + self.progress.setValue(self.progress.maximum()) raise BeeFileIOError(msg=str(e), filename=self.filename) from e return wrapper diff --git a/beeref/items.py b/beeref/items.py index 430efec..d594f76 100644 --- a/beeref/items.py +++ b/beeref/items.py @@ -50,6 +50,7 @@ class BeePixmapItem(QtWidgets.QGraphicsPixmapItem): logger.debug(f'Setting scale for image "{self.filename}" to {factor}') super().setScale(factor) + SelectionItem.update_selection(self) def set_pos_center(self, x, y): """Sets the position using the item's center as the origin point.""" diff --git a/beeref/scene.py b/beeref/scene.py index 5348070..decc107 100644 --- a/beeref/scene.py +++ b/beeref/scene.py @@ -118,7 +118,8 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene): Items to be saved are items that have an save_id attribute. """ - return filter(lambda i: hasattr(i, 'save_id'), self.items()) + return filter(lambda i: hasattr(i, 'save_id'), + self.items(order=Qt.SortOrder.AscendingOrder)) def clear_save_ids(self): for item in self.items_for_save(): diff --git a/beeref/selection.py b/beeref/selection.py index cab0a9b..9d1aa67 100644 --- a/beeref/selection.py +++ b/beeref/selection.py @@ -13,6 +13,7 @@ # You should have received a copy of the GNU General Public License # along with BeeRef. If not, see . +from collections.abc import Iterable import logging from PyQt6 import QtCore, QtGui, QtWidgets @@ -27,9 +28,10 @@ logger = logging.getLogger('BeeRef') class SelectionItem(QtWidgets.QGraphicsItem): color = QtGui.QColor(116, 234, 231, 255) - LINE_WIDTH = 3 - HANDLE_SIZE = 10 # scaling handles - RESIZE_SIZE = 30 # area for scaling hover events + LINE_WIDTH = 4 + HANDLE_SIZE = 15 # scale handles + RESIZE_SIZE = 30 # area for scale hover events + ROTATE_SIZE = 30 # area for rotation hover events debug = False @@ -41,86 +43,102 @@ class SelectionItem(QtWidgets.QGraphicsItem): QtWidgets.QGraphicsItem.GraphicsItemFlags.ItemIsSelectable | QtWidgets.QGraphicsItem.GraphicsItemFlags.ItemIsMovable) self.scale_active = False + self.previous_scale = None + self.setZValue(1) @property def bottom_right_scale_bounds(self): - """The intercatable shape of the bottom right scale handle""" - bounds = self.parentItem().boundingRect() - pos = bounds.bottomRight() + """The interactable shape of the bottom right scale handle""" return QtCore.QRectF( - pos.x() - self.resize_size/2, - pos.y() - self.resize_size/2, + self.parentItem().width - self.resize_size/2, + self.parentItem().height - self.resize_size/2, self.resize_size, self.resize_size) + @property + def bottom_right_rotate_bounds(self): + """The interactable shape of the bottom right rotate handle""" + return QtCore.QRectF( + self.parentItem().width + self.resize_size / 2, + self.parentItem().height + self.resize_size / 2, + self.rotate_size, self.rotate_size) + def scale_with_view(self, value): - """The handles and line thickness should always stay the same size on + """The interactable areas should always stay the same size on the screen so we need to adjust the values according to the scale factor of the view.""" - scale = self.parentItem().scene().views()[0].get_scale() + scale = self.scene().views()[0].get_scale() return value / scale / self.parentItem().scale() def boundingRect(self): bounds = self.parentItem().boundingRect() + margin = self.resize_size / 2 + self.rotate_size return QtCore.QRectF( - bounds.topLeft().x() - self.resize_size / 2, - bounds.topLeft().y() - self.resize_size / 2, - bounds.bottomRight().x() + self.resize_size, - bounds.bottomRight().y() + self.resize_size) - - @property - def handle_size(self): - return self.scale_with_view(self.HANDLE_SIZE) + bounds.topLeft().x() - margin, + bounds.topLeft().y() - margin, + bounds.bottomRight().x() + 2 * margin, + bounds.bottomRight().y() + 2 * margin) @property def resize_size(self): return self.scale_with_view(self.RESIZE_SIZE) @property - def line_width(self): - return self.scale_with_view(self.LINE_WIDTH) + def rotate_size(self): + return self.scale_with_view(self.ROTATE_SIZE) def shape(self): path = QtGui.QPainterPath() path.addRect(self.bottom_right_scale_bounds) + path.addRect(self.bottom_right_rotate_bounds) return path - def draw_debug_rect(self, painter, rect): - pen = QtGui.QPen(QtGui.QColor('red')) - pen.setWidth(self.line_width / 2) - painter.setPen(pen) - painter.drawRect(rect) + def draw_debug_shape(self, painter, shape): + color = QtGui.QColor(0, 255, 0, 20) + if isinstance(shape, QtCore.QRectF): + painter.fillRect(shape, color) + else: + painter.fillPath(shape, color) + + def update_geometry(self): + current_scale = self.scale_with_view(1) + if current_scale != self.previous_scale: + logger.debug('Selection geometry update') + self.prepareGeometryChange() + self.update() + self.previous_scale = current_scale def paint(self, painter, option, widget): pen = QtGui.QPen(self.color) - pen.setWidth(self.line_width) + pen.setWidth(self.LINE_WIDTH) + pen.setCosmetic(True) painter.setPen(pen) # Draw the main selection rectangle - bounds = self.parentItem().boundingRect() - painter.drawRect(bounds) + painter.drawRect( + 0, 0, self.parentItem().width, self.parentItem().height) - single_select_mode = self.parentItem().scene().has_single_selection() + single_select_mode = self.scene().has_single_selection() self.setEnabled(single_select_mode) # If it's a single selection, draw the handles: if single_select_mode: - pos = bounds.bottomRight() - painter.fillRect(pos.x() - self.handle_size/2, - pos.y() - self.handle_size/2, - self.handle_size, - self.handle_size, - self.color) + pen.setWidth(self.HANDLE_SIZE) + painter.setPen(pen) + painter.drawPoint( + self.parentItem().width, self.parentItem().height) if self.debug: - self.draw_debug_rect(painter, self.boundingRect()) - self.draw_debug_rect(painter, self.bottom_right_scale_bounds) + self.draw_debug_shape(painter, self.boundingRect()) + self.draw_debug_shape(painter, self.shape()) def hoverMoveEvent(self, event): # In bottomright scale area? if self.bottom_right_scale_bounds.contains(event.pos()): self.setCursor(Qt.CursorShape.SizeFDiagCursor) + elif self.bottom_right_rotate_bounds.contains(event.pos()): + self.setCursor(Qt.CursorShape.ForbiddenCursor) else: self.setCursor(Qt.CursorShape.ArrowCursor) @@ -141,7 +159,7 @@ class SelectionItem(QtWidgets.QGraphicsItem): self.parentItem().setScale(self.orig_scale_factor + delta) def mouseReleaseEvent(self, event): - self.parentItem().scene().undo_stack.push( + self.scene().undo_stack.push( commands.ScaleItemsBy(self.scene().selectedItems(), self.get_scale_delta(event), ignore_first_redo=True)) @@ -163,3 +181,11 @@ class SelectionItem(QtWidgets.QGraphicsItem): # Deleting them might have been the cause of segfaults when # they are in the middle of receiving events... item.childItems()[0].setVisible(False) + + @classmethod + def update_selection(cls, items): + if not isinstance(items, Iterable): + items = [items] + for item in items: + if item.childItems(): + item.childItems()[0].update_geometry() diff --git a/beeref/view.py b/beeref/view.py index 0ccc50d..86ef7b3 100644 --- a/beeref/view.py +++ b/beeref/view.py @@ -23,7 +23,7 @@ from beeref import fileio from beeref.gui import BeeProgressDialog, WelcomeOverlay from beeref.items import BeePixmapItem from beeref.scene import BeeGraphicsScene - +from beeref import selection logger = logging.getLogger('BeeRef') @@ -317,6 +317,10 @@ class BeeGraphicsView(QtWidgets.QGraphicsView): for i, filename in enumerate(filenames): logger.info(f'Loading image from file {filename}') img = QtGui.QImage(filename) + if progress: + progress.setValue(i) + if progress.wasCanceled(): + break if img.isNull(): errors.append(filename) continue @@ -325,10 +329,6 @@ class BeeGraphicsView(QtWidgets.QGraphicsView): items.append(item) pos.setX(pos.x() + 50) pos.setY(pos.y() + 50) - if progress: - progress.setValue(i) - if progress.wasCanceled(): - break self.undo_stack.push(commands.InsertItems(self.scene, items)) @@ -362,7 +362,7 @@ class BeeGraphicsView(QtWidgets.QGraphicsView): len(self.scene.selectedItems())) for action in self.actions_active_when_selection: action.setEnabled(self.scene.has_selection()) - self.viewport().repaint() + selection.SelectionItem.update_selection(self.scene.selectedItems()) def recalc_scene_rect(self): """Resize the scene rectangle so that it is always one view width @@ -408,6 +408,7 @@ class BeeGraphicsView(QtWidgets.QGraphicsView): def scale(self, *args, **kwargs): super().scale(*args, **kwargs) self.recalc_scene_rect() + selection.SelectionItem.update_selection(self.scene.selectedItems()) def get_scale(self): return self.transform().m11() diff --git a/tests/test_scene.py b/tests/test_scene.py index a36e1e7..30b753e 100644 --- a/tests/test_scene.py +++ b/tests/test_scene.py @@ -63,8 +63,20 @@ class BeeGraphicsSceneNormalizeTestCase(BeeTestCase): item2 = BeePixmapItem(QtGui.QImage()) self.scene.addItem(item2) item3 = QtWidgets.QGraphicsRectItem() - self.scene.clear_save_ids() self.scene.addItem(item3) + + self.scene.clear_save_ids() assert item1.save_id is None assert item2.save_id is None assert hasattr(item3, 'save_id') is False + + def test_items_for_save(self): + item1 = BeePixmapItem(QtGui.QImage()) + self.scene.addItem(item1) + item2 = BeePixmapItem(QtGui.QImage()) + self.scene.addItem(item2) + item3 = QtWidgets.QGraphicsRectItem() + self.scene.addItem(item3) + + items = list(self.scene.items_for_save()) + assert items == [item1, item2]