diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 726f5c4..f09a0a0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,8 @@ Added ----- +* Image cropping (Go to "Transform -> Crop", or press Shift + C) + Note that older version of BeeRef will display the images uncropped. * Show list of recent files on welcome screen * Keyboard shortcuts can now be configured via a settings file. Go to "Settings -> Open Settings Folder" and edit KeyboardSettings.ini diff --git a/beeref/actions/actions.py b/beeref/actions/actions.py index 1e1c395..e956a45 100644 --- a/beeref/actions/actions.py +++ b/beeref/actions/actions.py @@ -145,6 +145,13 @@ actions = [ 'callback': 'on_action_arrange_vertical', 'group': 'active_when_selection', }, + { + 'id': 'crop', + 'text': '&Crop', + 'shortcuts': ['Shift+C'], + 'callback': 'on_action_crop', + 'group': 'active_when_croppable', + }, { 'id': 'flip_horizontally', 'text': 'Flip &Horizontally', @@ -196,6 +203,12 @@ actions = [ 'callback': 'on_action_reset_flip', 'group': 'active_when_selection', }, + { + 'id': 'reset_crop', + 'text': 'Reset Cro&p', + 'callback': 'on_action_reset_crop', + 'group': 'active_when_selection', + }, { 'id': 'reset_transforms', 'text': 'Reset &All', diff --git a/beeref/actions/menu_structure.py b/beeref/actions/menu_structure.py index cf2aab9..5943a28 100644 --- a/beeref/actions/menu_structure.py +++ b/beeref/actions/menu_structure.py @@ -73,12 +73,14 @@ menu_structure = [ { 'menu': '&Transform', 'items': [ + 'crop', 'flip_horizontally', 'flip_vertically', MENU_SEPARATOR, 'reset_scale', 'reset_rotation', 'reset_flip', + 'reset_crop', 'reset_transforms', ], }, diff --git a/beeref/commands.py b/beeref/commands.py index 6b01b73..8a232f8 100644 --- a/beeref/commands.py +++ b/beeref/commands.py @@ -32,10 +32,9 @@ class InsertItems(QtGui.QUndoCommand): return if self.position: rect = self.scene.itemsBoundingRect(items=self.items) - center = (rect.topLeft() + rect.bottomRight()) / 2 for item in self.items: self.old_positions.append(item.pos()) - item.setPos(item.pos() + self.position - center) + item.setPos(item.pos() + self.position - rect.center()) self.scene.clearSelection() for item in self.items: self.scene.addItem(item) @@ -148,13 +147,11 @@ class NormalizeItems(QtGui.QUndoCommand): self.old_scale_factors = [] for item, factor in zip(self.items, self.scale_factors): self.old_scale_factors.append(item.scale()) - item.setScale(item.scale() * factor, - QtCore.QPointF(item.width, item.height) / 2) + item.setScale(item.scale() * factor, item.center) def undo(self): for item, factor in zip(self.items, self.old_scale_factors): - item.setScale(factor, - QtCore.QPointF(item.width, item.height) / 2) + item.setScale(factor, item.center) class FlipItems(QtGui.QUndoCommand): @@ -226,6 +223,23 @@ class ResetFlip(QtGui.QUndoCommand): item.do_flip(anchor=item.center) +class ResetCrop(QtGui.QUndoCommand): + + def __init__(self, items): + super().__init__('Reset Crop') + self.items = [item for item in items if item.is_croppable] + + def redo(self): + self.old_crops = [] + for item in self.items: + self.old_crops.append(item.crop) + item.reset_crop() + + def undo(self): + for item, crop in zip(self.items, self.old_crops): + item.crop = crop + + class ResetTransforms(QtGui.QUndoCommand): def __init__(self, items): @@ -235,11 +249,15 @@ class ResetTransforms(QtGui.QUndoCommand): def redo(self): self.old_values = [] for item in self.items: - self.old_values.append({ + values = { 'scale': item.scale(), 'rotation': item.rotation(), 'flip': item.flip(), - }) + } + if item.is_croppable: + values['crop'] = item.crop + item.reset_crop() + self.old_values.append(values) item.setScale(1, anchor=item.center) item.setRotation(0, anchor=item.center) @@ -252,6 +270,8 @@ class ResetTransforms(QtGui.QUndoCommand): item.setRotation(old['rotation'], anchor=item.center) if old['flip'] == -1: item.do_flip(anchor=item.center) + if item.is_croppable: + item.crop = old['crop'] class ArrangeItems(QtGui.QUndoCommand): @@ -266,7 +286,7 @@ class ArrangeItems(QtGui.QUndoCommand): self.old_positions = [] for item, pos in zip(self.items, self.positions): self.old_positions.append(item.pos()) - orig_topleft = item.corners_scene_coords[0] + orig_topleft = item.mapToScene(QtCore.QPointF(0, 0)) rect_topleft = self.scene.itemsBoundingRect( items=[item]).topLeft() item.setPos(pos + orig_topleft - rect_topleft) @@ -274,3 +294,17 @@ class ArrangeItems(QtGui.QUndoCommand): def undo(self): for item, pos in zip(self.items, self.old_positions): item.setPos(pos) + + +class CropItem(QtGui.QUndoCommand): + def __init__(self, item, crop): + super().__init__('Crop item') + self.item = item + self.crop = crop + + def redo(self): + self.old_crop = self.item.crop + self.item.crop = self.crop + + def undo(self): + self.item.crop = self.old_crop diff --git a/beeref/fileio/sql.py b/beeref/fileio/sql.py index 96f7f70..25106f8 100644 --- a/beeref/fileio/sql.py +++ b/beeref/fileio/sql.py @@ -204,7 +204,7 @@ class SQLiteIO: } if data['type'] == 'pixmap': - data['item'] = BeePixmapItem(QtGui.QImage(), **data['data']) + data['item'] = BeePixmapItem(QtGui.QImage()) data['item'].pixmap_from_bytes(row[9]) self.scene.add_item_later(data) diff --git a/beeref/items.py b/beeref/items.py index 8b0173f..28e855d 100644 --- a/beeref/items.py +++ b/beeref/items.py @@ -22,6 +22,7 @@ import logging from PyQt6 import QtCore, QtGui, QtWidgets from PyQt6.QtCore import Qt +from beeref import commands from beeref.constants import COLORS from beeref.selection import SelectableMixin @@ -79,12 +80,16 @@ class BeePixmapItem(BeeItemMixin, QtWidgets.QGraphicsPixmapItem): """Class for images added by the user.""" TYPE = 'pixmap' + CROP_HANDLE_SIZE = 15 def __init__(self, image, filename=None): super().__init__(QtGui.QPixmap.fromImage(image)) self.save_id = None self.filename = filename + self.reset_crop() logger.debug(f'Initialized {self}') + self.is_croppable = True + self.crop_mode = False self.init_selectable() @classmethod @@ -92,21 +97,37 @@ class BeePixmapItem(BeeItemMixin, QtWidgets.QGraphicsPixmapItem): item = kwargs.pop('item') data = kwargs.pop('data', {}) item.filename = item.filename or data.get('filename') + if 'crop' in data: + item.crop = QtCore.QRectF(*data['crop']) return item def __str__(self): - return (f'Image "{self.filename}" {self.width} x {self.height}') + size = self.pixmap().size() + return (f'Image "{self.filename}" {size.width()} x {size.height()}') @property - def width(self): - return self.pixmap().size().width() + def crop(self): + return self._crop - @property - def height(self): - return self.pixmap().size().height() + @crop.setter + def crop(self, value): + logger.debug(f'Setting crop for {self} to {value}') + self.prepareGeometryChange() + self._crop = value + self.update() + + def bounding_rect_unselected(self): + if self.crop_mode: + return QtWidgets.QGraphicsPixmapItem.boundingRect(self) + else: + return self.crop def get_extra_save_data(self): - return {'filename': self.filename} + return {'filename': self.filename, + 'crop': [self.crop.topLeft().x(), + self.crop.topLeft().y(), + self.crop.width(), + self.crop.height()]} def pixmap_to_bytes(self): """Convert the pixmap data to PNG bytestring.""" @@ -117,16 +138,16 @@ class BeePixmapItem(BeeItemMixin, QtWidgets.QGraphicsPixmapItem): img.save(buffer, 'PNG') return barray.data() + def setPixmap(self, pixmap): + super().setPixmap(pixmap) + self.reset_crop() + 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 create_copy(self): item = BeePixmapItem(QtGui.QImage(), self.filename) item.setPixmap(self.pixmap()) @@ -136,11 +157,196 @@ class BeePixmapItem(BeeItemMixin, QtWidgets.QGraphicsPixmapItem): item.setRotation(self.rotation()) if self.flip() == -1: item.do_flip() + item.crop = self.crop return item def copy_to_clipboard(self, clipboard): clipboard.setPixmap(self.pixmap()) + def reset_crop(self): + self.crop = QtCore.QRectF( + 0, 0, self.pixmap().size().width(), self.pixmap().size().height()) + + @property + def crop_handle_size(self): + return self.fixed_length_for_viewport(self.CROP_HANDLE_SIZE) + + def crop_handle_topleft(self): + topleft = self.crop_temp.topLeft() + return QtCore.QRectF( + topleft.x(), + topleft.y(), + self.crop_handle_size, + self.crop_handle_size) + + def crop_handle_bottomleft(self): + bottomleft = self.crop_temp.bottomLeft() + return QtCore.QRectF( + bottomleft.x(), + bottomleft.y() - self.crop_handle_size, + self.crop_handle_size, + self.crop_handle_size) + + def crop_handle_bottomright(self): + bottomright = self.crop_temp.bottomRight() + return QtCore.QRectF( + bottomright.x() - self.crop_handle_size, + bottomright.y() - self.crop_handle_size, + self.crop_handle_size, + self.crop_handle_size) + + def crop_handle_topright(self): + topright = self.crop_temp.topRight() + return QtCore.QRectF( + topright.x() - self.crop_handle_size, + topright.y(), + self.crop_handle_size, + self.crop_handle_size) + + def crop_handles(self): + return (self.crop_handle_topleft, + self.crop_handle_bottomleft, + self.crop_handle_bottomright, + self.crop_handle_topright) + + def get_crop_handle_cursor(self, handle): + """Gets the crop cursor for the given handle.""" + + is_topleft_or_bottomright = handle in ( + self.crop_handle_topleft, self.crop_handle_bottomright) + return self.get_diag_cursor(is_topleft_or_bottomright) + + def draw_crop_rect(self, painter, rect): + """Paint a dotted rectangle for the cropping UI.""" + pen = QtGui.QPen(QtGui.QColor(255, 255, 255)) + pen.setWidth(2) + pen.setCosmetic(True) + painter.setPen(pen) + painter.drawRect(rect) + pen.setColor(QtGui.QColor(0, 0, 0)) + pen.setStyle(Qt.PenStyle.DotLine) + painter.setPen(pen) + painter.drawRect(rect) + + def paint(self, painter, option, widget): + if self.crop_mode: + self.paint_debug(painter, option, widget) + + # Darken image outside of cropped area + painter.drawPixmap(0, 0, self.pixmap()) + path = QtWidgets.QGraphicsPixmapItem.shape(self) + path.addRect(self.crop_temp) + color = QtGui.QColor(0, 0, 0) + color.setAlpha(100) + painter.setBrush(QtGui.QBrush(color)) + painter.setPen(QtGui.QPen()) + painter.drawPath(path) + painter.setBrush(QtGui.QBrush()) + + for handle in self.crop_handles(): + self.draw_crop_rect(painter, handle()) + self.draw_crop_rect(painter, self.crop_temp) + else: + painter.drawPixmap(self.crop, self.pixmap(), self.crop) + self.paint_selectable(painter, option, widget) + + def enter_crop_mode(self): + logger.debug(f'Entering crop mode on {self}') + self.prepareGeometryChange() + self.crop_mode = True + self.crop_temp = QtCore.QRectF(self.crop) + self.crop_mode_move = None + self.crop_mode_event_start = None + self.grabKeyboard() + self.update() + self.scene().crop_item = self + + def exit_crop_mode(self, confirm): + logger.debug(f'Exiting crop mode with {confirm} on {self}') + if confirm and self.crop != self.crop_temp: + self.scene().undo_stack.push( + commands.CropItem(self, self.crop_temp)) + self.prepareGeometryChange() + self.crop_mode = False + self.crop_temp = None + self.crop_mode_move = None + self.crop_mode_event_start = None + self.ungrabKeyboard() + self.update() + self.scene().crop_item = None + + def keyPressEvent(self, event): + if event.key() in (Qt.Key.Key_Return, Qt.Key.Key_Enter): + self.exit_crop_mode(confirm=True) + elif event.key() == Qt.Key.Key_Escape: + self.exit_crop_mode(confirm=False) + else: + super().keyPressEvent(event) + + def hoverMoveEvent(self, event): + if not self.crop_mode: + return super().hoverMoveEvent(event) + + for handle in self.crop_handles(): + if handle().contains(event.pos()): + self.setCursor(self.get_crop_handle_cursor(handle)) + return + self.setCursor(Qt.CursorShape.ArrowCursor) + + def mousePressEvent(self, event): + if not self.crop_mode: + return super().mousePressEvent(event) + + event.accept() + for handle in self.crop_handles(): + # Click into a handle? + if handle().contains(event.pos()): + self.crop_mode_event_start = event.pos() + self.crop_mode_move = handle + return + # Click not in handle, end cropping mode: + self.exit_crop_mode( + confirm=self.crop_temp.contains(event.pos())) + + def ensure_point_within_pixmap_bounds(self, point): + """Returns the point, or the nearest point within the pixmap.""" + point.setX(min(self.pixmap().size().width(), max(0, point.x()))) + point.setY(min(self.pixmap().size().height(), max(0, point.y()))) + return point + + def mouseMoveEvent(self, event): + if self.crop_mode: + diff = event.pos() - self.crop_mode_event_start + if self.crop_mode_move == self.crop_handle_topleft: + new = self.ensure_point_within_pixmap_bounds( + self.crop_temp.topLeft() + diff) + self.crop_temp.setTopLeft(new) + if self.crop_mode_move == self.crop_handle_bottomleft: + new = self.ensure_point_within_pixmap_bounds( + self.crop_temp.bottomLeft() + diff) + self.crop_temp.setBottomLeft(new) + if self.crop_mode_move == self.crop_handle_bottomright: + new = self.ensure_point_within_pixmap_bounds( + self.crop_temp.bottomRight() + diff) + self.crop_temp.setBottomRight(new) + if self.crop_mode_move == self.crop_handle_topright: + new = self.ensure_point_within_pixmap_bounds( + self.crop_temp.topRight() + diff) + self.crop_temp.setTopRight(new) + self.update() + self.crop_mode_event_start = event.pos() + event.accept() + else: + super().mouseMoveEvent(event) + + def mouseReleaseEvent(self, event): + if self.crop_mode: + self.crop_mode_move = None + self.crop_mode_event_start = None + event.accept() + else: + super().mouseReleaseEvent(event) + @register_item class BeeTextItem(BeeItemMixin, QtWidgets.QGraphicsTextItem): @@ -152,6 +358,7 @@ class BeeTextItem(BeeItemMixin, QtWidgets.QGraphicsTextItem): super().__init__(text or "Text") self.save_id = None logger.debug(f'Initialized {self}') + self.is_croppable = False self.init_selectable() self.is_editable = True self.edit_mode = False @@ -167,14 +374,6 @@ class BeeTextItem(BeeItemMixin, QtWidgets.QGraphicsTextItem): txt = self.toPlainText()[:40] return (f'Text "{txt}"') - @property - def width(self): - return QtWidgets.QGraphicsTextItem.boundingRect(self).width() - - @property - def height(self): - return QtWidgets.QGraphicsTextItem.boundingRect(self).height() - def get_extra_save_data(self): return {'text': self.toPlainText()} @@ -203,15 +402,19 @@ class BeeTextItem(BeeItemMixin, QtWidgets.QGraphicsTextItem): return item def enter_edit_mode(self): + logger.debug(f'Entering edit mode on {self}') self.edit_mode = True self.setTextInteractionFlags( Qt.TextInteractionFlag.TextEditorInteraction) + self.scene().edit_item = self def exit_edit_mode(self): + logger.debug(f'Exiting edit mode on {self}') self.edit_mode = False # reset selection: self.setTextCursor(QtGui.QTextCursor(self.document())) self.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction) + self.scene().edit_item = None def has_selection_handles(self): return super().has_selection_handles() and not self.edit_mode diff --git a/beeref/scene.py b/beeref/scene.py index 2856d93..8185007 100644 --- a/beeref/scene.py +++ b/beeref/scene.py @@ -47,6 +47,7 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene): self.items_to_add = Queue() self.internal_clipboard = [] self.edit_item = None + self.crop_item = None def addItem(self, item): logger.debug(f'Adding item {item}') @@ -56,6 +57,11 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene): logger.debug(f'Removing item {item}') super().removeItem(item) + def cancel_crop_mode(self): + """Cancels an ongoing crop mode, if there is any.""" + if self.crop_item: + self.crop_item.exit_crop_mode(confirm=False) + def copy_selection_to_internal_clipboard(self): self.internal_clipboard = [] for item in self.selectedItems(user_only=True): @@ -70,6 +76,7 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene): self.undo_stack.push(commands.InsertItems(self, copies, position)) def raise_to_top(self): + self.cancel_crop_mode() items = self.selectedItems(user_only=True) z_values = map(lambda i: i.zValue(), items) delta = self.max_z + self.Z_STEP - min(z_values) @@ -78,6 +85,7 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene): item.setZValue(item.zValue() + delta) def lower_to_bottom(self): + self.cancel_crop_mode() items = self.selectedItems(user_only=True) z_values = map(lambda i: i.zValue(), items) delta = self.min_z - self.Z_STEP - max(z_values) @@ -93,6 +101,7 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene): :param mode: "width" or "height". """ + self.cancel_crop_mode() values = [] items = self.selectedItems(user_only=True) for item in items: @@ -124,6 +133,7 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene): Size meaning the area = widh * height. """ + self.cancel_crop_mode() sizes = [] items = self.selectedItems(user_only=True) for item in items: @@ -146,6 +156,8 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene): def arrange(self, vertical=False): """Arrange items in a line (horizontally or vertically).""" + self.cancel_crop_mode() + items = self.selectedItems(user_only=True) if len(items) < 2: return @@ -184,6 +196,8 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene): positions)) def arrange_optimal(self): + self.cancel_crop_mode() + items = self.selectedItems(user_only=True) if len(items) < 2: return @@ -218,13 +232,25 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene): def flip_items(self, vertical=False): """Flip selected items.""" + self.cancel_crop_mode() self.undo_stack.push( commands.FlipItems(self.selectedItems(user_only=True), self.get_selection_center(), vertical=vertical)) + def crop_items(self): + """Crop selected item.""" + + if self.crop_item: + return + if self.has_croppable_selection(): + item = self.selectedItems(user_only=True)[0] + if item.is_croppable: + item.enter_crop_mode() + def set_selected_all_items(self, value): """Sets the selection mode of all items to ``value``.""" + self.cancel_crop_mode() for item in self.items(): item.setSelected(value) @@ -243,6 +269,14 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene): return len(self.selectedItems(user_only=True)) > 1 + def has_croppable_selection(self): + """Checks whether the current selection is croppable, i.e. a + single selection whose item is croppable.""" + + if self.has_single_selection(): + return self.selectedItems(user_only=True)[0].is_croppable + return False + def mousePressEvent(self, event): if event.button() == Qt.MouseButton.RightButton: # Right-click invokes the context menu on the @@ -257,7 +291,12 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene): if self.edit_item: if item_at_pos != self.edit_item: self.edit_item.exit_edit_mode() - self.edit_item = None + else: + super().mousePressEvent(event) + return + if self.crop_item: + if item_at_pos != self.crop_item: + self.cancel_crop_mode() else: super().mousePressEvent(event) return @@ -275,7 +314,6 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene): if not item.isSelected(): item.setSelected(True) if item.is_editable: - self.edit_item = item item.enter_edit_mode() self.mousePressEvent(event) else: diff --git a/beeref/selection.py b/beeref/selection.py index 1e23a1b..2ae0c06 100644 --- a/beeref/selection.py +++ b/beeref/selection.py @@ -13,7 +13,7 @@ # You should have received a copy of the GNU General Public License # along with BeeRef. If not, see . -"""Classes for items that draw and handle selection stuff.""" +"""Classes that draw and handle selection stuff for items.""" import logging import math @@ -98,9 +98,20 @@ class BaseItemMixin: if vertical: self.setRotation(self.rotation() + 180) + def bounding_rect_unselected(self): + return super().boundingRect() + + @property + def width(self): + return self.bounding_rect_unselected().width() + + @property + def height(self): + return self.bounding_rect_unselected().height() + @property def center(self): - return QtCore.QPointF(self.width, self.height) / 2 + return self.bounding_rect_unselected().center() @property def center_scene_coords(self): @@ -166,7 +177,7 @@ class SelectableMixin(BaseItemMixin): else: painter.fillPath(shape, color) - def paint_selectable(self, painter, option, widget): + def paint_debug(self, painter, option, widget): if commandline_args.debug_shapes: self.draw_debug_shape(painter, self.shape(), 255, 0, 0) if commandline_args.debug_boundingrects: @@ -180,6 +191,9 @@ class SelectableMixin(BaseItemMixin): for edge in self.get_flip_bounds(): self.draw_debug_shape(painter, edge['rect'], 255, 255, 0) + def paint_selectable(self, painter, option, widget): + self.paint_debug(painter, option, widget) + if not self.has_selection_outline(): return @@ -189,7 +203,7 @@ class SelectableMixin(BaseItemMixin): painter.setPen(pen) # Draw the main selection rectangle - painter.drawRect(0, 0, self.width, self.height) + painter.drawRect(self.bounding_rect_unselected()) # If it's a single selection, draw the handles: if self.has_selection_handles(): @@ -202,10 +216,10 @@ class SelectableMixin(BaseItemMixin): @property def corners(self): """The corners of the item. Used for scale and rotate handles.""" - return (QtCore.QPointF(0, 0), - QtCore.QPointF(self.width, 0), - QtCore.QPointF(self.width, self.height), - QtCore.QPointF(0, self.height)) + return (self.bounding_rect_unselected().topLeft(), + self.bounding_rect_unselected().topRight(), + self.bounding_rect_unselected().bottomRight(), + self.bounding_rect_unselected().bottomLeft()) @property def corners_scene_coords(self): @@ -264,65 +278,66 @@ class SelectableMixin(BaseItemMixin): outer_margin = self.select_resize_size / 2 inner_margin = self.select_resize_size / 2 + origin = self.bounding_rect_unselected().topLeft() return [ # top: { - 'rect': QtCore.QRectF(inner_margin, - -outer_margin, - self.width - 2 * inner_margin, - outer_margin + inner_margin), + 'rect': QtCore.QRectF( + origin.x() + inner_margin, + origin.y() - outer_margin, + self.width - 2 * inner_margin, + outer_margin + inner_margin), 'flip_v': True, }, # bottom: { - 'rect': QtCore.QRectF(inner_margin, - self.height - inner_margin, - self.width - 2 * inner_margin, - outer_margin + inner_margin), + 'rect': QtCore.QRectF( + origin.x() + inner_margin, + origin.y() + self.height - inner_margin, + self.width - 2 * inner_margin, + outer_margin + inner_margin), 'flip_v': True, }, # left: { - 'rect': QtCore.QRectF(-outer_margin, - inner_margin, - outer_margin + inner_margin, - self.height - 2 * inner_margin), + 'rect': QtCore.QRectF( + origin.x() - outer_margin, + origin.y() + inner_margin, + outer_margin + inner_margin, + self.height - 2 * inner_margin), 'flip_v': False, }, # right: { - 'rect': QtCore.QRectF(self.width - inner_margin, - inner_margin, - outer_margin + inner_margin, - self.height - 2 * inner_margin), + 'rect': QtCore.QRectF( + origin.x() + self.width - inner_margin, + origin.y() + inner_margin, + outer_margin + inner_margin, + self.height - 2 * inner_margin), 'flip_v': False, } ] def boundingRect(self): if not self.has_selection_outline(): - return super().boundingRect() + return self.bounding_rect_unselected() # Add extra space for the interactive areas margin = self.select_resize_size / 2 + self.select_rotate_size - return QtCore.QRectF( - -margin, -margin, - self.width + 2 * margin, - self.height + 2 * margin) + return self.bounding_rect_unselected().marginsAdded( + QtCore.QMarginsF(margin, margin, margin, margin)) def shape(self): path = QtGui.QPainterPath() if self.has_selection_handles(): margin = self.select_resize_size / 2 - rect = QtCore.QRectF( - -margin, -margin, - self.width + 2 * margin, - self.height + 2 * margin) + rect = self.bounding_rect_unselected().marginsAdded( + QtCore.QMarginsF(margin, margin, margin, margin)) path.addRect(rect) for corner in self.corners: path.addPath(self.get_rotate_bounds(corner)) else: - rect = super().boundingRect() + rect = self.bounding_rect_unselected() path.addRect(rect) return path @@ -408,14 +423,15 @@ class SelectableMixin(BaseItemMixin): def get_scale_anchor(self, corner): """Get the anchor around which the scale for this corner operates.""" - return QtCore.QPointF(self.width - corner.x(), - self.height - corner.y()) + origin = self.bounding_rect_unselected().topLeft() + return QtCore.QPointF(self.width - corner.x() + 2*origin.x(), + self.height - corner.y() + 2*origin.y()) def get_corner_direction(self, corner): """Get the direction facing away from the center, e.g. the direction in which the scale for this corner increases.""" - return QtCore.QPointF(1 if corner.x() > 0 else -1, - 1 if corner.y() > 0 else -1) + return QtCore.QPointF(1 if corner.x() > self.center.x() else -1, + 1 if corner.y() > self.center.y() else -1) def get_direction_from_center(self, pos): """The direction of a point in relation to the item's center.""" @@ -444,11 +460,16 @@ class SelectableMixin(BaseItemMixin): def get_corner_scale_cursor(self, corner): """Gets the scale cursor for the given corner.""" + is_topleft_or_bottomright = corner in ( + self.bounding_rect_unselected().topLeft(), + self.bounding_rect_unselected().bottomRight()) + return self.get_diag_cursor(is_topleft_or_bottomright) + + def get_diag_cursor(self, is_topleft_or_bottomright): rotation = self.rotation() % 180 flipped = self.flip() == -1 - if corner in (QtCore.QPointF(0, 0), - QtCore.QPointF(self.width, self.height)): + if is_topleft_or_bottomright: if 22.5 < rotation < 67.5: return Qt.CursorShape.SizeVerCursor elif 67.5 < rotation < 112.5: @@ -571,14 +592,6 @@ class MultiSelectItem(SelectableMixin, def __str__(self): return (f'MultiSelectItem {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) @@ -641,14 +654,6 @@ class RubberbandItem(BaseItemMixin, QtWidgets.QGraphicsRectItem): def __str__(self): return (f'RubberbandItem {self.width} x {self.height}') - @property - def width(self): - return self.rect().width() - - @property - def height(self): - return self.rect().height() - def fit(self, point1, point2): """Updates itself to fit the two given points.""" diff --git a/beeref/view.py b/beeref/view.py index cf3e52b..8b83c17 100644 --- a/beeref/view.py +++ b/beeref/view.py @@ -215,10 +215,12 @@ class BeeGraphicsView(MainControlsMixin, def on_action_undo(self): logger.debug('Undo: %s' % self.undo_stack.undoText()) + self.scene.cancel_crop_mode() self.undo_stack.undo() def on_action_redo(self): logger.debug('Redo: %s' % self.undo_stack.redoText()) + self.scene.cancel_crop_mode() self.undo_stack.redo() def on_action_select_all(self): @@ -229,6 +231,7 @@ class BeeGraphicsView(MainControlsMixin, def on_action_delete_items(self): logger.debug('Deleting items...') + self.scene.cancel_crop_mode() self.undo_stack.push( commands.DeleteItems( self.scene, self.scene.selectedItems(user_only=True))) @@ -264,6 +267,9 @@ class BeeGraphicsView(MainControlsMixin, def on_action_arrange_optimal(self): self.scene.arrange_optimal() + def on_action_crop(self): + self.scene.crop_items() + def on_action_flip_horizontally(self): self.scene.flip_items(vertical=False) @@ -271,18 +277,27 @@ class BeeGraphicsView(MainControlsMixin, self.scene.flip_items(vertical=True) def on_action_reset_scale(self): + self.scene.cancel_crop_mode() self.undo_stack.push(commands.ResetScale( self.scene.selectedItems(user_only=True))) def on_action_reset_rotation(self): + self.scene.cancel_crop_mode() self.undo_stack.push(commands.ResetRotation( self.scene.selectedItems(user_only=True))) def on_action_reset_flip(self): + self.scene.cancel_crop_mode() self.undo_stack.push(commands.ResetFlip( self.scene.selectedItems(user_only=True))) + def on_action_reset_crop(self): + self.scene.cancel_crop_mode() + self.undo_stack.push(commands.ResetCrop( + self.scene.selectedItems(user_only=True))) + def on_action_reset_transforms(self): + self.scene.cancel_crop_mode() self.undo_stack.push(commands.ResetTransforms( self.scene.selectedItems(user_only=True))) @@ -316,6 +331,7 @@ class BeeGraphicsView(MainControlsMixin, self.worker.start() def on_action_open(self): + self.scene.cancel_crop_mode() filename, f = QtWidgets.QFileDialog.getOpenFileName( parent=self, caption='Open file', @@ -349,6 +365,7 @@ class BeeGraphicsView(MainControlsMixin, self.worker.start() def on_action_save_as(self): + self.scene.cancel_crop_mode() filename, f = QtWidgets.QFileDialog.getSaveFileName( parent=self, caption='Save file', @@ -357,6 +374,7 @@ class BeeGraphicsView(MainControlsMixin, self.do_save(filename, create_new=True) def on_action_save(self): + self.scene.cancel_crop_mode() if not self.filename: self.on_action_save_as() else: @@ -429,6 +447,7 @@ class BeeGraphicsView(MainControlsMixin, self.worker.start() def on_action_insert_images(self): + self.scene.cancel_crop_mode() formats = self.get_supported_image_formats(QtGui.QImageReader) logger.debug(f'Supported image types for reading: {formats}') filenames, f = QtWidgets.QFileDialog.getOpenFileNames( @@ -438,6 +457,7 @@ class BeeGraphicsView(MainControlsMixin, self.do_insert_images(filenames) def on_action_insert_text(self): + self.scene.cancel_crop_mode() item = BeeTextItem() pos = self.mapToScene(self.mapFromGlobal(self.cursor().pos())) item.setScale(1 / self.get_scale()) @@ -445,6 +465,7 @@ class BeeGraphicsView(MainControlsMixin, def on_action_copy(self): logger.debug('Copying to clipboard...') + self.scene.cancel_crop_mode() clipboard = QtWidgets.QApplication.clipboard() items = self.scene.selectedItems(user_only=True) @@ -462,6 +483,7 @@ class BeeGraphicsView(MainControlsMixin, 'beeref/items', QtCore.QByteArray.number(len(items))) def on_action_paste(self): + self.scene.cancel_crop_mode() logger.debug('Pasting from clipboard...') clipboard = QtWidgets.QApplication.clipboard() pos = self.mapToScene(self.mapFromGlobal(self.cursor().pos())) @@ -499,6 +521,8 @@ class BeeGraphicsView(MainControlsMixin, len(self.scene.selectedItems(user_only=True))) self.actiongroup_set_enabled('active_when_selection', self.scene.has_selection()) + self.actiongroup_set_enabled('active_when_croppable', + self.scene.has_croppable_selection()) self.viewport().repaint() def recalc_scene_rect(self): diff --git a/tests/fileio/test_sql.py b/tests/fileio/test_sql.py index a7e9b65..e754d76 100644 --- a/tests/fileio/test_sql.py +++ b/tests/fileio/test_sql.py @@ -4,7 +4,7 @@ import os.path import stat from unittest.mock import MagicMock, patch -from PyQt6 import QtGui +from PyQt6 import QtCore, QtGui import pytest from beeref.fileio import schema, is_bee_file @@ -233,6 +233,7 @@ def test_sqliteio_write_inserts_new_pixmap_item(tmpfile, view): item.setZValue(0.22) item.setRotation(33) item.do_flip() + item.crop = QtCore.QRectF(5, 5, 100, 80) item.pixmap_to_bytes = MagicMock(return_value=b'abc') io = SQLiteIO(tmpfile, view.scene, create_new=True) io.write() @@ -249,7 +250,10 @@ def test_sqliteio_write_inserts_new_pixmap_item(tmpfile, view): assert result[3] == 1.3 assert result[4] == 33 assert result[5] == -1 - assert json.loads(result[6]) == {'filename': 'bee.jpg'} + assert json.loads(result[6]) == { + 'filename': 'bee.jpg', + 'crop': [5, 5, 100, 80], + } assert result[7] == 'pixmap' assert result[8] == b'abc' assert result[9] == '0001-bee.png' @@ -265,7 +269,7 @@ def test_sqliteio_write_inserts_new_pixmap_item_without_filename( result = io.fetchone( 'SELECT items.data, sqlar.name FROM items ' 'INNER JOIN sqlar on sqlar.item_id = items.id') - assert json.loads(result[0]) == {'filename': None} + assert json.loads(result[0])['filename'] is None assert result[1] == '0001.png' @@ -311,6 +315,7 @@ def test_sqliteio_write_updates_existing_pixmap_item(tmpfile, view): item.setZValue(0.22) item.setRotation(33) item.save_id = 1 + item.crop = QtCore.QRectF(5, 5, 80, 100) item.pixmap_to_bytes = MagicMock(return_value=b'abc') io = SQLiteIO(tmpfile, view.scene, create_new=True) io.write() @@ -319,6 +324,7 @@ def test_sqliteio_write_updates_existing_pixmap_item(tmpfile, view): item.setZValue(0.33) item.setRotation(100) item.do_flip() + item.crop = QtCore.QRectF(1, 2, 30, 40) item.filename = 'new.png' item.pixmap_to_bytes.return_value = b'updated' io.create_new = False @@ -335,7 +341,10 @@ def test_sqliteio_write_updates_existing_pixmap_item(tmpfile, view): assert result[3] == 0.7 assert result[4] == 100 assert result[5] == -1 - assert json.loads(result[6]) == {'filename': 'new.png'} + assert json.loads(result[6]) == { + 'filename': 'new.png', + 'crop': [1, 2, 30, 40], + } assert result[7] == b'abc' diff --git a/tests/items/test_pixmapitem.py b/tests/items/test_pixmapitem.py index 25da944..012aaff 100644 --- a/tests/items/test_pixmapitem.py +++ b/tests/items/test_pixmapitem.py @@ -1,6 +1,8 @@ -from unittest.mock import patch, MagicMock, PropertyMock +import pytest +from unittest.mock import patch, MagicMock from PyQt6 import QtCore, QtGui, QtWidgets +from PyQt6.QtCore import Qt from beeref.items import BeePixmapItem, item_registry @@ -17,44 +19,66 @@ def test_init(selectable_mock, qapp, imgfilename3x3): assert item.height == 3 assert item.scale() == 1 assert item.filename == imgfilename3x3 + assert item.crop == QtCore.QRectF(0, 0, 3, 3) + assert item.is_croppable is True + assert item.crop_mode is False selectable_mock.assert_called_once() def test_set_pos_center(qapp, item): - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=200): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=100): - item.set_pos_center(QtCore.QPointF(0, 0)) - assert item.pos().x() == -100 - assert item.pos().y() == -50 + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 200, 100)): + item.set_pos_center(QtCore.QPointF(0, 0)) + assert item.pos().x() == -100 + assert item.pos().y() == -50 def test_set_pos_center_when_scaled(qapp, item): item.setScale(2) - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=200): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=100): - item.set_pos_center(QtCore.QPointF(0, 0)) - assert item.pos().x() == -200 - assert item.pos().y() == -100 + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 200, 100)): + item.set_pos_center(QtCore.QPointF(0, 0)) + assert item.pos().x() == -200 + assert item.pos().y() == -100 def test_set_pos_center_when_rotated(qapp, item): item.setRotation(90) - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=200): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=100): - item.set_pos_center(QtCore.QPointF(0, 0)) - assert item.pos().x() == 50 - assert item.pos().y() == -100 + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 200, 100)): + item.set_pos_center(QtCore.QPointF(0, 0)) + assert item.pos().x() == 50 + assert item.pos().y() == -100 + + +def test_set_crop(qapp, item): + item.update = MagicMock() + item.prepareGeometryChange = MagicMock() + item.crop = QtCore.QRectF(10, 20, 30, 40) + item.update.assert_called_once_with() + item.prepareGeometryChange.assert_called_once_with() + + +def test_bounding_rect_unselected(qapp, imgfilename3x3): + item = BeePixmapItem(QtGui.QImage(imgfilename3x3)) + item.crop = QtCore.QRectF(1, 1, 2, 2) + assert item.bounding_rect_unselected() == QtCore.QRectF(1, 1, 2, 2) + + +def test_bounding_rect_unselected_in_crop_mode(qapp, imgfilename3x3): + item = BeePixmapItem(QtGui.QImage(imgfilename3x3)) + item.crop = QtCore.QRectF(1, 1, 2, 2) + item.crop_mode = True + assert item.bounding_rect_unselected() == QtCore.QRectF(-0.5, -0.5, 4, 4) def test_get_extra_save_data(item): item.filename = 'foobar.png' - assert item.get_extra_save_data() == {'filename': 'foobar.png'} + item.crop = QtCore.QRectF(10, 20, 30, 40) + assert item.get_extra_save_data() == { + 'filename': 'foobar.png', + 'crop': [10, 20, 30, 40], + } def test_pixmap_to_bytes(qapp, imgfilename3x3): @@ -68,15 +92,7 @@ def test_pixmap_from_bytes(qapp, item, imgfilename3x3): item.pixmap_from_bytes(imgdata) assert item.width == 3 assert item.height == 3 - - -def test_paint(qapp, item): - item.pixmap = MagicMock(return_value='bee') - item.paint_selectable = MagicMock() - painter = MagicMock() - item.paint(painter, None, None) - item.paint_selectable.assert_called_once() - painter.drawPixmap.assert_called_with(0, 0, 'bee') + assert item.crop == QtCore.QRectF(0, 0, 3, 3) def test_has_selection_outline_when_not_selected(view, item): @@ -159,6 +175,14 @@ def test_create_from_data(item): assert item.filename == 'foobar.png' +def test_create_from_data_with_crop(item): + new_item = BeePixmapItem.create_from_data( + item=item, data={'filename': 'foobar.png', 'crop': [10, 20, 30, 40]}) + assert new_item is item + assert item.filename == 'foobar.png' + assert item.crop == QtCore.QRectF(10, 20, 30, 40) + + def test_create_copy(qapp, imgfilename3x3): item = BeePixmapItem(QtGui.QImage(imgfilename3x3), 'foo.png') item.setPos(20, 30) @@ -166,19 +190,401 @@ def test_create_copy(qapp, imgfilename3x3): item.do_flip() item.setZValue(0.5) item.setScale(2.2) + item.crop = QtCore.QRectF(10, 20, 30, 40) copy = item.create_copy() assert copy.pixmap_to_bytes() == item.pixmap_to_bytes() assert copy.filename == 'foo.png' assert copy.pos() == QtCore.QPointF(20, 30) assert copy.rotation() == 33 - assert item.flip() == -1 - assert item.zValue() == 0.5 - assert item.scale() == 2.2 + assert copy.flip() == -1 + assert copy.zValue() == 0.5 + assert copy.scale() == 2.2 + assert copy.crop == QtCore.QRectF(10, 20, 30, 40) -def test_item_to_clipboard(qapp, imgfilename3x3): +def test_copy_to_clipboard(qapp, imgfilename3x3): clipboard = QtWidgets.QApplication.clipboard() item = BeePixmapItem(QtGui.QImage(imgfilename3x3), 'foo.png') item.copy_to_clipboard(clipboard) assert clipboard.pixmap().size() == item.pixmap().size() + + +def test_reset_crop(qapp, imgfilename3x3): + item = BeePixmapItem(QtGui.QImage(imgfilename3x3)) + item.crop = QtCore.QRectF(10, 20, 30, 40) + item.reset_crop() + assert item.crop == QtCore.QRectF(0, 0, 3, 3) + + +def test_crop_handle_topleft(qapp, item): + item.crop_temp = QtCore.QRectF(100, 200, 300, 400) + assert item.crop_handle_topleft() == QtCore.QRectF(100, 200, 15, 15) + + +def test_crop_handle_bottomleft(qapp, item): + item.crop_temp = QtCore.QRectF(100, 200, 300, 400) + assert item.crop_handle_bottomleft() == QtCore.QRectF(100, 585, 15, 15) + + +def test_crop_handle_bottomright(qapp, item): + item.crop_temp = QtCore.QRectF(100, 200, 300, 400) + assert item.crop_handle_bottomright() == QtCore.QRectF(385, 585, 15, 15) + + +def test_crop_handle_topright(qapp, item): + item.crop_temp = QtCore.QRectF(100, 200, 300, 400) + assert item.crop_handle_topright() == QtCore.QRectF(385, 200, 15, 15) + + +def test_paint(qapp, item): + item.pixmap = MagicMock() + item.paint_selectable = MagicMock() + item.crop = QtCore.QRectF(10, 20, 30, 40) + painter = MagicMock() + item.paint(painter, None, None) + item.paint_selectable.assert_called_once() + painter.drawPixmap.assert_called_with( + QtCore.QRectF(10, 20, 30, 40), + item.pixmap(), + QtCore.QRectF(10, 20, 30, 40)) + + +def test_paint_when_crop_mode(qapp, item): + item.pixmap = MagicMock() + item.paint_selectable = MagicMock() + item.crop = QtCore.QRectF(10, 20, 30, 40) + item.crop_mode = True + item.crop_temp = QtCore.QRectF(11, 22, 29, 39) + painter = MagicMock() + item.paint(painter, None, None) + item.paint_selectable.assert_not_called() + painter.drawPixmap.assert_called_with(0, 0, item.pixmap()) + + +def test_enter_crop_mode(view, item): + view.scene.addItem(item) + item.crop = QtCore.QRectF(10, 20, 30, 40) + item.update = MagicMock() + item.prepareGeometryChange = MagicMock() + item.grabKeyboard = MagicMock() + + item.enter_crop_mode() + assert item.crop_mode is True + assert item.crop_temp == QtCore.QRectF(10, 20, 30, 40) + assert item.crop_mode_move is None + assert item.crop_mode_event_start is None + item.update.assert_called_once_with() + item.prepareGeometryChange.assert_called_once_with() + item.grabKeyboard.assert_called_once_with() + assert view.scene.crop_item == item + + +def test_exit_crop_mode_confirmed(view, item): + view.scene.addItem(item) + item.update = MagicMock() + item.prepareGeometryChange = MagicMock() + item.ungrabKeyboard = MagicMock() + item.crop = QtCore.QRectF(0, 0, 100, 80) + item.crop_temp = QtCore.QRectF(10, 20, 30, 40) + item.crop_mode = True + item.crop_mode_move = 'topleft' + item.crop_mode_event_start = QtCore.QRectF(1, 1, 1, 1) + + item.exit_crop_mode(confirm=True) + item.crop == QtCore.QRectF(10, 20, 30, 40) + assert item.crop_mode is False + assert item.crop_temp is None + assert item.crop_mode_move is None + assert item.crop_mode_event_start is None + item.update.assert_called() + item.prepareGeometryChange.assert_called() + item.ungrabKeyboard.assert_called_once_with() + assert view.scene.crop_item is None + view.scene.undo_stack.canUndo() is True + + +def test_exit_crop_mode_confirmed_no_change(view, item): + view.scene.addItem(item) + item.crop = QtCore.QRectF(0, 0, 100, 80) + item.crop_temp = QtCore.QRectF(0, 0, 100, 80) + item.crop_mode = True + + item.exit_crop_mode(confirm=True) + assert item.crop == QtCore.QRectF(0, 0, 100, 80) + view.scene.undo_stack.canUndo() is False + + +def test_exit_crop_mode_not_confirmed(view, item): + view.scene.addItem(item) + item.update = MagicMock() + item.prepareGeometryChange = MagicMock() + item.ungrabKeyboard = MagicMock() + item.crop = QtCore.QRectF(0, 0, 100, 80) + item.crop_temp = QtCore.QRectF(10, 20, 30, 40) + item.crop_mode = True + item.crop_mode_move = 'topleft' + item.crop_mode_event_start = QtCore.QRectF(1, 1, 1, 1) + + item.exit_crop_mode(confirm=False) + item.crop == QtCore.QRectF(0, 0, 100, 80) + assert item.crop_mode is False + assert item.crop_temp is None + assert item.crop_mode_move is None + assert item.crop_mode_event_start is None + item.update.assert_called() + item.prepareGeometryChange.assert_called() + item.ungrabKeyboard.assert_called_once_with() + assert view.scene.crop_item is None + view.scene.undo_stack.canUndo() is False + + +@patch('PyQt6.QtWidgets.QGraphicsPixmapItem.keyPressEvent') +def test_key_press_event_return(key_mock, qapp, item): + item.exit_crop_mode = MagicMock() + event = MagicMock() + event.key.return_value = Qt.Key.Key_Return + item.keyPressEvent(event) + item.exit_crop_mode.assert_called_once_with(confirm=True) + key_mock.assert_not_called() + + +@patch('PyQt6.QtWidgets.QGraphicsPixmapItem.keyPressEvent') +def test_key_press_event_escape(key_mock, qapp, item): + item.exit_crop_mode = MagicMock() + event = MagicMock() + event.key.return_value = Qt.Key.Key_Escape + item.keyPressEvent(event) + item.exit_crop_mode.assert_called_once_with(confirm=False) + key_mock.assert_not_called() + + +@patch('PyQt6.QtWidgets.QGraphicsPixmapItem.keyPressEvent') +def test_key_press_event_other(key_mock, qapp, item): + item.exit_crop_mode = MagicMock() + event = MagicMock() + event.key.return_value = Qt.Key.Key_Space + item.keyPressEvent(event) + item.exit_crop_mode.assert_not_called() + key_mock.assert_called_once_with(event) + + +@patch('beeref.selection.SelectableMixin.hoverMoveEvent') +def test_hover_move_event_when_not_crop_mode(hover_mock, qapp, item): + item.crop_mode = False + event = MagicMock() + + item.hoverMoveEvent(event) + hover_mock.assert_called_once_with(event) + + +@patch('beeref.selection.SelectableMixin.hoverMoveEvent') +def test_hover_move_event_crop_mode_inside_handle(hover_mock, qapp, item): + item.crop_mode = True + item.crop_temp = QtCore.QRectF(0, 0, 100, 80) + event = MagicMock() + event.pos.return_value = QtCore.QPointF(5, 5) + + item.hoverMoveEvent(event) + item.cursor() == Qt.CursorShape.SizeFDiagCursor + hover_mock.assert_not_called() + + +@patch('beeref.selection.SelectableMixin.hoverMoveEvent') +def test_hover_move_event_crop_mode_outside_handle(hover_mock, qapp, item): + item.crop_mode = True + item.crop_temp = QtCore.QRectF(0, 0, 100, 80) + item.setCursor(Qt.CursorShape.SizeFDiagCursor) + event = MagicMock() + event.pos.return_value = QtCore.QPointF(50, 50) + + item.hoverMoveEvent(event) + item.cursor() == Qt.CursorShape.ArrowCursor + hover_mock.assert_not_called() + + +@patch('beeref.selection.SelectableMixin.mousePressEvent') +def test_mouse_press_event_when_not_crop_mode(mouse_mock, qapp, item): + item.crop_mode = False + item.crop_mode_move = None + item.exit_crop_mode = MagicMock() + event = MagicMock() + + item.mousePressEvent(event) + assert item.crop_mode_move is None + item.exit_crop_mode.assert_not_called() + mouse_mock.assert_called_once_with(event) + event.accept.assert_not_called() + + +@patch('beeref.selection.SelectableMixin.mousePressEvent') +def test_mouse_press_event_crop_mode_inside_handle(mouse_mock, qapp, item): + item.crop_mode = True + item.crop_temp = QtCore.QRectF(0, 0, 100, 80) + item.crop_mode_move = None + item.exit_crop_mode = MagicMock() + event = MagicMock() + event.pos.return_value = QtCore.QPointF(5, 5) + + item.mousePressEvent(event) + assert item.crop_mode_move == item.crop_handle_topleft + assert item.crop_mode_event_start == QtCore.QPointF(5, 5) + item.exit_crop_mode.assert_not_called() + mouse_mock.assert_not_called() + event.accept.assert_called_once_with() + + +@patch('beeref.selection.SelectableMixin.mousePressEvent') +def test_mouse_press_event_crop_mode_outside_handle_inside_crop( + mouse_mock, qapp, item): + item.crop_mode = True + item.crop_temp = QtCore.QRectF(0, 0, 100, 80) + item.crop_mode_move = None + item.exit_crop_mode = MagicMock() + event = MagicMock() + event.pos.return_value = QtCore.QPointF(50, 50) + + item.mousePressEvent(event) + assert item.crop_mode_move is None + item.exit_crop_mode.assert_called_once_with(confirm=True) + mouse_mock.assert_not_called() + event.accept.assert_called_once_with() + + +@patch('beeref.selection.SelectableMixin.mousePressEvent') +def test_mouse_press_event_crop_mode_outside_handle_outside_crop( + mouse_mock, qapp, item): + item.crop_mode = True + item.crop_temp = QtCore.QRectF(0, 0, 100, 80) + item.crop_mode_move = None + item.exit_crop_mode = MagicMock() + event = MagicMock() + event.pos.return_value = QtCore.QPointF(150, 150) + + item.mousePressEvent(event) + assert item.crop_mode_move is None + item.exit_crop_mode.assert_called_once_with(confirm=False) + mouse_mock.assert_not_called() + event.accept.assert_called_once_with() + + +@pytest.mark.parametrize('point,expected', + [((45, 56), (45, 56)), + ((0, 0), (0, 0)), + ((-5, -5), (0, 0)), + ((100, 80), (100, 80)), + ((105, 85), (100, 80))]) +def test_ensure_point_within_pixmap_bounds_inside(point, expected, qapp, item): + pixmap = MagicMock() + pixmap.size.return_value = QtCore.QRectF(0, 0, 100, 80) + item.pixmap = MagicMock(return_value=pixmap) + result = item.ensure_point_within_pixmap_bounds(QtCore.QPointF(*point)) + assert result == QtCore.QPointF(*expected) + + +@patch('beeref.selection.SelectableMixin.mouseMoveEvent') +def test_mouse_move_when_crop_mode_bottomleft(mouse_mock, qapp, item): + pixmap = MagicMock() + pixmap.size.return_value = QtCore.QRectF(0, 0, 100, 80) + item.crop_mode = True + item.pixmap = MagicMock(return_value=pixmap) + item.crop_temp = QtCore.QRectF(10, 20, 30, 40) + item.crop_mode_event_start = QtCore.QPointF(15, 75) + item.crop_mode_move = item.crop_handle_bottomleft + event = MagicMock() + event.pos.return_value = QtCore.QPointF(10, 70) + + item.mouseMoveEvent(event) + assert item.crop_temp == QtCore.QRectF(5, 20, 35, 35) + event.accept.assert_called_once() + mouse_mock.assert_not_called() + + +@patch('beeref.selection.SelectableMixin.mouseMoveEvent') +def test_mouse_move_when_crop_mode_bottomright(mouse_mock, qapp, item): + pixmap = MagicMock() + pixmap.size.return_value = QtCore.QRectF(0, 0, 100, 80) + item.crop_mode = True + item.pixmap = MagicMock(return_value=pixmap) + item.crop_temp = QtCore.QRectF(10, 20, 30, 40) + item.crop_mode_event_start = QtCore.QPointF(95, 75) + item.crop_mode_move = item.crop_handle_bottomright + event = MagicMock() + event.pos.return_value = QtCore.QPointF(90, 70) + + item.mouseMoveEvent(event) + assert item.crop_temp == QtCore.QRectF(10, 20, 25, 35) + event.accept.assert_called_once() + mouse_mock.assert_not_called() + + +@patch('beeref.selection.SelectableMixin.mouseMoveEvent') +def test_mouse_move_when_crop_mode_topleft(mouse_mock, qapp, item): + pixmap = MagicMock() + pixmap.size.return_value = QtCore.QRectF(0, 0, 100, 80) + item.crop_mode = True + item.pixmap = MagicMock(return_value=pixmap) + item.crop_temp = QtCore.QRectF(10, 20, 30, 40) + item.crop_mode_event_start = QtCore.QPointF(15, 25) + item.crop_mode_move = item.crop_handle_topleft + event = MagicMock() + event.pos.return_value = QtCore.QPointF(10, 20) + + item.mouseMoveEvent(event) + assert item.crop_temp == QtCore.QRectF(5, 15, 35, 45) + event.accept.assert_called_once() + mouse_mock.assert_not_called() + + +@patch('beeref.selection.SelectableMixin.mouseMoveEvent') +def test_mouse_move_when_crop_mode_topright(mouse_mock, qapp, item): + pixmap = MagicMock() + pixmap.size.return_value = QtCore.QRectF(0, 0, 100, 80) + item.crop_mode = True + item.pixmap = MagicMock(return_value=pixmap) + item.crop_temp = QtCore.QRectF(10, 20, 30, 40) + item.crop_mode_event_start = QtCore.QPointF(35, 55) + item.crop_mode_move = item.crop_handle_topright + event = MagicMock() + event.pos.return_value = QtCore.QPointF(30, 50) + + item.mouseMoveEvent(event) + assert item.crop_temp == QtCore.QRectF(10, 15, 25, 45) + event.accept.assert_called_once() + mouse_mock.assert_not_called() + + +@patch('beeref.selection.SelectableMixin.mouseMoveEvent') +def test_mouse_move_when_not_crop_mode(mouse_mock, qapp, item): + event = MagicMock() + event.pos.return_value = QtCore.QPointF(30, 50) + + item.mouseMoveEvent(event) + event.accept.assert_not_called() + mouse_mock.assert_called_once_with(event) + + +@patch('beeref.selection.SelectableMixin.mouseReleaseEvent') +def test_mouse_release_event_when_crop_mode(mouse_mock, qapp, item): + event = MagicMock() + item.crop_mode = True + item.crop_mode_move = item.crop_handle_topright + item.crop_mode_event_start = QtCore.QPointF(44, 55) + + item.mouseReleaseEvent(event) + assert item.crop_mode is True + assert item.crop_mode_move is None + assert item.crop_mode_event_start is None + event.accept.assert_called_once() + mouse_mock.assert_not_called() + + +@patch('beeref.selection.SelectableMixin.mouseReleaseEvent') +def test_mouse_release_event_when_not_crop_mode(mouse_mock, qapp, item): + event = MagicMock() + item.crop_mode = False + + item.mouseReleaseEvent(event) + assert item.crop_mode is False + event.accept.assert_not_called() + mouse_mock.assert_called_once_with(event) diff --git a/tests/items/test_textitem.py b/tests/items/test_textitem.py index 6e9bb06..28a261e 100644 --- a/tests/items/test_textitem.py +++ b/tests/items/test_textitem.py @@ -1,4 +1,4 @@ -from unittest.mock import patch, MagicMock, PropertyMock +from unittest.mock import patch, MagicMock from PyQt6 import QtCore, QtWidgets from PyQt6.QtCore import Qt @@ -25,37 +25,31 @@ def test_init(selectable_mock, qapp): def test_set_pos_center(qapp): item = BeeTextItem('foo bar') - with patch('beeref.items.BeeTextItem.width', - new_callable=PropertyMock, return_value=200): - with patch('beeref.items.BeeTextItem.height', - new_callable=PropertyMock, return_value=100): - item.set_pos_center(QtCore.QPointF(0, 0)) - assert item.pos().x() == -100 - assert item.pos().y() == -50 + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 200, 100)): + item.set_pos_center(QtCore.QPointF(0, 0)) + assert item.pos().x() == -100 + assert item.pos().y() == -50 def test_set_pos_center_when_scaled(qapp): item = BeeTextItem('foo bar') item.setScale(2) - with patch('beeref.items.BeeTextItem.width', - new_callable=PropertyMock, return_value=200): - with patch('beeref.items.BeeTextItem.height', - new_callable=PropertyMock, return_value=100): - item.set_pos_center(QtCore.QPointF(0, 0)) - assert item.pos().x() == -200 - assert item.pos().y() == -100 + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 200, 100)): + item.set_pos_center(QtCore.QPointF(0, 0)) + assert item.pos().x() == -200 + assert item.pos().y() == -100 def test_set_pos_center_when_rotated(qapp): item = BeeTextItem('foo bar') item.setRotation(90) - with patch('beeref.items.BeeTextItem.width', - new_callable=PropertyMock, return_value=200): - with patch('beeref.items.BeeTextItem.height', - new_callable=PropertyMock, return_value=100): - item.set_pos_center(QtCore.QPointF(0, 0)) - assert item.pos().x() == 50 - assert item.pos().y() == -100 + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 200, 100)): + item.set_pos_center(QtCore.QPointF(0, 0)) + assert item.pos().x() == 50 + assert item.pos().y() == -100 def test_get_extra_save_data(qapp): @@ -206,21 +200,26 @@ def test_create_copy(qapp): assert item.scale() == 2.2 -def test_enter_edit_mode(qapp): +def test_enter_edit_mode(view): item = BeeTextItem('foo bar') + view.scene.addItem(item) item.enter_edit_mode() assert item.edit_mode is True + assert view.scene.edit_item == item flags = item.textInteractionFlags() assert flags == Qt.TextInteractionFlag.TextEditorInteraction @patch('PyQt6.QtGui.QTextCursor') @patch('beeref.items.BeeTextItem.setTextCursor') -def test_exit_edit_mode(setcursor_mock, cursor_mock, qapp): +def test_exit_edit_mode(setcursor_mock, cursor_mock, view): item = BeeTextItem('foo bar') item.edit_mode = True + view.scene.addItem(item) + view.scene.edit_item = item item.exit_edit_mode() assert item.edit_mode is False + assert view.scene.edit_item is None flags = item.textInteractionFlags() assert flags == Qt.TextInteractionFlag.NoTextInteraction cursor_mock.assert_called_once_with(item.document()) diff --git a/tests/selection/test_base_item_mixin.py b/tests/selection/test_base_item_mixin.py index 3b84729..7a80f97 100644 --- a/tests/selection/test_base_item_mixin.py +++ b/tests/selection/test_base_item_mixin.py @@ -1,4 +1,4 @@ -from unittest.mock import patch, MagicMock, PropertyMock +from unittest.mock import patch, MagicMock from PyQt6 import QtCore, QtGui @@ -118,12 +118,73 @@ def test_do_flip_vertical_anchor(qapp, item): assert item.pos() == QtCore.QPointF(0, 200) -def test_base_item_mixin_center_scene_coords(view, item): +def test_width(view, item): view.scene.addItem(item) item.setPos(5, 5) item.setScale(2) - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): - assert item.center_scene_coords == QtCore.QPointF(105, 85) + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + assert item.width == 100 + + +def test_width_cropped_item(view, item): + view.scene.addItem(item) + item.setPos(5, 5) + item.setScale(2) + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(5, 5, 100, 80)): + assert item.width == 100 + + +def test_height(view, item): + view.scene.addItem(item) + item.setPos(5, 5) + item.setScale(2) + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + assert item.height == 80 + + +def test_height_cropped_item(view, item): + view.scene.addItem(item) + item.setPos(5, 5) + item.setScale(2) + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(5, 5, 100, 80)): + assert item.height == 80 + + +def test_center(view, item): + view.scene.addItem(item) + item.setPos(5, 5) + item.setScale(2) + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + assert item.center == QtCore.QPointF(50, 40) + + +def test_center_cropped_item(view, item): + view.scene.addItem(item) + item.setPos(5, 5) + item.setScale(2) + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(10, 10, 40, 30)): + assert item.center == QtCore.QPointF(30, 25) + + +def test_center_scene_coords(view, item): + view.scene.addItem(item) + item.setPos(5, 5) + item.setScale(2) + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + assert item.center_scene_coords == QtCore.QPointF(105, 85) + + +def test_center_scene_coords_cropped_item(view, item): + view.scene.addItem(item) + item.setPos(5, 5) + item.setScale(2) + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(10, 10, 40, 30)): + assert item.center_scene_coords == QtCore.QPointF(65, 55) diff --git a/tests/selection/test_multi_select_item.py b/tests/selection/test_multi_select_item.py index 348808f..bc18728 100644 --- a/tests/selection/test_multi_select_item.py +++ b/tests/selection/test_multi_select_item.py @@ -14,18 +14,6 @@ def test_init(selectable_mock): selectable_mock.assert_called_once() -def test_width(): - item = MultiSelectItem() - item.setRect(0, 0, 50, 100) - assert item.width == 50 - - -def test_height(): - item = MultiSelectItem() - item.setRect(0, 0, 50, 100) - assert item.height == 100 - - def test_paint(): item = MultiSelectItem() item.paint_selectable = MagicMock() @@ -67,8 +55,8 @@ def test_fit_selection_area(): item.fit_selection_area(QtCore.QRectF(-10, -20, 100, 80)) assert item.pos().x() == -10 assert item.pos().y() == -20 - assert item.width == 100 - assert item.height == 80 + assert item.width == 101 + assert item.height == 81 assert item.scale() == 1 assert item.rotation() == 0 assert item.flip() == 1 diff --git a/tests/selection/test_rubberband_item.py b/tests/selection/test_rubberband_item.py index e10cc09..bd9fa16 100644 --- a/tests/selection/test_rubberband_item.py +++ b/tests/selection/test_rubberband_item.py @@ -3,18 +3,6 @@ from PyQt6 import QtCore from beeref.selection import RubberbandItem -def test_width(): - item = RubberbandItem() - item.setRect(5, 5, 100, 80) - assert item.width == 100 - - -def test_height(): - item = RubberbandItem() - item.setRect(5, 5, 100, 80) - assert item.height == 80 - - def test_fit_topleft_to_bottomright(): item = RubberbandItem() item.fit(QtCore.QPointF(-10, -20), QtCore.QPointF(30, 40)) diff --git a/tests/selection/test_selectable_mixin.py b/tests/selection/test_selectable_mixin.py index cd24db8..d94bd1a 100644 --- a/tests/selection/test_selectable_mixin.py +++ b/tests/selection/test_selectable_mixin.py @@ -1,6 +1,6 @@ import math from pytest import approx, mark -from unittest.mock import patch, MagicMock, PropertyMock +from unittest.mock import patch, MagicMock from PyQt6 import QtCore, QtGui from PyQt6.QtCore import Qt @@ -8,31 +8,6 @@ from PyQt6.QtCore import Qt from beeref.assets import BeeAssets from beeref import commands from beeref.items import BeePixmapItem -from beeref.scene import BeeGraphicsScene - - -@mark.skip -class SelectableMixinBaseTestCase(): - - def setUp(self): - self.scene = BeeGraphicsScene(None) - self.item = BeePixmapItem(QtGui.QImage()) - self.scene.addItem(self.item) - self.view = MagicMock(get_scale=MagicMock(return_value=1)) - views_patcher = patch('beeref.scene.BeeGraphicsScene.views', - return_value=[self.view]) - views_patcher.start() - self.addCleanup(views_patcher.stop) - width_patcher = patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, - return_value=100) - width_patcher.start() - self.addCleanup(width_patcher.stop) - height_patcher = patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, - return_value=80) - height_patcher.start() - self.addCleanup(height_patcher.stop) def test_init_selectable(view): @@ -195,43 +170,37 @@ def test_paint_when_debug_handles(view): def test_corners(view, item): - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): - assert len(item.corners) == 4 - assert QtCore.QPointF(0, 0) in item.corners - assert QtCore.QPointF(100, 0) in item.corners - assert QtCore.QPointF(0, 80) in item.corners - assert QtCore.QPointF(100, 80) in item.corners + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + assert len(item.corners) == 4 + assert QtCore.QPointF(0, 0) in item.corners + assert QtCore.QPointF(100, 0) in item.corners + assert QtCore.QPointF(0, 80) in item.corners + assert QtCore.QPointF(100, 80) in item.corners def test_corners_scene_coords_translated(view, item): item.setPos(5, 5) - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): - corners = item.corners_scene_coords - assert len(item.corners) == 4 - assert QtCore.QPointF(5, 5) in corners - assert QtCore.QPointF(105, 5) in corners - assert QtCore.QPointF(5, 85) in corners - assert QtCore.QPointF(105, 85) in corners + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + corners = item.corners_scene_coords + assert len(corners) == 4 + assert QtCore.QPointF(5, 5) in corners + assert QtCore.QPointF(105, 5) in corners + assert QtCore.QPointF(5, 85) in corners + assert QtCore.QPointF(105, 85) in corners def test_corners_scene_coords_scaled(view, item): item.setScale(2) - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): - corners = item.corners_scene_coords - assert len(item.corners) == 4 - assert QtCore.QPointF(0, 0) in corners - assert QtCore.QPointF(200, 0) in corners - assert QtCore.QPointF(0, 160) in corners - assert QtCore.QPointF(200, 160) in corners + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + corners = item.corners_scene_coords + assert len(corners) == 4 + assert QtCore.QPointF(0, 0) in corners + assert QtCore.QPointF(200, 0) in corners + assert QtCore.QPointF(0, 160) in corners + assert QtCore.QPointF(200, 160) in corners def test_get_scale_bounds(view, item): @@ -279,29 +248,47 @@ def test_rotate_bounds_topleft(view, item): def test_get_flip_bounds(view, item): item.SELECT_RESIZE_SIZE = 10 item.SELECT_ROTATE_SIZE = 10 - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): - edges = item.get_flip_bounds() - assert edges[0]['rect'].topLeft() == QtCore.QPointF(5, -5) - assert edges[0]['rect'].bottomRight() == QtCore.QPointF(95, 5) - assert edges[0]['flip_v'] is True - assert edges[1]['rect'].topLeft() == QtCore.QPointF(5, 75) - assert edges[1]['rect'].bottomRight() == QtCore.QPointF(95, 85) - assert edges[1]['flip_v'] is True - assert edges[2]['rect'].topLeft() == QtCore.QPointF(-5, 5) - assert edges[2]['rect'].bottomRight() == QtCore.QPointF(5, 75) - assert edges[2]['flip_v'] is False - assert edges[3]['rect'].topLeft() == QtCore.QPointF(95, 5) - assert edges[3]['rect'].bottomRight() == QtCore.QPointF(105, 75) - assert edges[3]['flip_v'] is False + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + edges = item.get_flip_bounds() + assert edges[0]['rect'].topLeft() == QtCore.QPointF(5, -5) + assert edges[0]['rect'].bottomRight() == QtCore.QPointF(95, 5) + assert edges[0]['flip_v'] is True + assert edges[1]['rect'].topLeft() == QtCore.QPointF(5, 75) + assert edges[1]['rect'].bottomRight() == QtCore.QPointF(95, 85) + assert edges[1]['flip_v'] is True + assert edges[2]['rect'].topLeft() == QtCore.QPointF(-5, 5) + assert edges[2]['rect'].bottomRight() == QtCore.QPointF(5, 75) + assert edges[2]['flip_v'] is False + assert edges[3]['rect'].topLeft() == QtCore.QPointF(95, 5) + assert edges[3]['rect'].bottomRight() == QtCore.QPointF(105, 75) + assert edges[3]['flip_v'] is False + + +def test_get_flip_bounds_cropped_item(view, item): + item.SELECT_RESIZE_SIZE = 10 + item.SELECT_ROTATE_SIZE = 10 + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(5, 5, 100, 80)): + edges = item.get_flip_bounds() + assert edges[0]['rect'].topLeft() == QtCore.QPointF(10, 0) + assert edges[0]['rect'].bottomRight() == QtCore.QPointF(100, 10) + assert edges[0]['flip_v'] is True + assert edges[1]['rect'].topLeft() == QtCore.QPointF(10, 80) + assert edges[1]['rect'].bottomRight() == QtCore.QPointF(100, 90) + assert edges[1]['flip_v'] is True + assert edges[2]['rect'].topLeft() == QtCore.QPointF(0, 10) + assert edges[2]['rect'].bottomRight() == QtCore.QPointF(10, 80) + assert edges[2]['flip_v'] is False + assert edges[3]['rect'].topLeft() == QtCore.QPointF(100, 10) + assert edges[3]['rect'].bottomRight() == QtCore.QPointF(110, 80) + assert edges[3]['flip_v'] is False def test_bounding_rect_when_not_selected(view, item): item.setSelected(False) - with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.boundingRect', - return_value=QtCore.QRectF(0, 0, 100, 80)): + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): rect = item.boundingRect() assert rect.topLeft().x() == 0 assert rect.topLeft().y() == 0 @@ -313,22 +300,20 @@ def test_bounding_rect_when_selected(view, item): item.SELECT_RESIZE_SIZE = 10 item.SELECT_ROTATE_SIZE = 10 item.setSelected(True) - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): - rect = item.boundingRect() - assert rect.topLeft().x() == -15 - assert rect.topLeft().y() == -15 - assert rect.bottomRight().x() == 115 - assert rect.bottomRight().y() == 95 + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + rect = item.boundingRect() + assert rect.topLeft().x() == -15 + assert rect.topLeft().y() == -15 + assert rect.bottomRight().x() == 115 + assert rect.bottomRight().y() == 95 def test_shape_when_not_selected(view, item): view.scene.addItem(item) item.setSelected(False) - with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.boundingRect', - return_value=QtCore.QRectF(0, 0, 100, 80)): + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): shape = item.shape().boundingRect() assert shape.topLeft().x() == 0 assert shape.topLeft().y() == 0 @@ -344,17 +329,13 @@ def test_shape_when_selected_single(view, item): path = QtGui.QPainterPath() path.addRect(QtCore.QRectF(0, 0, 100, 80)) - with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.shape', - return_value=path): - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): - shape = item.shape().boundingRect() - assert shape.topLeft().x() == -15 - assert shape.topLeft().y() == -15 - assert shape.bottomRight().x() == 115 - assert shape.bottomRight().y() == 95 + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + shape = item.shape().boundingRect() + assert shape.topLeft().x() == -15 + assert shape.topLeft().y() == -15 + assert shape.bottomRight().x() == 115 + assert shape.bottomRight().y() == 95 def test_shape_when_selected_multi(view, item): @@ -366,8 +347,8 @@ def test_shape_when_selected_multi(view, item): item.SELECT_ROTATE_SIZE = 10 item.setSelected(True) - with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.boundingRect', - return_value=QtCore.QRectF(0, 0, 100, 80)): + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): shape = item.shape().boundingRect() assert shape.topLeft().x() == 0 assert shape.topLeft().y() == 0 @@ -381,11 +362,20 @@ def test_get_scale_factor_bottomright(view, item): item.scale_orig_factor = 1 event = MagicMock() event.scenePos.return_value = QtCore.QPointF(20, 90) - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): - assert item.get_scale_factor(event) == approx(1.5, 0.01) + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + assert item.get_scale_factor(event) == approx(1.5, 0.01) + + +def test_get_scale_factor_bottomright_cropped_item(view, item): + item.event_start = QtCore.QPointF(15, 15) + item.event_direction = QtCore.QPointF(1, 1) / math.sqrt(2) + item.scale_orig_factor = 1 + event = MagicMock() + event.scenePos.return_value = QtCore.QPointF(25, 95) + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(5, 5, 100, 80)): + assert item.get_scale_factor(event) == approx(1.5, 0.01) def test_get_scale_factor_topleft(view, item): @@ -394,115 +384,176 @@ def test_get_scale_factor_topleft(view, item): item.scale_orig_factor = 0.5 event = MagicMock() event.scenePos.return_value = QtCore.QPointF(-10, -60) - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): - assert item.get_scale_factor(event) == approx(2, 0.01) + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + assert item.get_scale_factor(event) == approx(2, 0.01) + + +def test_get_scale_factor_topleft_cropped_item(view, item): + item.event_start = QtCore.QPointF(15, 15) + item.event_direction = QtCore.QPointF(-1, -1) / math.sqrt(2) + item.scale_orig_factor = 0.5 + event = MagicMock() + event.scenePos.return_value = QtCore.QPointF(-5, -55) + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(5, 5, 100, 80)): + assert item.get_scale_factor(event) == approx(2, 0.01) def test_get_scale_anchor_topleft(view, item): - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): - anchor = item.get_scale_anchor(QtCore.QPointF(0, 0)) - assert anchor.x() == 100 - assert anchor.y() == 80 + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + anchor = item.get_scale_anchor(QtCore.QPointF(0, 0)) + assert anchor.x() == 100 + assert anchor.y() == 80 + + +def test_get_scale_anchor_topleft_cropped_item(view, item): + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(5, 5, 100, 80)): + anchor = item.get_scale_anchor(QtCore.QPointF(5, 5)) + assert anchor.x() == 105 + assert anchor.y() == 85 def test_get_scale_anchor_bottomright(view, item): - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): - anchor = item.get_scale_anchor(QtCore.QPointF(100, 80)) - assert anchor.x() == 0 - assert anchor.y() == 0 + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + anchor = item.get_scale_anchor(QtCore.QPointF(100, 80)) + assert anchor.x() == 0 + assert anchor.y() == 0 + + +def test_get_scale_anchor_bottomright_cropped_item(view, item): + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(5, 5, 100, 80)): + anchor = item.get_scale_anchor(QtCore.QPointF(105, 85)) + assert anchor.x() == 5 + assert anchor.y() == 5 def test_get_scale_anchor_topright(view, item): - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): - anchor = item.get_scale_anchor(QtCore.QPointF(100, 0)) - assert anchor.x() == 0 - assert anchor.y() == 80 + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + anchor = item.get_scale_anchor(QtCore.QPointF(100, 0)) + assert anchor.x() == 0 + assert anchor.y() == 80 + + +def test_get_scale_anchor_topright_cropped_item(view, item): + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(5, 5, 100, 80)): + anchor = item.get_scale_anchor(QtCore.QPointF(105, 5)) + assert anchor.x() == 5 + assert anchor.y() == 85 def test_get_scale_anchor_bottomleft(view, item): - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): - anchor = item.get_scale_anchor(QtCore.QPointF(0, 80)) - assert anchor.x() == 100 - assert anchor.y() == 0 + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + anchor = item.get_scale_anchor(QtCore.QPointF(0, 80)) + assert anchor.x() == 100 + assert anchor.y() == 0 + + +def test_get_scale_anchor_bottomleft_cropped_item(view, item): + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(5, 5, 100, 80)): + anchor = item.get_scale_anchor(QtCore.QPointF(5, 85)) + assert anchor.x() == 105 + assert anchor.y() == 5 def test_get_corner_direction_topleft(view, item): - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): - assert item.get_corner_direction( - QtCore.QPointF(0, 0)) == QtCore.QPointF(-1, -1) + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + assert item.get_corner_direction( + QtCore.QPointF(0, 0)) == QtCore.QPointF(-1, -1) + + +def test_get_corner_direction_topleft_cropped_item(view, item): + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(5, 5, 100, 80)): + assert item.get_corner_direction( + QtCore.QPointF(5, 5)) == QtCore.QPointF(-1, -1) def test_get_corner_direction_bottomright(view, item): - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): - assert item.get_corner_direction( - QtCore.QPointF(100, 80)) == QtCore.QPointF(1, 1) + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + assert item.get_corner_direction( + QtCore.QPointF(100, 80)) == QtCore.QPointF(1, 1) + + +def test_get_corner_direction_bottomright_cropped_item(view, item): + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(5, 5, 100, 80)): + assert item.get_corner_direction( + QtCore.QPointF(105, 85)) == QtCore.QPointF(1, 1) def test_get_corner_direction_topright(view, item): - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): - assert item.get_corner_direction( - QtCore.QPointF(100, 0)) == QtCore.QPointF(1, -1) + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + assert item.get_corner_direction( + QtCore.QPointF(100, 0)) == QtCore.QPointF(1, -1) + + +def test_get_corner_direction_topright_cropped_item(view, item): + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(5, 5, 100, 80)): + assert item.get_corner_direction( + QtCore.QPointF(105, 5)) == QtCore.QPointF(1, -1) def test_get_corner_direction_bottomleft(view, item): - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): - assert item.get_corner_direction( - QtCore.QPointF(0, 80)) == QtCore.QPointF(-1, 1) + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + assert item.get_corner_direction( + QtCore.QPointF(0, 80)) == QtCore.QPointF(-1, 1) -def test_get_direction_from_center_bottomright(view, item): - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): - direction = item.get_direction_from_center(QtCore.QPointF(100, 90)) - assert direction == approx(QtCore.QPointF(1, 1) / math.sqrt(2)) +def test_get_corner_direction_bottomleft_cropped_item(view, item): + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(5, 5, 100, 80)): + assert item.get_corner_direction( + QtCore.QPointF(5, 85)) == QtCore.QPointF(-1, 1) def test_get_direction_from_center_topleft(view, item): - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): - direction = item.get_direction_from_center(QtCore.QPointF(0, -10)) - assert direction == approx(QtCore.QPointF(-1, -1) / math.sqrt(2)) + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + direction = item.get_direction_from_center(QtCore.QPointF(0, -10)) + assert direction == approx(QtCore.QPointF(-1, -1) / math.sqrt(2)) + + +def test_get_direction_from_center_topleft_cropped_item(view, item): + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(5, 5, 100, 80)): + direction = item.get_direction_from_center(QtCore.QPointF(5, -5)) + assert direction == approx(QtCore.QPointF(-1, -1) / math.sqrt(2)) + + +def test_get_direction_from_center_bottomright(view, item): + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + direction = item.get_direction_from_center(QtCore.QPointF(100, 90)) + assert direction == approx(QtCore.QPointF(1, 1) / math.sqrt(2)) + + +def test_get_direction_from_center_bottomright_cropped_item(view, item): + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(5, 5, 100, 80)): + direction = item.get_direction_from_center(QtCore.QPointF(105, 95)) + assert direction == approx(QtCore.QPointF(1, 1) / math.sqrt(2)) def test_get_direction_from_center_bottomright_when_rotated_180(view, item): item.setRotation(180, QtCore.QPointF(50, 40)) - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): - direction = item.get_direction_from_center(QtCore.QPointF(100, 90)) - assert direction == approx(QtCore.QPointF(1, 1) / math.sqrt(2)) + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + direction = item.get_direction_from_center(QtCore.QPointF(100, 90)) + assert direction == approx(QtCore.QPointF(1, 1) / math.sqrt(2)) def test_get_rotate_angle(view, item): @@ -548,12 +599,10 @@ def test_hover_move_event_no_selection(view, item): event = MagicMock() event.pos.return_value = QtCore.QPointF(0, 0) item.setCursor = MagicMock() - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): - item.hoverMoveEvent(event) - item.setCursor.assert_not_called() + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + item.hoverMoveEvent(event) + item.setCursor.assert_not_called() @mark.parametrize('pos,flipped,rotation, expected', @@ -575,7 +624,7 @@ def test_hover_move_event_no_selection(view, item): ((0, 0), True, 45, 'SizeVerCursor'), ((0, 0), True, 90, 'SizeFDiagCursor'), ((0, 0), True, 135, 'SizeHorCursor')]) -def test_hover_move_event_topleft_scale( +def test_hover_move_event_scale( pos, flipped, rotation, expected, view, item): view.scene.addItem(item) item.setSelected(True) @@ -584,25 +633,21 @@ def test_hover_move_event_topleft_scale( item.setRotation(rotation) event = MagicMock() event.pos.return_value = QtCore.QPointF(*pos) - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): - item.hoverMoveEvent(event) - assert item.cursor() == getattr(Qt.CursorShape, expected) + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + item.hoverMoveEvent(event) + assert item.cursor() == getattr(Qt.CursorShape, expected) -def test_hover_move_event_bottomright_scale_very_wide_item(view, item): +def test_hover_move_event_scale_bottomright_very_wide_item(view, item): view.scene.addItem(item) item.setSelected(True) event = MagicMock() event.pos.return_value = QtCore.QPointF(1000, 100) - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=1000): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=100): - item.hoverMoveEvent(event) - assert item.cursor() == Qt.CursorShape.SizeFDiagCursor + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 1000, 100)): + item.hoverMoveEvent(event) + assert item.cursor() == Qt.CursorShape.SizeFDiagCursor def test_hover_move_event_rotate(view, item): @@ -610,12 +655,10 @@ def test_hover_move_event_rotate(view, item): item.setSelected(True) event = MagicMock() event.pos.return_value = QtCore.QPointF(115, 95) - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): - item.hoverMoveEvent(event) - assert item.cursor() == BeeAssets().cursor_rotate + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + item.hoverMoveEvent(event) + assert item.cursor() == BeeAssets().cursor_rotate def test_hover_flip_event_top_edge(view, item): @@ -623,12 +666,10 @@ def test_hover_flip_event_top_edge(view, item): item.setSelected(True) event = MagicMock() event.pos.return_value = QtCore.QPointF(50, 0) - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): - item.hoverMoveEvent(event) - assert item.cursor() == BeeAssets().cursor_flip_v + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + item.hoverMoveEvent(event) + assert item.cursor() == BeeAssets().cursor_flip_v def test_hover_flip_event_bottom_edge(view, item): @@ -636,12 +677,10 @@ def test_hover_flip_event_bottom_edge(view, item): item.setSelected(True) event = MagicMock() event.pos.return_value = QtCore.QPointF(50, 80) - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): - item.hoverMoveEvent(event) - assert item.cursor() == BeeAssets().cursor_flip_v + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + item.hoverMoveEvent(event) + assert item.cursor() == BeeAssets().cursor_flip_v def test_hover_flip_event_left_edge(view, item): @@ -649,12 +688,10 @@ def test_hover_flip_event_left_edge(view, item): item.setSelected(True) event = MagicMock() event.pos.return_value = QtCore.QPointF(0, 50) - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): - item.hoverMoveEvent(event) - assert item.cursor() == BeeAssets().cursor_flip_h + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + item.hoverMoveEvent(event) + assert item.cursor() == BeeAssets().cursor_flip_h def test_hover_flip_event_right_edge(view, item): @@ -662,12 +699,10 @@ def test_hover_flip_event_right_edge(view, item): item.setSelected(True) event = MagicMock() event.pos.return_value = QtCore.QPointF(100, 50) - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): - item.hoverMoveEvent(event) - assert item.cursor() == BeeAssets().cursor_flip_h + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + item.hoverMoveEvent(event) + assert item.cursor() == BeeAssets().cursor_flip_h def test_hover_flip_event_top_edge_rotated_90(view, item): @@ -676,12 +711,10 @@ def test_hover_flip_event_top_edge_rotated_90(view, item): item.setRotation(90) event = MagicMock() event.pos.return_value = QtCore.QPointF(50, 0) - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): - item.hoverMoveEvent(event) - assert item.cursor() == BeeAssets().cursor_flip_h + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + item.hoverMoveEvent(event) + assert item.cursor() == BeeAssets().cursor_flip_h def test_hover_flip_event_left_edge_when_rotated_90(view, item): @@ -690,12 +723,10 @@ def test_hover_flip_event_left_edge_when_rotated_90(view, item): item.setSelected(True) item.setRotation(90) event.pos.return_value = QtCore.QPointF(0, 50) - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): - item.hoverMoveEvent(event) - assert item.cursor() == BeeAssets().cursor_flip_v + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + item.hoverMoveEvent(event) + assert item.cursor() == BeeAssets().cursor_flip_v def test_hover_move_event_not_in_handles(view, item): @@ -703,12 +734,10 @@ def test_hover_move_event_not_in_handles(view, item): item.setSelected(True) event = MagicMock() event.pos.return_value = QtCore.QPointF(50, 50) - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): - item.hoverMoveEvent(event) - assert item.cursor() == Qt.CursorShape.ArrowCursor + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + item.hoverMoveEvent(event) + assert item.cursor() == Qt.CursorShape.ArrowCursor def test_hover_enter_event_when_selected(view, item): @@ -772,17 +801,15 @@ def test_mouse_press_event_bottomright_scale(view, item): event.pos.return_value = QtCore.QPointF(99, 79) event.scenePos.return_value = QtCore.QPointF(101, 81) event.button.return_value = Qt.MouseButton.LeftButton - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): - item.mousePressEvent(event) - assert item.scale_active is True - assert item.event_start == QtCore.QPointF(101, 81) - assert item.event_direction.x() > 0 - assert item.event_direction.y() > 0 - assert item.scale_orig_factor == 1 - event.accept.assert_called_once_with() + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + item.mousePressEvent(event) + assert item.scale_active is True + assert item.event_start == QtCore.QPointF(101, 81) + assert item.event_direction.x() > 0 + assert item.event_direction.y() > 0 + assert item.scale_orig_factor == 1 + event.accept.assert_called_once_with() def test_mouse_press_event_rotate(view, item): @@ -792,10 +819,9 @@ def test_mouse_press_event_rotate(view, item): event.pos.return_value = QtCore.QPointF(111, 91) event.scenePos.return_value = QtCore.QPointF(66, 99) event.button.return_value = Qt.MouseButton.LeftButton - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.mousePressEvent'): item.mousePressEvent(event) assert item.rotate_active is True assert item.event_anchor == QtCore.QPointF(50, 40) @@ -810,12 +836,10 @@ def test_mouse_press_event_flip(view, item): event.pos.return_value = QtCore.QPointF(0, 40) event.button.return_value = Qt.MouseButton.LeftButton view.scene.undo_stack = MagicMock(push=MagicMock()) - with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.mousePressEvent'): - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): - item.mousePressEvent(event) + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.mousePressEvent'): + item.mousePressEvent(event) args = view.scene.undo_stack.push.call_args_list[0][0] cmd = args[0] isinstance(cmd, commands.FlipItems) @@ -893,14 +917,12 @@ def test_mouse_move_event_when_scale_action(view, item): item.scale_orig_factor = 1 with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.mouseMoveEvent') as m: - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): - item.mouseMoveEvent(event) - m.assert_not_called() - assert item.scale() == approx(1.5, 0.01) - event.accept.assert_called_once_with() + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + item.mouseMoveEvent(event) + m.assert_not_called() + assert item.scale() == approx(1.5, 0.01) + event.accept.assert_called_once_with() def test_mouse_move_event_when_rotate_action(view, item): @@ -955,21 +977,19 @@ def test_mouse_release_event_when_scale_action(view, item): item.scale_orig_factor = 1 view.scene.undo_stack = MagicMock(push=MagicMock()) - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): - item.mouseReleaseEvent(event) - view.scene.undo_stack.push.assert_called_once() - args = view.scene.undo_stack.push.call_args_list[0][0] - cmd = args[0] - isinstance(cmd, commands.ScaleItemsBy) - assert cmd.items == [item] - assert cmd.factor == approx(1.5, 0.01) - assert cmd.anchor == QtCore.QPointF(100, 80) - assert cmd.ignore_first_redo is True - assert item.scale_active is False - event.accept.assert_called_once_with() + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + item.mouseReleaseEvent(event) + view.scene.undo_stack.push.assert_called_once() + args = view.scene.undo_stack.push.call_args_list[0][0] + cmd = args[0] + isinstance(cmd, commands.ScaleItemsBy) + assert cmd.items == [item] + assert cmd.factor == approx(1.5, 0.01) + assert cmd.anchor == QtCore.QPointF(100, 80) + assert cmd.ignore_first_redo is True + assert item.scale_active is False + event.accept.assert_called_once_with() def test_mouse_release_event_when_scale_action_zero(view, item): @@ -983,14 +1003,12 @@ def test_mouse_release_event_when_scale_action_zero(view, item): item.scale_orig_factor = 1 view.scene.undo_stack = MagicMock(push=MagicMock()) - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): - item.mouseReleaseEvent(event) - view.scene.undo_stack.push.assert_not_called() - assert item.scale_active is False - event.accept.assert_called_once_with() + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + item.mouseReleaseEvent(event) + view.scene.undo_stack.push.assert_not_called() + assert item.scale_active is False + event.accept.assert_called_once_with() def test_mouse_release_event_when_rotate_action(view, item): @@ -1039,11 +1057,9 @@ def test_mouse_release_event_when_flip_action(view, item): item.flip_active = True view.scene.undo_stack = MagicMock(push=MagicMock()) - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): - item.mouseReleaseEvent(event) + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + item.mouseReleaseEvent(event) view.scene.undo_stack.push.assert_not_called() assert item.flip_active is False event.accept.assert_called_once_with() diff --git a/tests/test_commands.py b/tests/test_commands.py index 93a3edc..8e67104 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -1,9 +1,9 @@ -from unittest.mock import MagicMock, patch, PropertyMock +from unittest.mock import MagicMock, patch from PyQt6 import QtCore, QtGui from beeref import commands -from beeref.items import BeePixmapItem +from beeref.items import BeePixmapItem, BeeTextItem def test_insert_items(view): @@ -36,10 +36,10 @@ def test_insert_items_with_position(view): item2.setPos(50, 40) view.scene.addItem(item2) - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): + with patch.object(item1, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + with patch.object(item2, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): command = commands.InsertItems( view.scene, [item1, item2], QtCore.QPointF(100, 200)) command.redo() @@ -231,10 +231,10 @@ def test_normalize_items(qapp): item1.setScale(1) item2 = BeePixmapItem(QtGui.QImage()) item2.setScale(3) - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): + with patch.object(item1, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + with patch.object(item2, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): command = commands.NormalizeItems([item1, item2], [2, 0.5]) command.redo() assert item1.scale() == 2 @@ -304,46 +304,42 @@ def test_flip_items_vertical(qapp): def test_reset_scale(view, item): item.setScale(2) - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): - command = commands.ResetScale([item]) - command.redo() - assert item.scale() == 1 - assert item.pos().x() == 50 - assert item.pos().y() == 40 - command.undo() - assert item.scale() == 2 - assert item.pos().x() == 0 - assert item.pos().y() == 0 + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + command = commands.ResetScale([item]) + command.redo() + assert item.scale() == 1 + assert item.pos().x() == 50 + assert item.pos().y() == 40 + command.undo() + assert item.scale() == 2 + assert item.pos().x() == 0 + assert item.pos().y() == 0 def test_reset_rotate(view, item): item.setRotation(180) - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): - command = commands.ResetRotation([item]) - command.redo() - assert item.rotation() == 0 - assert item.pos().x() == -100 - assert item.pos().y() == -80 - command.undo() - assert item.rotation() == 180 - assert item.pos().x() == 0 - assert item.pos().y() == 0 + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + command = commands.ResetRotation([item]) + command.redo() + assert item.rotation() == 0 + assert item.pos().x() == -100 + assert item.pos().y() == -80 + command.undo() + assert item.rotation() == 180 + assert item.pos().x() == 0 + assert item.pos().y() == 0 def test_reset_flip(qapp): item1 = BeePixmapItem(QtGui.QImage()) item1.do_flip() item2 = BeePixmapItem(QtGui.QImage()) - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): + with patch.object(item1, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + with patch.object(item2, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): command = commands.ResetFlip([item1, item2]) command.redo() assert item1.flip() == 1 @@ -361,17 +357,53 @@ def test_reset_flip(qapp): assert item2.pos().y() == 0 +def test_reset_crop(qapp): + item1 = BeePixmapItem(QtGui.QImage()) + item1.crop = QtCore.QRectF(10, 20, 30, 50) + item2 = BeePixmapItem(QtGui.QImage()) + item2.crop = QtCore.QRectF(5, 6, 55, 66) + with patch.object(BeePixmapItem, 'reset_crop'): + command = commands.ResetCrop([item1, item2]) + command.redo() + assert BeePixmapItem.reset_crop.call_count == 2 + assert item1.pos() == QtCore.QPointF(0, 0) + assert item2.pos() == QtCore.QPointF(0, 0) + + item1.crop = QtCore.QRectF(0, 0, 0, 0) + item2.crop = QtCore.QRectF(0, 0, 0, 0) + command.undo() + assert item1.crop == QtCore.QRectF(10, 20, 30, 50) + assert item1.pos() == QtCore.QPointF(0, 0) + assert item2.crop == QtCore.QRectF(5, 6, 55, 66) + assert item2.pos() == QtCore.QPointF(0, 0) + + +def test_reset_crop_ignores_uncroppable(qapp): + item = BeeTextItem('foo') + brect = item.boundingRect() + command = commands.ResetCrop([item]) + command.redo() + assert item.pos() == QtCore.QPointF(0, 0) + assert item.boundingRect() == brect + command.undo() + assert item.pos() == QtCore.QPointF(0, 0) + assert item.boundingRect() == brect + + def test_reset_transforms(qapp): item1 = BeePixmapItem(QtGui.QImage()) item1.setScale(2) item1.do_flip() - item2 = BeePixmapItem(QtGui.QImage()) + item2 = BeeTextItem('foo') item2.setRotation(180) - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): - command = commands.ResetTransforms([item1, item2]) + item3 = BeePixmapItem(QtGui.QImage()) + item3.crop = QtCore.QRectF(10, 20, 30, 40) + item3.reset_crop = MagicMock() + with patch.object(item1, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + with patch.object(item2, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + command = commands.ResetTransforms([item1, item2, item3]) command.redo() assert item1.scale() == 1 assert item1.rotation() == 0 @@ -383,6 +415,9 @@ def test_reset_transforms(qapp): assert item2.flip() == 1 assert item2.pos().x() == -100 assert item2.pos().y() == -80 + item3.reset_crop.assert_called_once_with() + + item3.crop = QtCore.QRectF(0, 0, 0, 0) command.undo() assert item1.scale() == 2 assert item1.rotation() == 0 @@ -394,6 +429,7 @@ def test_reset_transforms(qapp): assert item2.flip() == 1 assert item2.pos().x() == 0 assert item2.pos().y() == 0 + assert item3.crop == QtCore.QRectF(10, 20, 30, 40) def test_arrange_items(view): @@ -403,18 +439,38 @@ def test_arrange_items(view): item2 = BeePixmapItem(QtGui.QImage()) item2.setRotation(90) view.scene.addItem(item2) - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): - command = commands.ArrangeItems( - view.scene, - [item1, item2], - [QtCore.QPointF(1, 2), QtCore.QPointF(203, 204)]) + item3 = BeePixmapItem(QtGui.QImage()) + view.scene.addItem(item3) + with patch.object(item1, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + with patch.object(item2, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + with patch.object(item3, 'bounding_rect_unselected', + return_value=QtCore.QRectF(5, 5, 20, 30)): - command.redo() - assert item1.pos() == QtCore.QPointF(101, 2) - assert item2.pos() == QtCore.QPointF(283, 204) - command.undo() - assert item1.pos() == QtCore.QPointF(0, 0) - assert item2.pos() == QtCore.QPointF(0, 0) + command = commands.ArrangeItems( + view.scene, + [item1, item2, item3], + [QtCore.QPointF(1, 2), + QtCore.QPointF(203, 204), + QtCore.QPointF(307, 308)]) + + command.redo() + assert item1.pos() == QtCore.QPointF(101, 2) + assert item2.pos() == QtCore.QPointF(283, 204) + assert item3.pos() == QtCore.QPointF(302, 303) + command.undo() + assert item1.pos() == QtCore.QPointF(0, 0) + assert item2.pos() == QtCore.QPointF(0, 0) + assert item3.pos() == QtCore.QPointF(0, 0) + + +def test_crop_item(item): + item.crop = QtCore.QRectF(0, 0, 100, 80) + command = commands.CropItem(item, QtCore.QRectF(10, 20, 30, 40)) + command.redo() + assert item.crop == QtCore.QRectF(10, 20, 30, 40) + assert item.pos() == QtCore.QPointF(0, 0) + command.undo() + assert item.crop == QtCore.QRectF(0, 0, 100, 80) + assert item.pos() == QtCore.QPointF(0, 0) diff --git a/tests/test_scene.py b/tests/test_scene.py index 656011d..4265957 100644 --- a/tests/test_scene.py +++ b/tests/test_scene.py @@ -1,5 +1,5 @@ import math -from unittest.mock import patch, MagicMock, PropertyMock +from unittest.mock import patch, MagicMock from pytest import approx @@ -17,6 +17,17 @@ def test_add_remove_item(view, item): assert view.scene.items() == [] +def test_cancel_crop_mode_when_crop(view, item): + view.scene.crop_item = item + item.exit_crop_mode = MagicMock() + view.scene.cancel_crop_mode() + item.exit_crop_mode.assert_called_once_with(confirm=False) + + +def test_cancel_crop_mode_when_no_crop(view, item): + view.scene.cancel_crop_mode() + + def test_copy_selection_to_internal_clipboard(view): item1 = BeePixmapItem(QtGui.QImage()) view.scene.addItem(item1) @@ -60,11 +71,13 @@ def test_raise_to_top(view): item3 = BeePixmapItem(QtGui.QImage()) view.scene.addItem(item3) item3.setZValue(0.07) + view.scene.cancel_crop_mode = MagicMock() view.scene.raise_to_top() assert item1.zValue() == 0.11 + view.scene.Z_STEP assert item2.zValue() == 0.07 + view.scene.Z_STEP assert item3.zValue() == 0.07 + view.scene.cancel_crop_mode.assert_called_once_with() def test_lower_to_bottom(view): @@ -79,11 +92,13 @@ def test_lower_to_bottom(view): item3 = BeePixmapItem(QtGui.QImage()) view.scene.addItem(item3) item3.setZValue(-0.07) + view.scene.cancel_crop_mode = MagicMock() view.scene.lower_to_bottom() assert item1.zValue() == -0.11 - view.scene.Z_STEP assert item2.zValue() == -0.07 - view.scene.Z_STEP assert item3.zValue() == -0.07 + view.scene.cancel_crop_mode.assert_called_once_with() def test_normalize_height(view): @@ -94,17 +109,19 @@ def test_normalize_height(view): view.scene.addItem(item2) item2.setSelected(True) item2.setScale(3) + view.scene.cancel_crop_mode = MagicMock() - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): + with patch.object(item1, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + with patch.object(item2, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): view.scene.normalize_height() assert item1.scale() == 2 assert item1.pos() == QtCore.QPointF(-50, -40) assert item2.scale() == 2 assert item2.pos() == QtCore.QPointF(50, 40) + view.scene.cancel_crop_mode.assert_called_once_with() def test_normalize_height_with_rotation(view): @@ -115,19 +132,23 @@ def test_normalize_height_with_rotation(view): view.scene.addItem(item2) item2.setSelected(True) item2.setRotation(90) + view.scene.cancel_crop_mode = MagicMock() - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=200): + with patch.object(item1, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 200)): + with patch.object(item2, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 200)): view.scene.normalize_height() assert item1.scale() == 0.75 assert item2.scale() == 1.5 + view.scene.cancel_crop_mode.assert_called_once_with() def test_normalize_height_when_no_items(view): + view.scene.cancel_crop_mode = MagicMock() view.scene.normalize_height() + view.scene.cancel_crop_mode.assert_called_once_with() def test_normalize_width(view): @@ -138,17 +159,19 @@ def test_normalize_width(view): view.scene.addItem(item2) item2.setSelected(True) item2.setScale(3) + view.scene.cancel_crop_mode = MagicMock() - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=80): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=100): + with patch.object(item1, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 80, 100)): + with patch.object(item2, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 80, 100)): view.scene.normalize_width() assert item1.scale() == 2 assert item1.pos() == QtCore.QPointF(-40, -50) assert item2.scale() == 2 assert item2.pos() == QtCore.QPointF(40, 50) + view.scene.cancel_crop_mode.assert_called_once_with() def test_normalize_width_with_rotation(view): @@ -159,19 +182,23 @@ def test_normalize_width_with_rotation(view): view.scene.addItem(item2) item2.setSelected(True) item2.setRotation(90) + view.scene.cancel_crop_mode = MagicMock() - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=200): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=100): + with patch.object(item1, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 200, 100)): + with patch.object(item2, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 200, 100)): view.scene.normalize_height() assert item1.scale() == 1.5 assert item2.scale() == 0.75 + view.scene.cancel_crop_mode.assert_called_once_with() def test_normalize_width_when_no_items(view): + view.scene.cancel_crop_mode = MagicMock() view.scene.normalize_width() + view.scene.cancel_crop_mode.assert_called_once_with() def test_normalize_size(view): @@ -182,15 +209,17 @@ def test_normalize_size(view): view.scene.addItem(item2) item2.setSelected(True) item2.setScale(2) + view.scene.cancel_crop_mode = MagicMock() - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=100): + with patch.object(item1, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 100)): + with patch.object(item2, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 100)): view.scene.normalize_size() assert item1.scale() == approx(math.sqrt(2.5)) assert item2.scale() == approx(math.sqrt(2.5)) + view.scene.cancel_crop_mode.assert_called_once_with() def test_normalize_size_with_rotation(view): @@ -201,19 +230,23 @@ def test_normalize_size_with_rotation(view): view.scene.addItem(item2) item2.setSelected(True) item2.setRotation(90) + view.scene.cancel_crop_mode = MagicMock() - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=200): + with patch.object(item1, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 200)): + with patch.object(item2, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 200)): view.scene.normalize_size() assert item1.scale() == 1 assert item2.scale() == 1 + view.scene.cancel_crop_mode.assert_called_once_with() def test_normalize_size_when_no_items(view): + view.scene.cancel_crop_mode = MagicMock() view.scene.normalize_size() + view.scene.cancel_crop_mode.assert_called_once_with() def test_arrange_horizontal(view): @@ -225,15 +258,17 @@ def test_arrange_horizontal(view): view.scene.addItem(item2) item2.setSelected(True) item2.setPos(-10, 40) + view.scene.cancel_crop_mode = MagicMock() - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): + with patch.object(item1, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + with patch.object(item2, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): view.scene.arrange() assert item2.pos() == QtCore.QPointF(-50, -30) assert item1.pos() == QtCore.QPointF(50, -30) + view.scene.cancel_crop_mode.assert_called_once_with() def test_arrange_vertical(view): @@ -245,15 +280,17 @@ def test_arrange_vertical(view): view.scene.addItem(item2) item2.setSelected(True) item2.setPos(-10, 40) + view.scene.cancel_crop_mode = MagicMock() - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): + with patch.object(item1, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + with patch.object(item2, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): view.scene.arrange(vertical=True) assert item1.pos() == QtCore.QPointF(0, -70) assert item2.pos() == QtCore.QPointF(0, 10) + view.scene.cancel_crop_mode = MagicMock() def test_arrange_when_rotated(view): @@ -267,19 +304,23 @@ def test_arrange_when_rotated(view): item2.setSelected(True) item2.setPos(-10, 40) item2.setRotation(90) + view.scene.cancel_crop_mode = MagicMock() - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): + with patch.object(item1, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): + with patch.object(item2, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 80)): view.scene.arrange() assert item2.pos() == QtCore.QPointF(-40, -30) assert item1.pos() == QtCore.QPointF(40, -30) + view.scene.cancel_crop_mode.assert_called_once_with() def test_arrange_when_no_items(view): + view.scene.cancel_crop_mode = MagicMock() view.scene.arrange() + view.scene.cancel_crop_mode.assert_called_once_with() def test_arrange_optimal(view): @@ -287,17 +328,16 @@ def test_arrange_optimal(view): item = BeePixmapItem(QtGui.QImage()) view.scene.addItem(item) item.setSelected(True) - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): - view.scene.arrange_optimal() + item.crop = QtCore.QRectF(0, 0, 100, 80) + view.scene.cancel_crop_mode = MagicMock() + view.scene.arrange_optimal() expected_positions = {(-50, -40), (50, -40), (-50, 40), (50, 40)} actual_positions = { (i.pos().x(), i.pos().y()) for i in view.scene.selectedItems(user_only=True)} assert expected_positions == actual_positions + view.scene.cancel_crop_mode.assert_called_once_with() def test_arrange_optimal_when_rotated(view): @@ -306,27 +346,30 @@ def test_arrange_optimal_when_rotated(view): view.scene.addItem(item) item.setRotation(90) item.setSelected(True) - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): - view.scene.arrange_optimal() + item.crop = QtCore.QRectF(0, 0, 100, 80) + + view.scene.cancel_crop_mode = MagicMock() + view.scene.arrange_optimal() expected_positions = {(-40, -50), (40, -50), (-40, 50), (40, 50)} actual_positions = { (i.pos().x(), i.pos().y()) for i in view.scene.selectedItems(user_only=True)} assert expected_positions == actual_positions + view.scene.cancel_crop_mode.assert_called_once_with() def test_arrange_optimal_when_no_items(view): + view.scene.cancel_crop_mode = MagicMock() view.scene.arrange_optimal() + view.scene.cancel_crop_mode.assert_called_once_with() def test_flip_items(view, item): view.scene.addItem(item) item.setSelected(True) view.scene.undo_stack = MagicMock(push=MagicMock()) + view.scene.cancel_crop_mode = MagicMock() with patch('beeref.scene.BeeGraphicsScene.itemsBoundingRect', return_value=QtCore.QRectF(10, 20, 100, 60)): view.scene.flip_items(vertical=True) @@ -336,32 +379,86 @@ def test_flip_items(view, item): assert cmd.items == [item] assert cmd.anchor == QtCore.QPointF(60, 50) assert cmd.vertical is True + view.scene.cancel_crop_mode.assert_called_once_with() -def test_set_selection_all_items_when_true(view): +def test_crop_items(view, item): + view.scene.addItem(item) + item.setSelected(True) + item.enter_crop_mode = MagicMock() + + view.scene.crop_items() + item.enter_crop_mode.assert_called_once_with() + + +def test_crop_items_when_in_crop_mode(view, item): + view.scene.addItem(item) + item.setSelected(True) + item.enter_crop_mode = MagicMock() + view.scene.crop_item = item + + view.scene.crop_items() + item.enter_crop_mode.assert_not_called() + + +def test_crop_item_multi_select(view, item): + view.scene.addItem(item) + item.setSelected(True) + item.enter_crop_mode = MagicMock() + item2 = BeePixmapItem(QtGui.QImage()) + view.scene.addItem(item2) + item2.setSelected(True) + + view.scene.crop_items() + item.enter_crop_mode.assert_not_called() + + +def test_crop_item_no_selection(view, item): + view.scene.addItem(item) + item.setSelected(False) + item.enter_crop_mode = MagicMock() + + view.scene.crop_items() + item.enter_crop_mode.assert_not_called() + + +def test_crop_item_when_not_croppable(view): + item = BeeTextItem('foo') + item.setSelected(True) + item.enter_crop_mode = MagicMock() + + view.scene.crop_items() + item.enter_crop_mode.assert_not_called() + + +def test_set_selected_all_items_when_true(view): item1 = BeePixmapItem(QtGui.QImage()) view.scene.addItem(item1) item1.setSelected(True) item2 = BeePixmapItem(QtGui.QImage()) view.scene.addItem(item2) item2.setSelected(True) + view.scene.cancel_crop_mode = MagicMock() view.scene.set_selected_all_items(True) assert item1.isSelected() is True assert item2.isSelected() is True + view.scene.cancel_crop_mode.assert_called_once_with() -def test_set_selection_all_items_when_false(view): +def test_set_selected_all_items_when_false(view): item1 = BeePixmapItem(QtGui.QImage()) view.scene.addItem(item1) item1.setSelected(True) item2 = BeePixmapItem(QtGui.QImage()) view.scene.addItem(item2) item2.setSelected(True) + view.scene.cancel_crop_mode = MagicMock() view.scene.set_selected_all_items(False) assert item1.isSelected() is False assert item2.isSelected() is False + view.scene.cancel_crop_mode.assert_called_once_with() def test_has_selection_when_no_selection(view, item): @@ -423,6 +520,34 @@ def test_has_multi_selection_when_multi_selection(view): assert view.scene.has_multi_selection() is True +def test_has_croppable_selection(view, item): + view.scene.addItem(item) + item.setSelected(True) + assert view.scene.has_croppable_selection() is True + + +def test_has_croppable_selection_when_item_not_croppable(view): + item = BeeTextItem('foo') + view.scene.addItem(item) + item.setSelected(True) + assert view.scene.has_croppable_selection() is False + + +def test_has_croppable_selection_when_no_selection(view, item): + view.scene.addItem(item) + item.setSelected(False) + assert view.scene.has_croppable_selection() is False + + +def test_has_croppable_selection_when_multi_selection(view, item): + view.scene.addItem(item) + item.setSelected(True) + item2 = BeePixmapItem(QtGui.QImage()) + view.scene.addItem(item2) + item2.setSelected(True) + assert view.scene.has_croppable_selection() is False + + @patch('PyQt6.QtWidgets.QGraphicsScene.mousePressEvent') def test_mouse_press_event_when_right_click(mouse_mock, view): event = MagicMock( @@ -461,7 +586,6 @@ def test_mouse_press_event_when_left_click_over_item_in_edit_mode( view.scene.mousePressEvent(event) event.accept.assert_not_called() mouse_mock.assert_called_once_with(event) - assert view.scene.edit_item == item item.exit_edit_mode.assert_not_called() assert view.scene.move_active is False assert view.scene.rubberband_active is False @@ -481,7 +605,6 @@ def test_mouse_press_event_when_left_click_over_diff_item_in_edit_mode( view.scene.mousePressEvent(event) event.accept.assert_not_called() mouse_mock.assert_called_once_with(event) - assert view.scene.edit_item is None txtitem.exit_edit_mode.assert_called_once_with() assert view.scene.move_active is True assert view.scene.rubberband_active is False @@ -501,12 +624,66 @@ def test_mouse_press_event_when_left_click_over_no_item_in_edit_mode( view.scene.mousePressEvent(event) event.accept.assert_not_called() mouse_mock.assert_called_once_with(event) - assert view.scene.edit_item is None item.exit_edit_mode.assert_called_once_with() assert view.scene.move_active is False assert view.scene.rubberband_active is True +@patch('PyQt6.QtWidgets.QGraphicsScene.mousePressEvent') +def test_mouse_press_event_when_left_click_over_item_in_crop_mode( + mouse_mock, view, item): + view.scene.addItem(item) + view.scene.cancel_crop_mode = MagicMock() + view.scene.crop_item = item + view.scene.itemAt = MagicMock(return_value=item) + event = MagicMock( + button=MagicMock(return_value=Qt.MouseButton.LeftButton), + ) + view.scene.mousePressEvent(event) + event.accept.assert_not_called() + mouse_mock.assert_called_once_with(event) + view.scene.cancel_crop_mode.assert_not_called() + assert view.scene.move_active is False + assert view.scene.rubberband_active is False + + +@patch('PyQt6.QtWidgets.QGraphicsScene.mousePressEvent') +def test_mouse_press_event_when_left_click_over_diff_item_in_crop_mode( + mouse_mock, view, item): + view.scene.addItem(item) + view.scene.cancel_crop_mode = MagicMock() + view.scene.crop_item = item + other_item = BeePixmapItem(QtGui.QImage()) + view.scene.itemAt = MagicMock(return_value=other_item) + event = MagicMock( + button=MagicMock(return_value=Qt.MouseButton.LeftButton), + ) + view.scene.mousePressEvent(event) + event.accept.assert_not_called() + mouse_mock.assert_called_once_with(event) + view.scene.cancel_crop_mode.assert_called_once_with() + assert view.scene.move_active is True + assert view.scene.rubberband_active is False + + +@patch('PyQt6.QtWidgets.QGraphicsScene.mousePressEvent') +def test_mouse_press_event_when_left_click_over_no_item_in_crop_mode( + mouse_mock, view, item): + view.scene.addItem(item) + view.scene.cancel_crop_mode = MagicMock() + view.scene.crop_item = item + view.scene.itemAt = MagicMock(return_value=None) + event = MagicMock( + button=MagicMock(return_value=Qt.MouseButton.LeftButton), + ) + view.scene.mousePressEvent(event) + event.accept.assert_not_called() + mouse_mock.assert_called_once_with(event) + view.scene.cancel_crop_mode.assert_called_once_with() + assert view.scene.move_active is False + assert view.scene.rubberband_active is True + + @patch('PyQt6.QtWidgets.QGraphicsScene.mousePressEvent') def test_mouse_press_event_when_left_click_not_over_item( mouse_mock, view, item): @@ -549,11 +726,9 @@ def test_mouse_doubleclick_event_when_over_item(mouse_mock, view, item): view.scene.itemAt = MagicMock(return_value=item) view.fit_rect = MagicMock() - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=100): - view.scene.mouseDoubleClickEvent(event) + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 100)): + view.scene.mouseDoubleClickEvent(event) assert view.scene.move_active is False view.fit_rect.assert_called_once_with( @@ -575,15 +750,12 @@ def test_mouse_doubleclick_event_when_over_editable_item( view.scene.itemAt = MagicMock(return_value=item) view.fit_rect = MagicMock() - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=100): - view.scene.mouseDoubleClickEvent(event) + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 100)): + view.scene.mouseDoubleClickEvent(event) assert view.scene.move_active is False item.enter_edit_mode.assert_called_once_with() - view.scene.edit_item == item double_mock.assert_not_called() press_mock.assert_called_once_with(event) @@ -599,11 +771,9 @@ def test_mouse_doubleclick_event_when_item_not_selected( view.scene.itemAt = MagicMock(return_value=item) view.fit_rect = MagicMock() - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=100): - view.scene.mouseDoubleClickEvent(event) + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 100)): + view.scene.mouseDoubleClickEvent(event) assert view.scene.move_active is False view.fit_rect.assert_called_once_with( @@ -863,10 +1033,10 @@ def test_items_bounding_rect_given_items(view): item3.setSelected(True) item3.setPos(1000, 1000) - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=100): + with patch.object(item1, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 100)): + with patch.object(item2, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 100)): rect = view.scene.itemsBoundingRect(items=[item1, item2]) assert rect.topLeft().x() == -33 @@ -889,10 +1059,10 @@ def test_items_bounding_rect_two_items_selection_only(view): item3.setSelected(False) item3.setPos(1000, 1000) - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=100): + with patch.object(item1, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 100)): + with patch.object(item2, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 100)): rect = view.scene.itemsBoundingRect(selection_only=True) assert rect.topLeft().x() == -33 @@ -905,11 +1075,9 @@ def test_items_bounding_rect_rotated_item(view, item): view.scene.addItem(item) item.setRotation(-45) - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=100): - rect = view.scene.itemsBoundingRect() + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 100, 100)): + rect = view.scene.itemsBoundingRect() assert rect.topLeft().x() == 0 assert rect.topLeft().y() == approx(-math.sqrt(2) * 50) @@ -921,11 +1089,9 @@ def test_items_bounding_rect_flipped_item(view): item = BeePixmapItem(QtGui.QImage()) view.scene.addItem(item) item.do_flip() - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=50): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=100): - rect = view.scene.itemsBoundingRect() + with patch.object(item, 'bounding_rect_unselected', + return_value=QtCore.QRectF(0, 0, 50, 100)): + rect = view.scene.itemsBoundingRect() assert rect.topLeft().x() == -50 assert rect.topLeft().y() == 0 diff --git a/tests/test_view.py b/tests/test_view.py index abe1fba..dbd1ab4 100644 --- a/tests/test_view.py +++ b/tests/test_view.py @@ -182,6 +182,8 @@ def test_on_action_open(dialog_mock, view, qtbot): filename = os.path.join(root, 'assets', 'test1item.bee') dialog_mock.return_value = (filename, None) view.on_loading_finished = MagicMock() + view.scene.cancel_crop_mode = MagicMock() + view.on_action_open() qtbot.waitUntil(lambda: view.on_loading_finished.called is True) assert len(view.scene.items()) == 1 @@ -189,26 +191,31 @@ def test_on_action_open(dialog_mock, view, qtbot): assert item.isSelected() is False assert item.pixmap() view.on_loading_finished.assert_called_once_with(filename, []) + view.scene.cancel_crop_mode.assert_called_once_with() @patch('PyQt6.QtWidgets.QFileDialog.getOpenFileName') -@patch('beeref.view.BeeGraphicsView.on_action_open') -def test_on_action_open_when_no_filename(dialog_mock, open_mock, view): +@patch('beeref.view.BeeGraphicsView.open_from_file') +def test_on_action_open_when_no_filename(open_mock, dialog_mock, view): dialog_mock.return_value = (None, None) + view.scene.cancel_crop_mode = MagicMock() view.on_action_open() open_mock.assert_not_called() + view.scene.cancel_crop_mode.assert_called_once_with() @patch('PyQt6.QtWidgets.QFileDialog.getSaveFileName') def test_on_action_save_as(dialog_mock, view, imgfilename3x3, tmpdir): item = BeePixmapItem(QtGui.QImage(imgfilename3x3)) view.scene.addItem(item) + view.scene.cancel_crop_mode = MagicMock() filename = os.path.join(tmpdir, 'test.bee') assert os.path.exists(filename) is False dialog_mock.return_value = (filename, None) view.on_action_save_as() view.worker.wait() assert os.path.exists(filename) is True + view.scene.cancel_crop_mode.assert_called_once_with() @patch('PyQt6.QtWidgets.QFileDialog.getSaveFileName') @@ -217,9 +224,11 @@ def test_on_action_save_as_when_no_filename( save_mock, dialog_mock, view, imgfilename3x3): item = BeePixmapItem(QtGui.QImage(imgfilename3x3)) view.scene.addItem(item) + view.scene.cancel_crop_mode = MagicMock() dialog_mock.return_value = (None, None) view.on_action_save_as() save_mock.assert_not_called() + view.scene.cancel_crop_mode.assert_called_once_with() @patch('PyQt6.QtWidgets.QFileDialog.getSaveFileName') @@ -227,6 +236,7 @@ def test_on_action_save_as_filename_doesnt_end_with_bee( dialog_mock, view, qtbot, imgfilename3x3, tmpdir): item = BeePixmapItem(QtGui.QImage(imgfilename3x3)) view.scene.addItem(item) + view.scene.cancel_crop_mode = MagicMock() view.on_saving_finished = MagicMock() filename = os.path.join(tmpdir, 'test') assert os.path.exists(filename) is False @@ -235,6 +245,7 @@ def test_on_action_save_as_filename_doesnt_end_with_bee( qtbot.waitUntil(lambda: view.on_saving_finished.called is True) assert os.path.exists(f'{filename}.bee') is True view.on_saving_finished.assert_called_once_with(f'{filename}.bee', []) + view.scene.cancel_crop_mode.assert_called_once_with() @patch('PyQt6.QtWidgets.QFileDialog.getSaveFileName') @@ -244,17 +255,20 @@ def test_on_action_save_as_when_error( item = BeePixmapItem(QtGui.QImage(imgfilename3x3)) view.scene.addItem(item) view.on_saving_finished = MagicMock() + view.scene.cancel_crop_mode = MagicMock() filename = os.path.join(tmpdir, 'test.bee') dialog_mock.return_value = (filename, None) save_mock.side_effect = sqlite3.Error('foo') view.on_action_save_as() qtbot.waitUntil(lambda: view.on_saving_finished.called is True) view.on_saving_finished.assert_called_once_with(filename, ['foo']) + view.scene.cancel_crop_mode.assert_called_once_with() def test_on_action_save(view, qtbot, imgfilename3x3, tmpdir): item = BeePixmapItem(QtGui.QImage(imgfilename3x3)) view.scene.addItem(item) + view.scene.cancel_crop_mode = MagicMock() view.filename = os.path.join(tmpdir, 'test.bee') root = os.path.dirname(__file__) shutil.copyfile(os.path.join(root, 'assets', 'test1item.bee'), @@ -264,15 +278,18 @@ def test_on_action_save(view, qtbot, imgfilename3x3, tmpdir): qtbot.waitUntil(lambda: view.on_saving_finished.called is True) assert os.path.exists(view.filename) is True view.on_saving_finished.assert_called_once_with(view.filename, []) + view.scene.cancel_crop_mode.assert_called_once_with() @patch('beeref.view.BeeGraphicsView.on_action_save_as') def test_on_action_save_when_no_filename(save_as_mock, view, imgfilename3x3): item = BeePixmapItem(QtGui.QImage(imgfilename3x3)) view.scene.addItem(item) + view.scene.cancel_crop_mode = MagicMock() view.filename = None view.on_action_save() save_as_mock.assert_called_once_with() + view.scene.cancel_crop_mode.assert_called_once_with() @patch('beeref.widgets.HelpDialog.show') @@ -295,6 +312,7 @@ def test_on_action_insert_images_new_scene( dialog_mock, clear_mock, view, imgfilename3x3, qtbot): dialog_mock.return_value = ([imgfilename3x3], None) view.on_insert_images_finished = MagicMock() + view.scene.cancel_crop_mode = MagicMock() view.on_action_insert_images() qtbot.waitUntil(lambda: view.on_insert_images_finished.called is True) assert len(view.scene.items()) == 1 @@ -303,6 +321,7 @@ def test_on_action_insert_images_new_scene( assert item.pixmap() clear_mock.assert_called_once_with() view.on_insert_images_finished.assert_called_once_with(True, '', []) + view.scene.cancel_crop_mode.assert_called_once_with() @patch('beeref.scene.BeeGraphicsScene.clearSelection') @@ -312,6 +331,7 @@ def test_on_action_insert_images_existing_scene( view.scene.addItem(item) dialog_mock.return_value = ([imgfilename3x3], None) view.on_insert_images_finished = MagicMock() + view.scene.cancel_crop_mode = MagicMock() view.on_action_insert_images() qtbot.waitUntil(lambda: view.on_insert_images_finished.called is True) assert len(view.scene.items()) == 2 @@ -320,6 +340,7 @@ def test_on_action_insert_images_existing_scene( assert item.pixmap() clear_mock.assert_called_once_with() view.on_insert_images_finished.assert_called_once_with(False, '', []) + view.scene.cancel_crop_mode.assert_called_once_with() @patch('beeref.scene.BeeGraphicsScene.clearSelection') @@ -328,6 +349,7 @@ def test_on_action_insert_images_when_error( dialog_mock, clear_mock, view, imgfilename3x3, qtbot): dialog_mock.return_value = ([imgfilename3x3, 'iaeiae', 'trntrn'], None) view.on_insert_images_finished = MagicMock() + view.scene.cancel_crop_mode = MagicMock() view.on_action_insert_images() qtbot.waitUntil(lambda: view.on_insert_images_finished.called is True) assert len(view.scene.items()) == 1 @@ -337,22 +359,26 @@ def test_on_action_insert_images_when_error( clear_mock.assert_called_once_with() view.on_insert_images_finished.assert_called_once_with( True, '', ['iaeiae', 'trntrn']) + view.scene.cancel_crop_mode.assert_called_once_with() @patch('beeref.scene.BeeGraphicsScene.clearSelection') def test_on_action_insert_text(clear_mock, view): + view.scene.cancel_crop_mode = MagicMock() view.on_action_insert_text() clear_mock.assert_called_once_with() assert len(view.scene.items()) == 1 item = view.scene.items()[0] assert item.toPlainText() == 'Text' assert item.isSelected() is True + view.scene.cancel_crop_mode.assert_called_once_with() @patch('PyQt6.QtWidgets.QApplication.clipboard') def test_on_action_copy_image(clipboard_mock, view, imgfilename3x3): item = BeePixmapItem(QtGui.QImage(imgfilename3x3)) view.scene.addItem(item) + view.scene.cancel_crop_mode = MagicMock() item.setSelected(True) mimedata = QtCore.QMimeData() clipboard_mock.return_value.mimeData.return_value = mimedata @@ -361,12 +387,14 @@ def test_on_action_copy_image(clipboard_mock, view, imgfilename3x3): clipboard_mock.return_value.setPixmap.assert_called_once() view.scene.internal_clipboard == [item] assert mimedata.data('beeref/items') == b'1' + view.scene.cancel_crop_mode.assert_called_once_with() @patch('PyQt6.QtWidgets.QApplication.clipboard') def test_on_action_copy_text(clipboard_mock, view, imgfilename3x3): item = BeeTextItem('foo bar') view.scene.addItem(item) + view.scene.cancel_crop_mode = MagicMock() item.setSelected(True) mimedata = QtCore.QMimeData() clipboard_mock.return_value.mimeData.return_value = mimedata @@ -375,6 +403,7 @@ def test_on_action_copy_text(clipboard_mock, view, imgfilename3x3): clipboard_mock.return_value.setText.assert_called_once_with('foo bar') view.scene.internal_clipboard == [item] assert mimedata.data('beeref/items') == b'1' + view.scene.cancel_crop_mode.assert_called_once_with() @patch('beeref.view.BeeGraphicsView.on_action_fit_scene') @@ -383,10 +412,12 @@ def test_on_action_copy_text(clipboard_mock, view, imgfilename3x3): def test_on_action_paste_external_new_scene( clipboard_mock, clear_mock, fit_mock, view, imgfilename3x3): clipboard_mock.return_value = QtGui.QImage(imgfilename3x3) + view.scene.cancel_crop_mode = MagicMock() view.on_action_paste() assert len(view.scene.items()) == 1 assert view.scene.items()[0].isSelected() is True fit_mock.assert_called_once_with() + view.scene.cancel_crop_mode.assert_called_once_with() @patch('beeref.view.BeeGraphicsView.on_action_fit_scene') @@ -395,12 +426,14 @@ def test_on_action_paste_external_new_scene( def test_on_action_paste_external_existing_scene( clipboard_mock, clear_mock, fit_mock, view, item, imgfilename3x3): view.scene.addItem(item) + view.scene.cancel_crop_mode = MagicMock() clipboard_mock.return_value = QtGui.QImage(imgfilename3x3) view.on_action_paste() assert len(view.scene.items()) == 2 assert view.scene.items()[0].isSelected() is True assert view.scene.items()[1].isSelected() is False fit_mock.assert_not_called() + view.scene.cancel_crop_mode.assert_called_once_with() @patch('beeref.scene.BeeGraphicsScene.clearSelection') @@ -411,10 +444,12 @@ def test_on_action_paste_internal(mimedata_mock, clear_mock, view): mimedata_mock.return_value = mimedata item = BeePixmapItem(QtGui.QImage()) view.scene.internal_clipboard = [item] + view.scene.cancel_crop_mode = MagicMock() view.on_action_paste() assert len(view.scene.items()) == 1 assert view.scene.items()[0].isSelected() is True clear_mock.assert_called_once_with() + view.scene.cancel_crop_mode.assert_called() @patch('beeref.scene.BeeGraphicsScene.clearSelection') @@ -423,22 +458,26 @@ def test_on_action_paste_internal(mimedata_mock, clear_mock, view): def test_on_action_paste_when_text(img_mock, text_mock, clear_mock, view): img_mock.return_value = QtGui.QImage() text_mock.return_value = 'foo bar' + view.scene.cancel_crop_mode = MagicMock() view.on_action_paste() assert len(view.scene.items()) == 1 assert view.scene.items()[0].isSelected() is True assert view.scene.items()[0].toPlainText() == 'foo bar' clear_mock.assert_called_once_with() + view.scene.cancel_crop_mode.assert_called_once_with() @patch('beeref.scene.BeeGraphicsScene.clearSelection') @patch('PyQt6.QtGui.QClipboard.text') @patch('PyQt6.QtGui.QClipboard.image') def test_on_action_paste_when_empty(img_mock, text_mock, clear_mock, view): + view.scene.cancel_crop_mode = MagicMock() img_mock.return_value = QtGui.QImage() text_mock.return_value = '' view.on_action_paste() assert len(view.scene.items()) == 0 clear_mock.assert_not_called() + view.scene.cancel_crop_mode.assert_called_once_with() @patch('beeref.view.BeeGraphicsView.on_action_copy') @@ -508,11 +547,13 @@ def test_on_action_show_titlebar_unchecked( def test_on_action_delete_items(view, item): + view.scene.cancel_crop_mode = MagicMock() view.scene.addItem(item) item.setSelected(True) view.on_action_delete_items() assert view.scene.items() == [] assert view.undo_stack.isClean() is False + view.scene.cancel_crop_mode.assert_called_once() @patch('PyQt6.QtGui.QUndoStack.isClean', return_value=True)