From 511848146e55b916b717ddd635865e06893dbead Mon Sep 17 00:00:00 2001 From: Rebecca Breu Date: Fri, 2 Apr 2021 16:13:52 +0200 Subject: [PATCH] Add scaling for multiple items at once --- beeref/commands.py | 33 ++--- beeref/items.py | 289 ++++++++++++++++++++++++++--------------- beeref/scene.py | 42 +++++- tests/test_commands.py | 50 ++++--- tests/test_items.py | 59 ++++++--- 5 files changed, 321 insertions(+), 152 deletions(-) diff --git a/beeref/commands.py b/beeref/commands.py index 6de04bf..ae489b8 100644 --- a/beeref/commands.py +++ b/beeref/commands.py @@ -74,31 +74,34 @@ class MoveItemsBy(QtGui.QUndoCommand): item.moveBy(-self.delta_x, -self.delta_y) -class ScaleItemsByDelta(QtGui.QUndoCommand): - """Scale Items by a given delta around the given anchor point. - Delta will be *added* to the current scale factor.""" +class ScaleItemsBy(QtGui.QUndoCommand): + """Scale items by a given factor around the given anchor point.""" - def __init__(self, items, delta, anchor, ignore_first_redo=False): + def __init__(self, items, factor, ignore_first_redo=False): super().__init__('Scale items') - self.items = items - self.delta = delta - self.anchor = anchor self.ignore_first_redo = ignore_first_redo + self.items = items + self.factor = factor + self.item_data = [ + {'anchor': item.scale_anchor, + 'orig_factor': item.scale_orig_factor, + 'orig_pos': item.scale_orig_pos} for item in items] def redo(self): if self.ignore_first_redo: self.ignore_first_redo = False return - for item in self.items: - item.setScale(item.scale() + self.delta) - item.translate_for_scale_anchor( - item.pos(), self.delta, self.anchor) + for item, data in zip(self.items, self.item_data): + item.scale_orig_factors = data['orig_factor'] + item.scale_orig_pos = data['orig_pos'] + item.scale_anchor = data['anchor'] + item.setScale(item.scale() * self.factor) + item.translate_for_scale_anchor(self.factor) def undo(self): - for item in self.items: - item.setScale(item.scale() - self.delta) - item.translate_for_scale_anchor( - item.pos(), -self.delta, self.anchor) + for item, data in zip(self.items, self.item_data): + item.setScale(item.scale() / self.factor) + item.setPos(data['orig_pos']) class NormalizeItems(QtGui.QUndoCommand): diff --git a/beeref/items.py b/beeref/items.py index 0371079..e1f5a93 100644 --- a/beeref/items.py +++ b/beeref/items.py @@ -31,8 +31,8 @@ commandline_args = CommandlineArgs() logger = logging.getLogger('BeeRef') -class BeePixmapItem(QtWidgets.QGraphicsPixmapItem): - """Class for images added by the user.""" +class SelectableMixin: + """Common code for selectable items: Selection outline, handles etc.""" select_color = QtGui.QColor(116, 234, 231, 255) SELECT_LINE_WIDTH = 4 # line width for the selection box @@ -40,77 +40,19 @@ class BeePixmapItem(QtWidgets.QGraphicsPixmapItem): SELECT_RESIZE_SIZE = 20 # size of hover area for scaling SELECT_ROTATE_SIZE = 20 # size of hover area for rotating - def __init__(self, image, filename=None): - super().__init__(QtGui.QPixmap.fromImage(image)) - self.save_id = None - self.filename = filename - logger.debug(f'Initialized {self}') - + def init_selectable(self): self.setAcceptHoverEvents(True) self.setFlags( QtWidgets.QGraphicsItem.GraphicsItemFlags.ItemIsMovable | QtWidgets.QGraphicsItem.GraphicsItemFlags.ItemIsSelectable) - self.scale_active_corner = None + self.scale_active = False self.viewport_scale = 1 self.conf_debug_shapes = commandline_args.draw_debug_shapes - def __str__(self): - return (f'Image "{self.filename}" ' - f'with dimensions {self.width} x {self.height}') - - def setScale(self, factor): - if factor <= 0: - return - - logger.debug(f'Setting scale for image "{self.filename}" to {factor}') - self.prepareGeometryChange() - super().setScale(factor) - - def setZValue(self, value): - logger.debug(f'Setting z-value for image "{self.filename}" to {value}') - super().setZValue(value) - self.scene().max_z = max(self.scene().max_z, value) - def bring_to_front(self): self.setZValue(self.scene().max_z + 0.001) - def set_pos_center(self, x, y): - """Sets the position using the item's center as the origin point.""" - - self.setPos(x - self.width * self.scale() / 2, - y - self.height * self.scale() / 2) - - @property - def width(self): - return self.pixmap().size().width() - - @property - def height(self): - return self.pixmap().size().height() - - def itemChange(self, change, value): - if change == QGraphicsItem.GraphicsItemChange.ItemSelectedChange: - self.prepareGeometryChange() - if(value and self.scene() and not self.scene().has_selection()): - self.bring_to_front() - return super().itemChange(change, value) - - def pixmap_to_bytes(self): - """Convert the pixmap data to PNG bytestring.""" - barray = QtCore.QByteArray() - buffer = QtCore.QBuffer(barray) - buffer.open(QtCore.QIODevice.OpenMode.WriteOnly) - img = self.pixmap().toImage() - img.save(buffer, 'PNG') - return barray.data() - - def pixmap_from_bytes(self, data): - """Set image pimap from a bytestring.""" - pixmap = QtGui.QPixmap() - pixmap.loadFromData(data) - self.setPixmap(pixmap) - def fixed_length_for_viewport(self, value): """The interactable areas need to stay the same size on the screen so we need to adjust the values according to the scale @@ -140,14 +82,12 @@ class BeePixmapItem(QtWidgets.QGraphicsPixmapItem): else: painter.fillPath(shape, color) - def paint(self, painter, option, widget): - painter.drawPixmap(0, 0, self.pixmap()) - + def paint_selectable(self, painter, option, widget): if self.conf_debug_shapes: self.draw_debug_shape(painter, self.boundingRect(), 0, 255, 0) self.draw_debug_shape(painter, self.shape(), 255, 0, 0) - if not self.isSelected(): + if not self.has_selection_outline(): return pen = QtGui.QPen(self.select_color) @@ -158,10 +98,8 @@ class BeePixmapItem(QtWidgets.QGraphicsPixmapItem): # Draw the main selection rectangle painter.drawRect(0, 0, self.width, self.height) - single_select_mode = self.scene().has_single_selection() - # If it's a single selection, draw the handles: - if single_select_mode: + if self.has_selection_handles(): pen.setWidth(self.SELECT_HANDLE_SIZE) painter.setPen(pen) for corner in self.corners: @@ -193,7 +131,7 @@ class BeePixmapItem(QtWidgets.QGraphicsPixmapItem): def boundingRect(self): bounds = super().boundingRect() - if not self.isSelected(): + if not self.has_selection_outline(): return bounds # Add extra space for scale and rotate interactive areas @@ -206,7 +144,7 @@ class BeePixmapItem(QtWidgets.QGraphicsPixmapItem): def shape(self): shape_ = super().shape() - if self.isSelected(): + if self.has_selection_outline(): # Add extra space for scale and rotate interactive areas path = QtGui.QPainterPath() for corner in self.corners: @@ -216,7 +154,7 @@ class BeePixmapItem(QtWidgets.QGraphicsPixmapItem): return shape_ def hoverMoveEvent(self, event): - if not self.isSelected() or not self.scene().has_single_selection(): + if not self.has_selection_handles(): return for corner in self.corners: @@ -236,35 +174,40 @@ class BeePixmapItem(QtWidgets.QGraphicsPixmapItem): self.setCursor(Qt.CursorShape.ArrowCursor) def hoverEnterEvent(self, event): - # Always return regular cursor when item isn't selected - if not self.isSelected() or not self.scene().has_single_selection(): + # Always return regular cursor when there aren't any selection handles + if not self.has_selection_handles(): self.setCursor(Qt.CursorShape.ArrowCursor) def mousePressEvent(self, event): if (event.button() == Qt.MouseButtons.LeftButton - and self.isSelected() and self.scene().has_single_selection()): + and self.has_selection_handles()): for corner in self.corners: # Check if we are in one of the corner's scale areas if self.get_scale_bounds(corner).contains(event.pos()): # Start scale action for this corner - self.scale_active_corner = corner + self.scale_active = True self.scale_start = event.scenePos() - self.scale_orig_factor = self.scale() - self.scale_orig_pos = self.pos() + self.scale_direction = self.get_scale_direction(corner) + for item in self.selection_action_items(): + item.scale_anchor = self.get_scale_anchor(item, corner) + item.scale_orig_factor = item.scale() + item.scale_orig_pos = item.pos() event.accept() return super().mousePressEvent(event) - def get_scale_delta(self, event, corner): + def get_scale_factor(self, event): imgsize = self.width + self.height p = event.scenePos() - self.scale_start - direction = self.get_scale_direction(corner) - return (direction[0] * p.x() + direction[1] * p.y()) / imgsize + direction = self.scale_direction + delta = (direction[0] * p.x() + direction[1] * p.y()) / imgsize + return (self.scale_orig_factor + delta) / self.scale_orig_factor - def get_scale_anchor(self, corner): + def get_scale_anchor(self, item, corner): """Get the anchor around which the scale for this corner operates.""" - return(self.width - corner[0], self.height - corner[1]) + return item.mapFromScene( + self.mapToScene(self.width - corner[0], self.height - corner[1])) def get_scale_direction(self, corner): """Get the direction in which the scale for this corner increases""" @@ -272,41 +215,183 @@ class BeePixmapItem(QtWidgets.QGraphicsPixmapItem): y = 1 if corner[1] > 0 else -1 return (x, y) - def translate_for_scale_anchor(self, orig_pos, scale_delta, anchor): + def translate_for_scale_anchor(self, scale_factor): """Adjust the item's position so that a scale with the given scale - delta appears to operate around the given anchor. ``setScale`` needs - to be called separately with delta *added* to the current item's scale - factor.""" + factor appears to operate around the scale anchor. ``setScale`` + needs to be called separately with ``scale_factor`` multiplied by + the item's current scale factor. + """ + factor = self.scale_orig_factor * (scale_factor - 1) self.setPos( - orig_pos.x() - anchor[0] * scale_delta, - orig_pos.y() - anchor[1] * scale_delta, + self.scale_orig_pos.x() - self.scale_anchor.x() * factor, + self.scale_orig_pos.y() - self.scale_anchor.y() * factor, ) def mouseMoveEvent(self, event): - if self.scale_active_corner: - delta = self.get_scale_delta(event, self.scale_active_corner) - self.setScale(self.scale_orig_factor + delta) - self.translate_for_scale_anchor( - self.scale_orig_pos, - delta, - self.get_scale_anchor(self.scale_active_corner)) + if self.scale_active: + factor = self.get_scale_factor(event) + for item in self.selection_action_items(): + item.setScale(item.scale_orig_factor * factor) + item.translate_for_scale_anchor(factor) event.accept() else: super().mouseMoveEvent(event) def mouseReleaseEvent(self, event): - if self.scale_active_corner: + if self.scale_active: self.scene().undo_stack.push( - commands.ScaleItemsByDelta( - [self], - self.get_scale_delta(event, self.scale_active_corner), - self.get_scale_anchor(self.scale_active_corner), + commands.ScaleItemsBy( + self.selection_action_items(), + self.get_scale_factor(event), ignore_first_redo=True)) - self.scale_active_corner = None + self.scale_active = False event.accept() else: super().mouseReleaseEvent(event) def on_view_scale_change(self): self.prepareGeometryChange() + + def itemChange(self, change, value): + if change == QGraphicsItem.GraphicsItemChange.ItemSelectedChange: + self.prepareGeometryChange() + if hasattr(self, 'on_selected_change'): + self.on_selected_change(value) + return super().itemChange(change, value) + + +class BeePixmapItem(SelectableMixin, QtWidgets.QGraphicsPixmapItem): + """Class for images added by the user.""" + + def __init__(self, image, filename=None): + super().__init__(QtGui.QPixmap.fromImage(image)) + self.save_id = None + self.filename = filename + logger.debug(f'Initialized {self}') + self.init_selectable() + + def __str__(self): + return (f'Image "{self.filename}" ' + f'with dimensions {self.width} x {self.height}') + + def setScale(self, factor): + if factor <= 0: + return + + logger.debug(f'Setting scale for image "{self.filename}" to {factor}') + self.prepareGeometryChange() + super().setScale(factor) + + def setZValue(self, value): + logger.debug(f'Setting z-value for image "{self.filename}" to {value}') + super().setZValue(value) + self.scene().max_z = max(self.scene().max_z, value) + + def set_pos_center(self, x, y): + """Sets the position using the item's center as the origin point.""" + + self.setPos(x - self.width * self.scale() / 2, + y - self.height * self.scale() / 2) + + @property + def width(self): + return self.pixmap().size().width() + + @property + def height(self): + return self.pixmap().size().height() + + @property + def scene_bottomright(self): + """The bottom right corner in scene coordinates.""" + return self.mapToScene(QtCore.QPointF(self.width, self.height)) + + def pixmap_to_bytes(self): + """Convert the pixmap data to PNG bytestring.""" + barray = QtCore.QByteArray() + buffer = QtCore.QBuffer(barray) + buffer.open(QtCore.QIODevice.OpenMode.WriteOnly) + img = self.pixmap().toImage() + img.save(buffer, 'PNG') + return barray.data() + + def pixmap_from_bytes(self, data): + """Set image pimap from a bytestring.""" + pixmap = QtGui.QPixmap() + pixmap.loadFromData(data) + self.setPixmap(pixmap) + + def paint(self, painter, option, widget): + painter.drawPixmap(0, 0, self.pixmap()) + self.paint_selectable(painter, option, widget) + + def has_selection_outline(self): + return self.isSelected() + + def has_selection_handles(self): + return self.isSelected() and self.scene().has_single_selection() + + def selection_action_items(self): + """The items affected by selection actions like scaling and rotating. + """ + return [self] + + def on_selected_change(self, value): + if(value and self.scene() and not self.scene().has_selection()): + self.bring_to_front() + + +class MultiSelectItem(SelectableMixin, QtWidgets.QGraphicsRectItem): + """Class for images added by the user.""" + + def __init__(self): + super().__init__() + self.init_selectable() + + def __str__(self): + return (f'MultiSelectItem ' + f'with dimensions {self.width} x {self.height}') + + @property + def width(self): + return self.rect().width() + + @property + def height(self): + return self.rect().height() + + def paint(self, painter, option, widget): + self.paint_selectable(painter, option, widget) + + def has_selection_outline(self): + return True + + def has_selection_handles(self): + return True + + def selection_action_items(self): + """The items affected by selection actions like scaling and rotating. + """ + return list(self.scene().selectedItems()) + + def fit_selection_area(self, rect): + """Updates itself to fit the given selection area.""" + + logging.debug(f'Fit selection area to {rect}') + self.setRect(0, 0, rect.width(), rect.height()) + self.setPos(rect.topLeft()) + self.setScale(1) + self.setRotation(0) + self.setSelected(True) + + def mousePressEvent(self, event): + if (event.button() == Qt.MouseButtons.LeftButton + and event.modifiers() == Qt.KeyboardModifiers.ControlModifier): + # We still need to be able to select additional images + # within/"under" the multi select rectangle, so let ctrl+click + # events pass through + event.ignore() + return + + super().mousePressEvent(event) diff --git a/beeref/scene.py b/beeref/scene.py index 341a919..3f87817 100644 --- a/beeref/scene.py +++ b/beeref/scene.py @@ -16,10 +16,12 @@ import logging import math -from PyQt6 import QtWidgets +from PyQt6 import QtCore, QtWidgets from PyQt6.QtCore import Qt from beeref import commands +from beeref.items import MultiSelectItem + logger = logging.getLogger('BeeRef') @@ -31,6 +33,8 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene): self.move_active = False self.undo_stack = undo_stack self.max_z = 0 + self.multi_select_item = MultiSelectItem() + self.selectionChanged.connect(self.on_selection_change) def normalize_width_or_height(self, mode): """Scale the selected images to have the same width or height, as @@ -89,6 +93,11 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene): return len(self.selectedItems()) == 1 + def has_multi_selection(self): + """Checks whether there are currently more than one items selected.""" + + return len(self.selectedItems()) > 1 + def mousePressEvent(self, event): if event.button() == Qt.MouseButtons.RightButton: # Right-click invokes the context menu on the @@ -129,3 +138,34 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene): def on_view_scale_change(self): for item in self.selectedItems(): item.on_view_scale_change() + + def get_selection_rect(self): + """Returns the bounding rect of the currently selected items.""" + + items = list(filter(lambda i: hasattr(i, 'save_id'), + self.selectedItems())) + + topleft = items[0].pos() + bottomright = items[0].scene_bottomright + + for item in items[1:]: + tl = item.pos() + br = item.scene_bottomright + topleft.setX(min(topleft.x(), tl.x(), br.x())) + topleft.setY(min(topleft.y(), tl.y(), br.y())) + bottomright.setX(max(bottomright.x(), tl.x(), br.x())) + bottomright.setY(max(bottomright.y(), tl.y(), br.y())) + + return QtCore.QRectF(topleft, bottomright) + + def on_selection_change(self): + if self.has_multi_selection(): + self.multi_select_item.fit_selection_area( + self.get_selection_rect()) + if self.has_multi_selection() and not self.multi_select_item.scene(): + logger.debug('Adding multi select outline') + self.addItem(self.multi_select_item) + self.multi_select_item.bring_to_front() + if not self.has_multi_selection() and self.multi_select_item.scene(): + logger.debug('Removing multi select outline') + self.removeItem(self.multi_select_item) diff --git a/tests/test_commands.py b/tests/test_commands.py index 32f4eaf..e9f089a 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -1,6 +1,6 @@ from unittest.mock import MagicMock, patch -from PyQt6 import QtGui +from PyQt6 import QtCore, QtGui from beeref import commands from beeref.items import BeePixmapItem @@ -93,41 +93,59 @@ class ScaleItemsByTestCase(BeeTestCase): def test_redo_undo(self): item1 = BeePixmapItem(QtGui.QImage()) item1.setScale(1) + item1.scale_orig_factor = 1 + item1.scale_anchor = QtCore.QPointF(100, 100) + item1.scale_orig_pos = QtCore.QPointF(0, 0) item2 = BeePixmapItem(QtGui.QImage()) item2.setScale(3) - command = commands.ScaleItemsByDelta([item1, item2], 2, (100, 100)) + item2.scale_orig_factor = 3 + item2.scale_anchor = QtCore.QPointF(0, 0) + item2.setPos(100, 100) + item2.scale_orig_pos = QtCore.QPointF(100, 100) + command = commands.ScaleItemsBy([item1, item2], 2) command.redo() - assert item1.scale() == 3 - assert item1.pos().x() == -200 - assert item1.pos().y() == -200 - assert item2.scale() == 5 - assert item2.pos().x() == -200 - assert item2.pos().y() == -200 + assert item1.scale() == 2 + assert item1.pos().x() == -100 + assert item1.pos().y() == -100 + assert item2.scale() == 6 + assert item2.pos().x() == 100 + assert item2.pos().y() == 100 command.undo() assert item1.scale() == 1 assert item1.pos().x() == 0 assert item1.pos().y() == 0 assert item2.scale() == 3 - assert item2.pos().x() == 0 - assert item2.pos().y() == 0 + assert item2.pos().x() == 100 + assert item2.pos().y() == 100 def test_ignore_first_redo(self): item1 = BeePixmapItem(QtGui.QImage()) item1.setScale(1) + item1.scale_orig_factor = 1 + item1.scale_anchor = QtCore.QPointF(100, 100) + item1.scale_orig_pos = QtCore.QPointF(0, 0) item2 = BeePixmapItem(QtGui.QImage()) item2.setScale(3) - command = commands.ScaleItemsByDelta([item1, item2], 2, (100, 100), - ignore_first_redo=True) + item2.scale_orig_factor = 3 + item2.scale_anchor = QtCore.QPointF(0, 0) + item2.setPos(100, 100) + item2.scale_orig_pos = QtCore.QPointF(100, 100) + command = commands.ScaleItemsBy([item1, item2], 2, + ignore_first_redo=True) command.redo() assert item1.scale() == 1 assert item2.scale() == 3 assert item1.pos().x() == 0 assert item1.pos().y() == 0 + assert item2.pos().x() == 100 + assert item2.pos().y() == 100 command.redo() - assert item1.scale() == 3 - assert item2.scale() == 5 - assert item1.pos().x() == -200 - assert item1.pos().y() == -200 + assert item1.scale() == 2 + assert item1.pos().x() == -100 + assert item1.pos().y() == -100 + assert item2.scale() == 6 + assert item2.pos().x() == 100 + assert item2.pos().y() == 100 class NormalizeItemsTestCase(BeeTestCase): diff --git a/tests/test_items.py b/tests/test_items.py index 59b6446..c0c97fd 100644 --- a/tests/test_items.py +++ b/tests/test_items.py @@ -266,29 +266,41 @@ class BeePixmapItemPaintstuffTestCase(BeePixmapItemWithViewBaseTestCase): class BeePixmapItemScalingTestCase(BeePixmapItemWithViewBaseTestCase): - def test_get_scale_delta_bottomright(self): + def test_get_scale_factor_bottomright(self): self.item.scale_start = QtCore.QPoint(10, 10) + self.item.scale_direction = (1, 1) + self.item.scale_orig_factor = 1 event = MagicMock() event.scenePos = MagicMock(return_value=QtCore.QPoint(20, 90)) - assert self.item.get_scale_delta(event, (100, 80)) == 0.5 + assert self.item.get_scale_factor(event) == 1.5 def test_get_scale_delta_topleft(self): self.item.scale_start = QtCore.QPoint(10, 10) + self.item.scale_direction = (-1, -1) + self.item.scale_orig_factor = 0.5 event = MagicMock() event.scenePos = MagicMock(return_value=QtCore.QPoint(-10, -60)) - assert self.item.get_scale_delta(event, (0, 0)) == 0.5 + assert self.item.get_scale_factor(event) == 2 def test_get_scale_anchor_topleft(self): - assert self.item.get_scale_anchor((0, 0)) == (100, 80) + anchor = self.item.get_scale_anchor(self.item, (0, 0)) + assert anchor.x() == 100 + assert anchor.y() == 80 def test_get_scale_anchor_bottomright(self): - assert self.item.get_scale_anchor((100, 80)) == (0, 0) + anchor = self.item.get_scale_anchor(self.item, (100, 80)) + assert anchor.x() == 0 + assert anchor.y() == 0 def test_get_scale_anchor_topright(self): - assert self.item.get_scale_anchor((100, 0)) == (0, 80) + anchor = self.item.get_scale_anchor(self.item, (100, 0)) + assert anchor.x() == 0 + assert anchor.y() == 80 def test_get_scale_anchor_bottomleft(self): - assert self.item.get_scale_anchor((0, 80)) == (100, 0) + anchor = self.item.get_scale_anchor(self.item, (0, 80)) + assert anchor.x() == 100 + assert anchor.y() == 0 def test_get_scale_direction_topleft(self): assert self.item.get_scale_direction((0, 0)) == (-1, -1) @@ -303,8 +315,10 @@ class BeePixmapItemScalingTestCase(BeePixmapItemWithViewBaseTestCase): assert self.item.get_scale_direction((0, 80)) == (-1, 1) def test_translate_for_scale_anchor(self): - pos = QtCore.QPoint(50, 70) - self.item.translate_for_scale_anchor(pos, 2, (100, 80)) + self.item.scale_orig_pos = QtCore.QPoint(50, 70) + self.item.scale_anchor = QtCore.QPoint(100, 80) + self.item.scale_orig_factor = 1 + self.item.translate_for_scale_anchor(3) assert self.item.pos().x() == -150 assert self.item.pos().y() == -90 @@ -367,8 +381,9 @@ class BeePixmapItemEventsstuffTestCase(BeePixmapItemWithViewBaseTestCase): self.event.button = MagicMock( return_value=Qt.MouseButtons.LeftButton) self.item.mousePressEvent(self.event) - assert self.item.scale_active_corner == (0, 0) + assert self.item.scale_active is True assert self.item.scale_start == QtCore.QPointF(66, 99) + assert self.item.scale_direction == (-1, -1) assert self.item.scale_orig_factor == 1 assert self.item.scale_orig_pos == QtCore.QPointF(0, 0) @@ -379,7 +394,8 @@ class BeePixmapItemEventsstuffTestCase(BeePixmapItemWithViewBaseTestCase): self.event.button = MagicMock( return_value=Qt.MouseButtons.LeftButton) self.item.mousePressEvent(self.event) - assert self.item.scale_active_corner == (100, 80) + assert self.item.scale_active is True + assert self.item.scale_direction == (1, 1) assert self.item.scale_start == QtCore.QPointF(66, 99) assert self.item.scale_orig_factor == 1 assert self.item.scale_orig_pos == QtCore.QPointF(0, 0) @@ -389,7 +405,7 @@ class BeePixmapItemEventsstuffTestCase(BeePixmapItemWithViewBaseTestCase): with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.mousePressEvent') as m: self.item.mousePressEvent(self.event) m.assert_called_once_with(self.event) - assert self.item.scale_active_corner is None + assert self.item.scale_active is False def test_mouse_press_not_in_handles(self): self.item.setSelected(True) @@ -399,7 +415,7 @@ class BeePixmapItemEventsstuffTestCase(BeePixmapItemWithViewBaseTestCase): with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.mousePressEvent') as m: self.item.mousePressEvent(self.event) m.assert_called_once_with(self.event) - assert self.item.scale_active_corner is None + assert self.item.scale_active is False def test_mouse_move_event_when_no_action(self): with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.mouseMoveEvent') as m: @@ -408,7 +424,9 @@ class BeePixmapItemEventsstuffTestCase(BeePixmapItemWithViewBaseTestCase): def test_move_event_when_scale_action(self): self.event.scenePos = MagicMock(return_value=QtCore.QPointF(20, 90)) - self.item.scale_active_corner = (100, 80) + self.item.scale_active = True + self.item.scale_direction = (1, 1) + self.item.scale_anchor = QtCore.QPointF(100, 80) self.item.scale_start = QtCore.QPointF(10, 10) self.item.scale_orig_factor = 1 self.item.scale_orig_pos = QtCore.QPointF(0, 0) @@ -424,7 +442,9 @@ class BeePixmapItemEventsstuffTestCase(BeePixmapItemWithViewBaseTestCase): def test_mouse_release_event_when_scale_action(self): self.event.scenePos = MagicMock(return_value=QtCore.QPointF(20, 90)) - self.item.scale_active_corner = (100, 80) + self.item.scale_active = True + self.item.scale_direction = (1, 1) + self.item.scale_anchor = QtCore.QPointF(100, 80) self.item.scale_start = QtCore.QPointF(10, 10) self.item.scale_orig_factor = 1 self.item.scale_orig_pos = QtCore.QPointF(0, 0) @@ -436,7 +456,10 @@ class BeePixmapItemEventsstuffTestCase(BeePixmapItemWithViewBaseTestCase): args = self.scene.undo_stack.push.call_args_list[0][0] cmd = args[0] assert cmd.items == [self.item] - assert cmd.delta == 0.5 - assert cmd.anchor == (0, 0) + assert cmd.factor == 1.5 + assert cmd.item_data == [{ + 'anchor': QtCore.QPointF(100, 80), + 'orig_factor': 1, + 'orig_pos': QtCore.QPointF(0, 0)}] assert cmd.ignore_first_redo is True - assert self.item.scale_active_corner is None + assert self.item.scale_active is False