Unittests for multi selections

This commit is contained in:
Rebecca Breu 2021-04-03 16:53:19 +02:00
parent 511848146e
commit 165283d56c
6 changed files with 1192 additions and 749 deletions

View file

@ -20,247 +20,13 @@ text).
import logging
from PyQt6 import QtCore, QtGui, QtWidgets
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QGraphicsItem
from beeref import commands
from beeref.config import CommandlineArgs
from beeref.selection import SelectableMixin
commandline_args = CommandlineArgs()
logger = logging.getLogger('BeeRef')
class SelectableMixin:
"""Common code for selectable items: Selection outline, handles etc."""
select_color = QtGui.QColor(116, 234, 231, 255)
SELECT_LINE_WIDTH = 4 # line width for the selection box
SELECT_HANDLE_SIZE = 15 # size of selection handles for scaling
SELECT_RESIZE_SIZE = 20 # size of hover area for scaling
SELECT_ROTATE_SIZE = 20 # size of hover area for rotating
def init_selectable(self):
self.setAcceptHoverEvents(True)
self.setFlags(
QtWidgets.QGraphicsItem.GraphicsItemFlags.ItemIsMovable
| QtWidgets.QGraphicsItem.GraphicsItemFlags.ItemIsSelectable)
self.scale_active = False
self.viewport_scale = 1
self.conf_debug_shapes = commandline_args.draw_debug_shapes
def bring_to_front(self):
self.setZValue(self.scene().max_z + 0.001)
def fixed_length_for_viewport(self, value):
"""The interactable areas need to stay the same size on the
screen so we need to adjust the values according to the scale
factor sof the view and the item."""
if self.scene():
scale = self.scene().views()[0].get_scale()
self._view_scale = scale
# It can happen that the item is already removed from
# the scene but its boundingRect is still needed. Keep the
# last known scaling factor for that case
return value / getattr(self, '_view_scale', 1) / self.scale()
@property
def select_resize_size(self):
return self.fixed_length_for_viewport(self.SELECT_RESIZE_SIZE)
@property
def select_rotate_size(self):
return self.fixed_length_for_viewport(self.SELECT_ROTATE_SIZE)
def draw_debug_shape(self, painter, shape, r, g, b):
color = QtGui.QColor(r, g, b, 50)
if isinstance(shape, QtCore.QRectF):
painter.fillRect(shape, color)
else:
painter.fillPath(shape, color)
def paint_selectable(self, painter, option, widget):
if self.conf_debug_shapes:
self.draw_debug_shape(painter, self.boundingRect(), 0, 255, 0)
self.draw_debug_shape(painter, self.shape(), 255, 0, 0)
if not self.has_selection_outline():
return
pen = QtGui.QPen(self.select_color)
pen.setWidth(self.SELECT_LINE_WIDTH)
pen.setCosmetic(True)
painter.setPen(pen)
# Draw the main selection rectangle
painter.drawRect(0, 0, self.width, self.height)
# If it's a single selection, draw the handles:
if self.has_selection_handles():
pen.setWidth(self.SELECT_HANDLE_SIZE)
painter.setPen(pen)
for corner in self.corners:
painter.drawPoint(*corner)
@property
def corners(self):
"""The corners of the items. Used for scale and rotate handles."""
return ((0, 0),
(0, self.height),
(self.width, 0),
(self.width, self.height))
def get_scale_bounds(self, center):
"""The interactable shape of the scale handles."""
return QtCore.QRectF(
center[0] - self.select_resize_size/2,
center[1] - self.select_resize_size/2,
self.select_resize_size,
self.select_resize_size)
@property
def bottom_right_rotate_bounds(self):
"""The interactable shape of the bottom right rotate handle"""
return QtCore.QRectF(
self.width + self.select_resize_size / 2,
self.height + self.select_resize_size / 2,
self.select_rotate_size, self.select_rotate_size)
def boundingRect(self):
bounds = super().boundingRect()
if not self.has_selection_outline():
return bounds
# Add extra space for scale and rotate interactive areas
margin = self.select_resize_size / 2 + self.select_rotate_size
return QtCore.QRectF(
bounds.topLeft().x() - margin,
bounds.topLeft().y() - margin,
bounds.bottomRight().x() + 2 * margin,
bounds.bottomRight().y() + 2 * margin)
def shape(self):
shape_ = super().shape()
if self.has_selection_outline():
# Add extra space for scale and rotate interactive areas
path = QtGui.QPainterPath()
for corner in self.corners:
path.addRect(self.get_scale_bounds(corner))
path.addRect(self.bottom_right_rotate_bounds)
shape_ = shape_ + path
return shape_
def hoverMoveEvent(self, event):
if not self.has_selection_handles():
return
for corner in self.corners:
# See if we need to change the cursor for scale areas
if self.get_scale_bounds(corner).contains(event.pos()):
direction = self.get_scale_direction(corner)
if direction[0] == direction[1]:
self.setCursor(Qt.CursorShape.SizeFDiagCursor)
else:
self.setCursor(Qt.CursorShape.SizeBDiagCursor)
return
if self.bottom_right_rotate_bounds.contains(event.pos()):
self.setCursor(Qt.CursorShape.ForbiddenCursor)
return
self.setCursor(Qt.CursorShape.ArrowCursor)
def hoverEnterEvent(self, event):
# Always return regular cursor when there aren't any selection handles
if not self.has_selection_handles():
self.setCursor(Qt.CursorShape.ArrowCursor)
def mousePressEvent(self, event):
if (event.button() == Qt.MouseButtons.LeftButton
and self.has_selection_handles()):
for corner in self.corners:
# Check if we are in one of the corner's scale areas
if self.get_scale_bounds(corner).contains(event.pos()):
# Start scale action for this corner
self.scale_active = True
self.scale_start = event.scenePos()
self.scale_direction = self.get_scale_direction(corner)
for item in self.selection_action_items():
item.scale_anchor = self.get_scale_anchor(item, corner)
item.scale_orig_factor = item.scale()
item.scale_orig_pos = item.pos()
event.accept()
return
super().mousePressEvent(event)
def get_scale_factor(self, event):
imgsize = self.width + self.height
p = event.scenePos() - self.scale_start
direction = self.scale_direction
delta = (direction[0] * p.x() + direction[1] * p.y()) / imgsize
return (self.scale_orig_factor + delta) / self.scale_orig_factor
def get_scale_anchor(self, item, corner):
"""Get the anchor around which the scale for this corner operates."""
return item.mapFromScene(
self.mapToScene(self.width - corner[0], self.height - corner[1]))
def get_scale_direction(self, corner):
"""Get the direction in which the scale for this corner increases"""
x = 1 if corner[0] > 0 else -1
y = 1 if corner[1] > 0 else -1
return (x, y)
def translate_for_scale_anchor(self, scale_factor):
"""Adjust the item's position so that a scale with the given scale
factor appears to operate around the scale anchor. ``setScale``
needs to be called separately with ``scale_factor`` multiplied by
the item's current scale factor.
"""
factor = self.scale_orig_factor * (scale_factor - 1)
self.setPos(
self.scale_orig_pos.x() - self.scale_anchor.x() * factor,
self.scale_orig_pos.y() - self.scale_anchor.y() * factor,
)
def mouseMoveEvent(self, event):
if self.scale_active:
factor = self.get_scale_factor(event)
for item in self.selection_action_items():
item.setScale(item.scale_orig_factor * factor)
item.translate_for_scale_anchor(factor)
event.accept()
else:
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
if self.scale_active:
self.scene().undo_stack.push(
commands.ScaleItemsBy(
self.selection_action_items(),
self.get_scale_factor(event),
ignore_first_redo=True))
self.scale_active = False
event.accept()
else:
super().mouseReleaseEvent(event)
def on_view_scale_change(self):
self.prepareGeometryChange()
def itemChange(self, change, value):
if change == QGraphicsItem.GraphicsItemChange.ItemSelectedChange:
self.prepareGeometryChange()
if hasattr(self, 'on_selected_change'):
self.on_selected_change(value)
return super().itemChange(change, value)
class BeePixmapItem(SelectableMixin, QtWidgets.QGraphicsPixmapItem):
"""Class for images added by the user."""
@ -272,21 +38,7 @@ class BeePixmapItem(SelectableMixin, QtWidgets.QGraphicsPixmapItem):
self.init_selectable()
def __str__(self):
return (f'Image "{self.filename}" '
f'with dimensions {self.width} x {self.height}')
def setScale(self, factor):
if factor <= 0:
return
logger.debug(f'Setting scale for image "{self.filename}" to {factor}')
self.prepareGeometryChange()
super().setScale(factor)
def setZValue(self, value):
logger.debug(f'Setting z-value for image "{self.filename}" to {value}')
super().setZValue(value)
self.scene().max_z = max(self.scene().max_z, value)
return (f'Image "{self.filename}" {self.width} x {self.height}')
def set_pos_center(self, x, y):
"""Sets the position using the item's center as the origin point."""
@ -302,11 +54,6 @@ class BeePixmapItem(SelectableMixin, QtWidgets.QGraphicsPixmapItem):
def height(self):
return self.pixmap().size().height()
@property
def scene_bottomright(self):
"""The bottom right corner in scene coordinates."""
return self.mapToScene(QtCore.QPointF(self.width, self.height))
def pixmap_to_bytes(self):
"""Convert the pixmap data to PNG bytestring."""
barray = QtCore.QByteArray()
@ -340,58 +87,3 @@ class BeePixmapItem(SelectableMixin, QtWidgets.QGraphicsPixmapItem):
def on_selected_change(self, value):
if(value and self.scene() and not self.scene().has_selection()):
self.bring_to_front()
class MultiSelectItem(SelectableMixin, QtWidgets.QGraphicsRectItem):
"""Class for images added by the user."""
def __init__(self):
super().__init__()
self.init_selectable()
def __str__(self):
return (f'MultiSelectItem '
f'with dimensions {self.width} x {self.height}')
@property
def width(self):
return self.rect().width()
@property
def height(self):
return self.rect().height()
def paint(self, painter, option, widget):
self.paint_selectable(painter, option, widget)
def has_selection_outline(self):
return True
def has_selection_handles(self):
return True
def selection_action_items(self):
"""The items affected by selection actions like scaling and rotating.
"""
return list(self.scene().selectedItems())
def fit_selection_area(self, rect):
"""Updates itself to fit the given selection area."""
logging.debug(f'Fit selection area to {rect}')
self.setRect(0, 0, rect.width(), rect.height())
self.setPos(rect.topLeft())
self.setScale(1)
self.setRotation(0)
self.setSelected(True)
def mousePressEvent(self, event):
if (event.button() == Qt.MouseButtons.LeftButton
and event.modifiers() == Qt.KeyboardModifiers.ControlModifier):
# We still need to be able to select additional images
# within/"under" the multi select rectangle, so let ctrl+click
# events pass through
event.ignore()
return
super().mousePressEvent(event)

View file

@ -20,7 +20,7 @@ from PyQt6 import QtCore, QtWidgets
from PyQt6.QtCore import Qt
from beeref import commands
from beeref.items import MultiSelectItem
from beeref.selection import MultiSelectItem
logger = logging.getLogger('BeeRef')
@ -119,13 +119,13 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene):
commands.MoveItemsBy(self.selectedItems(),
delta.x(), delta.y(),
ignore_first_redo=True))
self.move_active = False
self.move_active = False
super().mouseReleaseEvent(event)
def items_for_save(self):
"""Returns the items that are to be saved.
Items to be saved are items that have an save_id attribute.
Items to be saved are items that have a save_id attribute.
"""
return filter(lambda i: hasattr(i, 'save_id'),
@ -144,19 +144,17 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene):
items = list(filter(lambda i: hasattr(i, 'save_id'),
self.selectedItems()))
x = []
y = []
topleft = items[0].pos()
bottomright = items[0].scene_bottomright
for item in items:
for corner in item.corners_scene_coords:
x.append(corner.x())
y.append(corner.y())
for item in items[1:]:
tl = item.pos()
br = item.scene_bottomright
topleft.setX(min(topleft.x(), tl.x(), br.x()))
topleft.setY(min(topleft.y(), tl.y(), br.y()))
bottomright.setX(max(bottomright.x(), tl.x(), br.x()))
bottomright.setY(max(bottomright.y(), tl.y(), br.y()))
return QtCore.QRectF(topleft, bottomright)
return QtCore.QRectF(
QtCore.QPointF(min(x), min(y)),
QtCore.QPointF(max(x), max(y)))
def on_selection_change(self):
if self.has_multi_selection():

332
beeref/selection.py Normal file
View file

@ -0,0 +1,332 @@
# This file is part of BeeRef.
#
# BeeRef is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BeeRef is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BeeRef. If not, see <https://www.gnu.org/licenses/>.
"""Classes for items that draw and handle selection stuff."""
import logging
from PyQt6 import QtCore, QtGui, QtWidgets
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QGraphicsItem
from beeref import commands
from beeref.config import CommandlineArgs
commandline_args = CommandlineArgs()
logger = logging.getLogger('BeeRef')
class SelectableMixin:
"""Common code for selectable items: Selection outline, handles etc."""
select_color = QtGui.QColor(116, 234, 231, 255)
SELECT_LINE_WIDTH = 4 # line width for the selection box
SELECT_HANDLE_SIZE = 15 # size of selection handles for scaling
SELECT_RESIZE_SIZE = 20 # size of hover area for scaling
SELECT_ROTATE_SIZE = 20 # size of hover area for rotating
def init_selectable(self):
self.setAcceptHoverEvents(True)
self.setFlags(
QtWidgets.QGraphicsItem.GraphicsItemFlags.ItemIsMovable
| QtWidgets.QGraphicsItem.GraphicsItemFlags.ItemIsSelectable)
self.scale_active = False
self.viewport_scale = 1
self.conf_debug_shapes = commandline_args.draw_debug_shapes
def setScale(self, factor):
if factor <= 0:
return
logger.debug(f'Setting scale for {self} to {factor}')
self.prepareGeometryChange()
super().setScale(factor)
def setZValue(self, value):
logger.debug(f'Setting z-value for image {self} to {value}')
super().setZValue(value)
self.scene().max_z = max(self.scene().max_z, value)
def bring_to_front(self):
self.setZValue(self.scene().max_z + 0.001)
def fixed_length_for_viewport(self, value):
"""The interactable areas need to stay the same size on the
screen so we need to adjust the values according to the scale
factor sof the view and the item."""
if self.scene():
scale = self.scene().views()[0].get_scale()
self._view_scale = scale
# It can happen that the item is already removed from
# the scene but its boundingRect is still needed. Keep the
# last known scaling factor for that case
return value / getattr(self, '_view_scale', 1) / self.scale()
@property
def select_resize_size(self):
return self.fixed_length_for_viewport(self.SELECT_RESIZE_SIZE)
@property
def select_rotate_size(self):
return self.fixed_length_for_viewport(self.SELECT_ROTATE_SIZE)
def draw_debug_shape(self, painter, shape, r, g, b):
color = QtGui.QColor(r, g, b, 50)
if isinstance(shape, QtCore.QRectF):
painter.fillRect(shape, color)
else:
painter.fillPath(shape, color)
def paint_selectable(self, painter, option, widget):
if self.conf_debug_shapes:
self.draw_debug_shape(painter, self.boundingRect(), 0, 255, 0)
self.draw_debug_shape(painter, self.shape(), 255, 0, 0)
if not self.has_selection_outline():
return
pen = QtGui.QPen(self.select_color)
pen.setWidth(self.SELECT_LINE_WIDTH)
pen.setCosmetic(True)
painter.setPen(pen)
# Draw the main selection rectangle
painter.drawRect(0, 0, self.width, self.height)
# If it's a single selection, draw the handles:
if self.has_selection_handles():
pen.setWidth(self.SELECT_HANDLE_SIZE)
painter.setPen(pen)
for corner in self.corners:
painter.drawPoint(corner)
@property
def corners(self):
"""The corners of the item. Used for scale and rotate handles."""
return (QtCore.QPointF(0, 0),
QtCore.QPointF(0, self.height),
QtCore.QPointF(self.width, 0),
QtCore.QPointF(self.width, self.height))
@property
def corners_scene_coords(self):
"""The corners of the item mapped to scene coordinates."""
return [self.mapToScene(corner) for corner in self.corners]
def get_scale_bounds(self, center):
"""The interactable shape of the scale handles."""
return QtCore.QRectF(
center.x() - self.select_resize_size/2,
center.y() - self.select_resize_size/2,
self.select_resize_size,
self.select_resize_size)
@property
def bottom_right_rotate_bounds(self):
"""The interactable shape of the bottom right rotate handle"""
return QtCore.QRectF(
self.width + self.select_resize_size / 2,
self.height + self.select_resize_size / 2,
self.select_rotate_size, self.select_rotate_size)
def boundingRect(self):
bounds = super().boundingRect()
if not self.has_selection_outline():
return bounds
# Add extra space for scale and rotate interactive areas
margin = self.select_resize_size / 2 + self.select_rotate_size
return QtCore.QRectF(
bounds.topLeft().x() - margin,
bounds.topLeft().y() - margin,
bounds.bottomRight().x() + 2 * margin,
bounds.bottomRight().y() + 2 * margin)
def shape(self):
shape_ = super().shape()
if self.has_selection_outline():
# Add extra space for scale and rotate interactive areas
path = QtGui.QPainterPath()
for corner in self.corners:
path.addRect(self.get_scale_bounds(corner))
path.addRect(self.bottom_right_rotate_bounds)
shape_ = shape_ + path
return shape_
def hoverMoveEvent(self, event):
if not self.has_selection_handles():
return
for corner in self.corners:
# See if we need to change the cursor for scale areas
if self.get_scale_bounds(corner).contains(event.pos()):
direction = self.get_scale_direction(corner)
if direction[0] == direction[1]:
self.setCursor(Qt.CursorShape.SizeFDiagCursor)
else:
self.setCursor(Qt.CursorShape.SizeBDiagCursor)
return
if self.bottom_right_rotate_bounds.contains(event.pos()):
self.setCursor(Qt.CursorShape.ForbiddenCursor)
return
self.setCursor(Qt.CursorShape.ArrowCursor)
def hoverEnterEvent(self, event):
# Always return regular cursor when there aren't any selection handles
if not self.has_selection_handles():
self.setCursor(Qt.CursorShape.ArrowCursor)
def mousePressEvent(self, event):
if (event.button() == Qt.MouseButtons.LeftButton
and self.has_selection_handles()):
for corner in self.corners:
# Check if we are in one of the corner's scale areas
if self.get_scale_bounds(corner).contains(event.pos()):
# Start scale action for this corner
self.scale_active = True
self.scale_start = event.scenePos()
self.scale_direction = self.get_scale_direction(corner)
for item in self.selection_action_items():
item.scale_anchor = self.get_scale_anchor(item, corner)
item.scale_orig_factor = item.scale()
item.scale_orig_pos = item.pos()
event.accept()
return
super().mousePressEvent(event)
def get_scale_factor(self, event):
imgsize = self.width + self.height
p = event.scenePos() - self.scale_start
direction = self.scale_direction
delta = (direction[0] * p.x() + direction[1] * p.y()) / imgsize
return (self.scale_orig_factor + delta) / self.scale_orig_factor
def get_scale_anchor(self, item, corner):
"""Get the anchor around which the scale for this corner operates."""
return item.mapFromScene(
self.mapToScene(self.width - corner.x(), self.height - corner.y()))
def get_scale_direction(self, corner):
"""Get the direction in which the scale for this corner increases"""
x = 1 if corner.x() > 0 else -1
y = 1 if corner.y() > 0 else -1
return (x, y)
def translate_for_scale_anchor(self, scale_factor):
"""Adjust the item's position so that a scale with the given scale
factor appears to operate around the scale anchor. ``setScale``
needs to be called separately with ``scale_factor`` multiplied by
the item's current scale factor.
"""
factor = self.scale_orig_factor * (scale_factor - 1)
self.setPos(
self.scale_orig_pos.x() - self.scale_anchor.x() * factor,
self.scale_orig_pos.y() - self.scale_anchor.y() * factor,
)
def mouseMoveEvent(self, event):
if self.scale_active:
factor = self.get_scale_factor(event)
for item in self.selection_action_items():
item.setScale(item.scale_orig_factor * factor)
item.translate_for_scale_anchor(factor)
event.accept()
else:
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
if self.scale_active:
self.scene().undo_stack.push(
commands.ScaleItemsBy(
self.selection_action_items(),
self.get_scale_factor(event),
ignore_first_redo=True))
self.scale_active = False
event.accept()
else:
super().mouseReleaseEvent(event)
def on_view_scale_change(self):
self.prepareGeometryChange()
def itemChange(self, change, value):
if change == QGraphicsItem.GraphicsItemChange.ItemSelectedChange:
self.prepareGeometryChange()
if hasattr(self, 'on_selected_change'):
self.on_selected_change(value)
return super().itemChange(change, value)
class MultiSelectItem(SelectableMixin, QtWidgets.QGraphicsRectItem):
"""Class for images added by the user."""
def __init__(self):
super().__init__()
self.init_selectable()
def __str__(self):
return (f'MultiSelectItem {self.width} x {self.height}')
@property
def width(self):
return self.rect().width()
@property
def height(self):
return self.rect().height()
def paint(self, painter, option, widget):
self.paint_selectable(painter, option, widget)
def has_selection_outline(self):
return True
def has_selection_handles(self):
return True
def selection_action_items(self):
"""The items affected by selection actions like scaling and rotating.
"""
return list(self.scene().selectedItems())
def fit_selection_area(self, rect):
"""Updates itself to fit the given selection area."""
logging.debug(f'Fit selection area to {rect}')
self.setRect(0, 0, rect.width(), rect.height())
self.setPos(rect.topLeft())
self.setScale(1)
self.setRotation(0)
self.setSelected(True)
def mousePressEvent(self, event):
if (event.button() == Qt.MouseButtons.LeftButton
and event.modifiers() == Qt.KeyboardModifiers.ControlModifier):
# We still need to be able to select additional images
# within/"under" the multi select rectangle, so let ctrl+click
# events pass through
event.ignore()
return
super().mousePressEvent(event)

View file

@ -1,7 +1,6 @@
from unittest.mock import patch, MagicMock, PropertyMock
from PyQt6 import QtCore, QtGui, QtWidgets
from PyQt6.QtCore import Qt
from PyQt6 import QtGui
from beeref.items import BeePixmapItem
from beeref.scene import BeeGraphicsScene
@ -13,37 +12,16 @@ class BeePixmapItemTestCase(BeeTestCase):
def setUp(self):
self.scene = BeeGraphicsScene(None)
def test_init(self):
@patch('beeref.selection.SelectableMixin.init_selectable')
def test_init(self, selectable_mock):
item = BeePixmapItem(
QtGui.QImage(self.imgfilename3x3), self.imgfilename3x3)
assert item.save_id is None
assert item.width == 3
assert item.height == 3
assert item.scale() == 1
assert item.flags() == (
QtWidgets.QGraphicsItem.GraphicsItemFlags.ItemIsMovable
| QtWidgets.QGraphicsItem.GraphicsItemFlags.ItemIsSelectable)
assert item.filename == self.imgfilename3x3
def test_set_scale(self):
item = BeePixmapItem(
QtGui.QImage(self.imgfilename3x3), self.imgfilename3x3)
item.prepareGeometryChange = MagicMock()
item.setScale(3)
assert item.scale() == 3
assert item.pos().x() == 0
assert item.pos().y() == 0
item.prepareGeometryChange.assert_called_once()
def test_set_scale_ignores_zero(self):
item = BeePixmapItem(QtGui.QImage())
item.setScale(0)
assert item.scale() == 1
def test_set_scale_ignores_negative(self):
item = BeePixmapItem(QtGui.QImage())
item.setScale(-0.1)
assert item.scale() == 1
selectable_mock.assert_called_once()
def test_set_pos_center(self):
item = BeePixmapItem(QtGui.QImage())
@ -55,411 +33,69 @@ class BeePixmapItemTestCase(BeeTestCase):
assert item.pos().x() == -100
assert item.pos().y() == -50
def test_set_zvalue_sets_new_max(self):
def test_pixmap_to_bytes(self):
item = BeePixmapItem(QtGui.QImage(self.imgfilename3x3))
assert item.pixmap_to_bytes().startswith(b'\x89PNG')
def test_pixmap_from_bytes(self):
item = BeePixmapItem(QtGui.QImage())
with open(self.imgfilename3x3, 'rb') as f:
imgdata = f.read()
item.pixmap_from_bytes(imgdata)
assert item.width == 3
assert item.height == 3
def test_paint(self):
item = BeePixmapItem(QtGui.QImage())
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')
def test_has_selection_outline_when_not_selected(self):
item = BeePixmapItem(QtGui.QImage())
self.scene.addItem(item)
item.setZValue(1.1)
assert item.zValue() == 1.1
assert self.scene.max_z == 1.1
item.setSelected(False)
item.has_selection_outline() is False
def test_set_zvalue_keeps_old_max(self):
def test_has_selection_outline_when_selected(self):
item = BeePixmapItem(QtGui.QImage())
self.scene.addItem(item)
self.scene.max_z = 3.3
item.setZValue(1.1)
assert item.zValue() == 1.1
assert self.scene.max_z == 3.3
item.setSelected(True)
item.has_selection_outline() is True
def test_bring_to_front(self):
def test_has_selection_handles_when_not_selected(self):
item1 = BeePixmapItem(QtGui.QImage())
self.scene.addItem(item1)
item1.setZValue(3.3)
item1.setSelected(False)
item2 = BeePixmapItem(QtGui.QImage())
self.scene.addItem(item2)
item2.bring_to_front()
assert item2.zValue() > item1.zValue()
assert item2.zValue() == self.scene.max_z
item2.setSelected(False)
item1.has_selection_handles() is False
def test_on_view_scale_change(self):
item = BeePixmapItem(QtGui.QImage())
with patch('beeref.items.BeePixmapItem.prepareGeometryChange') as m:
item.on_view_scale_change()
m.assert_called_once()
class BeePixmapItemWithViewBaseTestCase(BeeTestCase):
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)
class BeePixmapItemPaintstuffTestCase(BeePixmapItemWithViewBaseTestCase):
def test_fixed_length_for_viewport_when_default_scales(self):
self.view.get_scale = MagicMock(return_value=1)
assert self.item.fixed_length_for_viewport(100) == 100
assert self.item._view_scale == 1
def test_fixed_length_for_viewport_when_viewport_scaled(self):
self.view.get_scale = MagicMock(return_value=2)
assert self.item.fixed_length_for_viewport(100) == 50
assert self.item._view_scale == 2
def test_fixed_length_for_viewport_when_item_scaled(self):
self.view.get_scale = MagicMock(return_value=1)
self.item.setScale(5)
assert self.item.fixed_length_for_viewport(100) == 20
assert self.item._view_scale == 1
def test_fixed_length_for_viewport_when_no_scene(self):
item = BeePixmapItem(QtGui.QImage())
item._view_scale = 0.5
assert item.fixed_length_for_viewport(100) == 200
def test_resize_size_when_scaled(self):
self.view.get_scale = MagicMock(return_value=2)
self.item.setScale(2)
self.item.SELECT_RESIZE_SIZE = 100
assert self.item.select_resize_size == 25
def test_rotate_size_when_scaled(self):
self.view.get_scale = MagicMock(return_value=2)
self.item.setScale(2)
self.item.SELECT_ROTATE_SIZE = 100
assert self.item.select_rotate_size == 25
@patch('beeref.items.BeePixmapItem.draw_debug_shape')
def test_paint_when_not_selected(self, debug_mock):
painter = MagicMock()
self.item.setSelected(False)
self.item.pixmap = MagicMock(return_value='pixmap')
self.item.paint(painter, None, None)
painter.drawPixmap.assert_called_once_with(0, 0, 'pixmap')
painter.drawRect.assert_not_called()
painter.drawPoint.assert_not_called()
debug_mock.assert_not_called()
def test_paint_when_selected_single_selection(self):
painter = MagicMock()
self.item.setSelected(True)
self.item.paint(painter, None, None)
painter.drawPixmap.assert_called_once()
painter.drawRect.assert_called_once()
assert painter.drawPoint.call_count == 4
def test_paint_when_selected_multi_selection(self):
def test_has_selection_handles_when_selected_single(self):
item1 = BeePixmapItem(QtGui.QImage())
self.scene.addItem(item1)
item1.setSelected(True)
item2 = BeePixmapItem(QtGui.QImage())
item2.setSelected(True)
self.scene.addItem(item2)
painter = MagicMock()
self.item.setSelected(True)
self.item.paint(painter, None, None)
painter.drawPixmap.assert_called_once()
painter.drawRect.assert_called_once()
painter.drawPoint.assert_not_called()
item2.setSelected(False)
item1.has_selection_handles() is True
def test_paint_when_draw_debug_shapes(self):
with patch('beeref.items.commandline_args') as args_mock:
with patch('beeref.items.BeePixmapItem.draw_debug_shape') as m:
args_mock.draw_debug_shapes = True
item = BeePixmapItem(QtGui.QImage())
item.paint(MagicMock(), None, None)
m.assert_called()
def test_has_selection_handles_when_selected_multi(self):
view = MagicMock(get_scale=MagicMock(return_value=1))
with patch('beeref.scene.BeeGraphicsScene.views',
return_value=[view]):
item1 = BeePixmapItem(QtGui.QImage())
self.scene.addItem(item1)
item1.setSelected(True)
item2 = BeePixmapItem(QtGui.QImage())
self.scene.addItem(item2)
item2.setSelected(True)
item1.has_selection_handles() is False
def test_corners(self):
assert set(self.item.corners) == set((
(0, 0),
(100, 0),
(0, 80),
(100, 80)))
def test_get_scale_bounds(self):
self.view.get_scale = MagicMock(return_value=1)
self.item.SELECT_RESIZE_SIZE = 10
rect = self.item.get_scale_bounds((100, 100))
assert rect.topLeft().x() == 95
assert rect.topLeft().y() == 95
assert rect.bottomRight().x() == 105
assert rect.bottomRight().y() == 105
def test_bottom_right_rotate_bounds(self):
self.view.get_scale = MagicMock(return_value=1)
self.item.SELECT_RESIZE_SIZE = 10
self.item.SELECT_ROTATE_SIZE = 10
rect = self.item.bottom_right_rotate_bounds
assert rect.topLeft().x() == 105
assert rect.topLeft().y() == 85
assert rect.bottomRight().x() == 115
assert rect.bottomRight().y() == 95
def test_bounding_rect_when_not_selected(self):
self.view.get_scale = MagicMock(return_value=1)
self.item.setSelected(False)
with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.boundingRect',
return_value=QtCore.QRectF(0, 0, 100, 80)):
rect = self.item.boundingRect()
assert rect.topLeft().x() == 0
assert rect.topLeft().y() == 0
assert rect.bottomRight().x() == 100
assert rect.bottomRight().y() == 80
def test_bounding_rect_when_selected(self):
self.item.SELECT_RESIZE_SIZE = 10
self.item.SELECT_ROTATE_SIZE = 10
self.view.get_scale = MagicMock(return_value=1)
self.item.setSelected(True)
with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.boundingRect',
return_value=QtCore.QRectF(0, 0, 100, 80)):
rect = self.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(self):
self.view.get_scale = MagicMock(return_value=1)
self.item.setSelected(False)
path = QtGui.QPainterPath()
path.addRect(QtCore.QRectF(0, 0, 100, 80))
with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.shape',
return_value=path):
shape = self.item.shape().boundingRect()
assert shape.topLeft().x() == 0
assert shape.topLeft().y() == 0
assert shape.bottomRight().x() == 100
assert shape.bottomRight().y() == 80
def test_shape_when_selected(self):
self.item.SELECT_RESIZE_SIZE = 10
self.item.SELECT_ROTATE_SIZE = 10
self.view.get_scale = MagicMock(return_value=1)
self.item.setSelected(True)
path = QtGui.QPainterPath()
path.addRect(QtCore.QRectF(0, 0, 100, 80))
with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.shape',
return_value=path):
shape = self.item.shape().boundingRect()
assert shape.topLeft().x() == -5
assert shape.topLeft().y() == -5
assert shape.bottomRight().x() == 115
assert shape.bottomRight().y() == 95
class BeePixmapItemScalingTestCase(BeePixmapItemWithViewBaseTestCase):
def test_get_scale_factor_bottomright(self):
self.item.scale_start = QtCore.QPoint(10, 10)
self.item.scale_direction = (1, 1)
self.item.scale_orig_factor = 1
event = MagicMock()
event.scenePos = MagicMock(return_value=QtCore.QPoint(20, 90))
assert self.item.get_scale_factor(event) == 1.5
def test_get_scale_delta_topleft(self):
self.item.scale_start = QtCore.QPoint(10, 10)
self.item.scale_direction = (-1, -1)
self.item.scale_orig_factor = 0.5
event = MagicMock()
event.scenePos = MagicMock(return_value=QtCore.QPoint(-10, -60))
assert self.item.get_scale_factor(event) == 2
def test_get_scale_anchor_topleft(self):
anchor = self.item.get_scale_anchor(self.item, (0, 0))
assert anchor.x() == 100
assert anchor.y() == 80
def test_get_scale_anchor_bottomright(self):
anchor = self.item.get_scale_anchor(self.item, (100, 80))
assert anchor.x() == 0
assert anchor.y() == 0
def test_get_scale_anchor_topright(self):
anchor = self.item.get_scale_anchor(self.item, (100, 0))
assert anchor.x() == 0
assert anchor.y() == 80
def test_get_scale_anchor_bottomleft(self):
anchor = self.item.get_scale_anchor(self.item, (0, 80))
assert anchor.x() == 100
assert anchor.y() == 0
def test_get_scale_direction_topleft(self):
assert self.item.get_scale_direction((0, 0)) == (-1, -1)
def test_get_scale_direction_bottomright(self):
assert self.item.get_scale_direction((100, 80)) == (1, 1)
def test_get_scale_direction_topright(self):
assert self.item.get_scale_direction((100, 0)) == (1, -1)
def test_get_scale_direction_bottomleft(self):
assert self.item.get_scale_direction((0, 80)) == (-1, 1)
def test_translate_for_scale_anchor(self):
self.item.scale_orig_pos = QtCore.QPoint(50, 70)
self.item.scale_anchor = QtCore.QPoint(100, 80)
self.item.scale_orig_factor = 1
self.item.translate_for_scale_anchor(3)
assert self.item.pos().x() == -150
assert self.item.pos().y() == -90
class BeePixmapItemEventsstuffTestCase(BeePixmapItemWithViewBaseTestCase):
def setUp(self):
super().setUp()
self.event = MagicMock()
self.item.setCursor = MagicMock()
def test_hover_move_event_no_selection(self):
self.event.pos = MagicMock(return_value=QtCore.QPointF(0, 0))
self.item.hoverMoveEvent(self.event)
self.item.setCursor.assert_not_called()
def test_hover_move_event_topleft_scale(self):
self.item.setSelected(True)
self.event.pos = MagicMock(return_value=QtCore.QPointF(0, 0))
self.item.hoverMoveEvent(self.event)
self.item.setCursor.assert_called_once_with(
Qt.CursorShape.SizeFDiagCursor)
def test_hover_move_event_bottomright_scale(self):
self.item.setSelected(True)
self.event.pos = MagicMock(return_value=QtCore.QPointF(100, 80))
self.item.hoverMoveEvent(self.event)
self.item.setCursor.assert_called_once_with(
Qt.CursorShape.SizeFDiagCursor)
def test_hover_move_event_topright_scale(self):
self.item.setSelected(True)
self.event.pos = MagicMock(return_value=QtCore.QPointF(100, 0))
self.item.hoverMoveEvent(self.event)
self.item.setCursor.assert_called_once_with(
Qt.CursorShape.SizeBDiagCursor)
def test_hover_move_event_not_in_handles(self):
self.item.setSelected(True)
self.event.pos = MagicMock(return_value=QtCore.QPointF(50, 50))
self.item.hoverMoveEvent(self.event)
self.item.setCursor.assert_called_once_with(
Qt.CursorShape.ArrowCursor)
def test_hover_enter_event_when_selected(self):
self.item.setSelected(True)
self.item.hoverEnterEvent(self.event)
self.item.setCursor.assert_not_called()
def test_hover_enter_event_when_not_selected(self):
self.item.setSelected(False)
self.item.hoverEnterEvent(self.event)
self.item.setCursor.assert_called_once_with(
Qt.CursorShape.ArrowCursor)
def test_mouse_press_event_topleft_scale(self):
self.item.setSelected(True)
self.event.pos = MagicMock(return_value=QtCore.QPointF(2, 2))
self.event.scenePos = MagicMock(return_value=QtCore.QPointF(66, 99))
self.event.button = MagicMock(
return_value=Qt.MouseButtons.LeftButton)
self.item.mousePressEvent(self.event)
assert self.item.scale_active is True
assert self.item.scale_start == QtCore.QPointF(66, 99)
assert self.item.scale_direction == (-1, -1)
assert self.item.scale_orig_factor == 1
assert self.item.scale_orig_pos == QtCore.QPointF(0, 0)
def test_mouse_press_event_bottomright_scale(self):
self.item.setSelected(True)
self.event.pos = MagicMock(return_value=QtCore.QPointF(99, 79))
self.event.scenePos = MagicMock(return_value=QtCore.QPointF(66, 99))
self.event.button = MagicMock(
return_value=Qt.MouseButtons.LeftButton)
self.item.mousePressEvent(self.event)
assert self.item.scale_active is True
assert self.item.scale_direction == (1, 1)
assert self.item.scale_start == QtCore.QPointF(66, 99)
assert self.item.scale_orig_factor == 1
assert self.item.scale_orig_pos == QtCore.QPointF(0, 0)
def test_mouse_press_event_not_selected(self):
self.item.setSelected(False)
with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.mousePressEvent') as m:
self.item.mousePressEvent(self.event)
m.assert_called_once_with(self.event)
assert self.item.scale_active is False
def test_mouse_press_not_in_handles(self):
self.item.setSelected(True)
self.event.pos = MagicMock(return_value=QtCore.QPointF(50, 40))
self.event.button = MagicMock(
return_value=Qt.MouseButtons.LeftButton)
with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.mousePressEvent') as m:
self.item.mousePressEvent(self.event)
m.assert_called_once_with(self.event)
assert self.item.scale_active is False
def test_mouse_move_event_when_no_action(self):
with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.mouseMoveEvent') as m:
self.item.mouseMoveEvent(self.event)
m.assert_called_once_with(self.event)
def test_move_event_when_scale_action(self):
self.event.scenePos = MagicMock(return_value=QtCore.QPointF(20, 90))
self.item.scale_active = True
self.item.scale_direction = (1, 1)
self.item.scale_anchor = QtCore.QPointF(100, 80)
self.item.scale_start = QtCore.QPointF(10, 10)
self.item.scale_orig_factor = 1
self.item.scale_orig_pos = QtCore.QPointF(0, 0)
self.item.mouseMoveEvent(self.event)
assert self.item.scale() == 1.5
def test_mouse_release_event_when_no_action(self):
with patch('PyQt6.QtWidgets.QGraphicsPixmapItem'
'.mouseReleaseEvent') as m:
self.item.mouseReleaseEvent(self.event)
m.assert_called_once_with(self.event)
def test_mouse_release_event_when_scale_action(self):
self.event.scenePos = MagicMock(return_value=QtCore.QPointF(20, 90))
self.item.scale_active = True
self.item.scale_direction = (1, 1)
self.item.scale_anchor = QtCore.QPointF(100, 80)
self.item.scale_start = QtCore.QPointF(10, 10)
self.item.scale_orig_factor = 1
self.item.scale_orig_pos = QtCore.QPointF(0, 0)
self.scene.undo_stack = MagicMock()
self.scene.undo_stack.push = MagicMock()
self.item.mouseReleaseEvent(self.event)
self.scene.undo_stack.push.assert_called_once()
args = self.scene.undo_stack.push.call_args_list[0][0]
cmd = args[0]
assert cmd.items == [self.item]
assert cmd.factor == 1.5
assert cmd.item_data == [{
'anchor': QtCore.QPointF(100, 80),
'orig_factor': 1,
'orig_pos': QtCore.QPointF(0, 0)}]
assert cmd.ignore_first_redo is True
assert self.item.scale_active is False
def test_selection_action_items(self):
item = BeePixmapItem(QtGui.QImage())
assert item.selection_action_items() == [item]

View file

@ -1,7 +1,10 @@
import math
from unittest.mock import patch, MagicMock
from unittest.mock import patch, MagicMock, PropertyMock
from PyQt6 import QtGui, QtWidgets
from pytest import approx
from PyQt6 import QtCore, QtGui, QtWidgets
from PyQt6.QtCore import Qt
from beeref.items import BeePixmapItem
from beeref.scene import BeeGraphicsScene
@ -13,6 +16,10 @@ class BeeGraphicsSceneNormalizeTestCase(BeeTestCase):
def setUp(self):
self.undo_stack = QtGui.QUndoStack()
self.scene = BeeGraphicsScene(self.undo_stack)
self.view = MagicMock(get_scale=MagicMock(return_value=1))
views_patcher = patch('beeref.scene.BeeGraphicsScene.views',
return_value=[self.view])
views_patcher.start()
def test_normalize_height(self):
item1 = MagicMock(height=100, scale_factor=1)
@ -56,6 +63,144 @@ class BeeGraphicsSceneNormalizeTestCase(BeeTestCase):
def test_normalize_size_when_no_items(self):
self.scene.normalize_size()
def test_has_selection_when_no_selection(self):
item = BeePixmapItem(QtGui.QImage())
self.scene.addItem(item)
assert self.scene.has_selection() is False
def test_has_selection_when_selection(self):
item = BeePixmapItem(QtGui.QImage())
self.scene.addItem(item)
item.setSelected(True)
assert self.scene.has_selection() is True
def test_has_single_selection_when_no_selection(self):
item = BeePixmapItem(QtGui.QImage())
self.scene.addItem(item)
assert self.scene.has_single_selection() is False
def test_has_single_selection_when_single_selection(self):
item1 = BeePixmapItem(QtGui.QImage())
self.scene.addItem(item1)
item1.setSelected(True)
item2 = BeePixmapItem(QtGui.QImage())
self.scene.addItem(item2)
assert self.scene.has_single_selection() is True
def test_has_single_selection_when_multi_selection(self):
item1 = BeePixmapItem(QtGui.QImage())
self.scene.addItem(item1)
item1.setSelected(True)
item2 = BeePixmapItem(QtGui.QImage())
self.scene.addItem(item2)
item2.setSelected(True)
assert self.scene.has_single_selection() is False
def test_has_multi_selection_when_no_selection(self):
item = BeePixmapItem(QtGui.QImage())
self.scene.addItem(item)
assert self.scene.has_multi_selection() is False
def test_has_multi_selection_when_single_selection(self):
item1 = BeePixmapItem(QtGui.QImage())
self.scene.addItem(item1)
item1.setSelected(True)
item2 = BeePixmapItem(QtGui.QImage())
self.scene.addItem(item2)
assert self.scene.has_multi_selection() is False
def test_has_multi_selection_when_multi_selection(self):
item1 = BeePixmapItem(QtGui.QImage())
self.scene.addItem(item1)
item1.setSelected(True)
item2 = BeePixmapItem(QtGui.QImage())
self.scene.addItem(item2)
item2.setSelected(True)
assert self.scene.has_multi_selection() is True
@patch('PyQt6.QtWidgets.QGraphicsScene.mousePressEvent')
def test_mouse_press_event_when_right_click(self, mouse_mock):
event = MagicMock(
button=MagicMock(return_value=Qt.MouseButtons.RightButton))
self.scene.mousePressEvent(event)
event.accept.assert_not_called()
mouse_mock.assert_not_called()
@patch('PyQt6.QtWidgets.QGraphicsScene.mousePressEvent')
def test_mouse_press_event_when_left_click(self, mouse_mock):
event = MagicMock(
button=MagicMock(return_value=Qt.MouseButtons.LeftButton),
scenePos=MagicMock(return_value=QtCore.QPoint(10, 20)),
)
self.scene.mousePressEvent(event)
event.accept.assert_not_called()
mouse_mock.assert_called_once_with(event)
assert self.scene.move_active is True
assert self.scene.move_start == QtCore.QPoint(10, 20)
@patch('PyQt6.QtWidgets.QGraphicsScene.mouseReleaseEvent')
def test_mouse_release_event_when_move_active(self, mouse_mock):
item = BeePixmapItem(QtGui.QImage())
self.scene.addItem(item)
item.setSelected(True)
event = MagicMock(
scenePos=MagicMock(return_value=QtCore.QPoint(10, 20)))
self.scene.move_active = True
self.scene.move_start = QtCore.QPoint(0, 0)
self.scene.undo_stack = MagicMock(push=MagicMock())
self.scene.mouseReleaseEvent(event)
self.scene.undo_stack.push.assert_called_once()
args = self.scene.undo_stack.push.call_args_list[0][0]
cmd = args[0]
assert cmd.items == [item]
assert cmd.ignore_first_redo is True
assert cmd.delta_x == 10
assert cmd.delta_y == 20
mouse_mock.assert_called_once_with(event)
assert self.scene.move_active is False
@patch('PyQt6.QtWidgets.QGraphicsScene.mouseReleaseEvent')
def test_mouse_release_event_when_move_not_active(self, mouse_mock):
item = BeePixmapItem(QtGui.QImage())
self.scene.addItem(item)
item.setSelected(True)
event = MagicMock(
scenePos=MagicMock(return_value=QtCore.QPoint(10, 20)))
self.scene.move_active = False
self.scene.undo_stack = MagicMock(push=MagicMock())
self.scene.mouseReleaseEvent(event)
self.scene.undo_stack.push.assert_not_called()
mouse_mock.assert_called_once_with(event)
assert self.scene.move_active is False
@patch('PyQt6.QtWidgets.QGraphicsScene.mouseReleaseEvent')
def test_mouse_release_event_when_no_selection(self, mouse_mock):
item = BeePixmapItem(QtGui.QImage())
self.scene.addItem(item)
item.setSelected(False)
event = MagicMock(
scenePos=MagicMock(return_value=QtCore.QPoint(10, 20)))
self.scene.move_active = True
self.scene.undo_stack = MagicMock(push=MagicMock())
self.scene.mouseReleaseEvent(event)
self.scene.undo_stack.push.assert_not_called()
mouse_mock.assert_called_once_with(event)
assert self.scene.move_active is False
def test_items_for_save(self):
item1 = BeePixmapItem(QtGui.QImage())
self.scene.addItem(item1)
item2 = BeePixmapItem(QtGui.QImage())
self.scene.addItem(item2)
item3 = QtWidgets.QGraphicsRectItem()
self.scene.addItem(item3)
items = list(self.scene.items_for_save())
assert items == [item1, item2]
def test_clear_save_ids(self):
item1 = BeePixmapItem(QtGui.QImage())
item1.save_id = 5
@ -70,13 +215,100 @@ class BeeGraphicsSceneNormalizeTestCase(BeeTestCase):
assert item2.save_id is None
assert hasattr(item3, 'save_id') is False
def test_items_for_save(self):
def test_on_view_scale_change(self):
item = BeePixmapItem(QtGui.QImage())
self.scene.addItem(item)
item.setSelected(True)
item.on_view_scale_change = MagicMock()
self.scene.on_view_scale_change()
item.on_view_scale_change.assert_called_once()
def test_get_selection_rect_two_items(self):
item1 = BeePixmapItem(QtGui.QImage())
self.scene.addItem(item1)
item1.setSelected(True)
item1.setPos(4, -6)
item2 = BeePixmapItem(QtGui.QImage())
self.scene.addItem(item2)
item3 = QtWidgets.QGraphicsRectItem()
item2.setSelected(True)
item2.setPos(-33, 22)
item3 = BeePixmapItem(QtGui.QImage())
self.scene.addItem(item3)
item3.setSelected(False)
item3.setPos(1000, 1000)
items = list(self.scene.items_for_save())
assert items == [item1, item2]
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 = self.scene.get_selection_rect()
assert rect.topLeft().x() == -33
assert rect.topLeft().y() == -6
assert rect.bottomRight().x() == 104
assert rect.bottomRight().y() == 122
def test_get_selection_rect_rotated_item(self):
item = BeePixmapItem(QtGui.QImage())
self.scene.addItem(item)
item.setSelected(True)
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 = self.scene.get_selection_rect()
assert rect.topLeft().x() == 0
assert rect.topLeft().y() == approx(-math.sqrt(2) * 50)
assert rect.bottomRight().x() == approx(math.sqrt(2) * 100)
assert rect.bottomRight().y() == approx(math.sqrt(2) * 50)
def test_on_selection_change_when_multi_selection_new(self):
self.scene.has_multi_selection = MagicMock(return_value=True)
self.scene.multi_select_item.fit_selection_area = MagicMock()
self.scene.multi_select_item.bring_to_front = MagicMock()
self.scene.get_selection_rect = MagicMock(
return_value=QtCore.QRectF(0, 0, 100, 80))
self.scene.addItem = MagicMock()
self.scene.on_selection_change()
m_item = self.scene.multi_select_item
m_item.fit_selection_area.assert_called_once_with(
QtCore.QRectF(0, 0, 100, 80))
m_item.bring_to_front.assert_called_once()
self.scene.addItem.assert_called_once_with(m_item)
def test_on_selection_change_when_multi_selection_existing(self):
self.scene.addItem(self.scene.multi_select_item)
self.scene.has_multi_selection = MagicMock(return_value=True)
self.scene.multi_select_item.fit_selection_area = MagicMock()
self.scene.multi_select_item.bring_to_front = MagicMock()
self.scene.get_selection_rect = MagicMock(
return_value=QtCore.QRectF(0, 0, 100, 80))
self.scene.addItem = MagicMock()
self.scene.on_selection_change()
m_item = self.scene.multi_select_item
m_item.fit_selection_area.assert_called_once_with(
QtCore.QRectF(0, 0, 100, 80))
m_item.bring_to_front.assert_not_called()
self.scene.addItem.assert_not_called()
def test_on_selection_change_when_multi_selection_ended(self):
self.scene.addItem(self.scene.multi_select_item)
self.scene.has_multi_selection = MagicMock(return_value=False)
self.scene.multi_select_item.fit_selection_area = MagicMock()
self.scene.multi_select_item.bring_to_front = MagicMock()
self.scene.get_selection_rect = MagicMock(
return_value=QtCore.QRectF(0, 0, 100, 80))
self.scene.removeItem = MagicMock()
self.scene.on_selection_change()
m_item = self.scene.multi_select_item
m_item.fit_selection_area.assert_not_called()
self.scene.removeItem.assert_called_once_with(m_item)

553
tests/test_selection.py Normal file
View file

@ -0,0 +1,553 @@
from unittest.mock import patch, MagicMock, PropertyMock
from PyQt6 import QtCore, QtGui
from PyQt6.QtCore import Qt
from beeref.items import BeePixmapItem
from beeref.scene import BeeGraphicsScene
from beeref.selection import MultiSelectItem
from .base import BeeTestCase
class SelectableMixinBaseTestCase(BeeTestCase):
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)
class SelectableMixinTestCase(SelectableMixinBaseTestCase):
def test_set_scale(self):
item = BeePixmapItem(
QtGui.QImage(self.imgfilename3x3), self.imgfilename3x3)
item.prepareGeometryChange = MagicMock()
item.setScale(3)
assert item.scale() == 3
assert item.pos().x() == 0
assert item.pos().y() == 0
item.prepareGeometryChange.assert_called_once()
def test_set_scale_ignores_zero(self):
item = BeePixmapItem(QtGui.QImage())
item.setScale(0)
assert item.scale() == 1
def test_set_scale_ignores_negative(self):
item = BeePixmapItem(QtGui.QImage())
item.setScale(-0.1)
assert item.scale() == 1
def test_set_zvalue_sets_new_max(self):
item = BeePixmapItem(QtGui.QImage())
self.scene.addItem(item)
item.setZValue(1.1)
assert item.zValue() == 1.1
assert self.scene.max_z == 1.1
def test_set_zvalue_keeps_old_max(self):
item = BeePixmapItem(QtGui.QImage())
self.scene.addItem(item)
self.scene.max_z = 3.3
item.setZValue(1.1)
assert item.zValue() == 1.1
assert self.scene.max_z == 3.3
def test_bring_to_front(self):
item1 = BeePixmapItem(QtGui.QImage())
self.scene.addItem(item1)
item1.setZValue(3.3)
item2 = BeePixmapItem(QtGui.QImage())
self.scene.addItem(item2)
item2.bring_to_front()
assert item2.zValue() > item1.zValue()
assert item2.zValue() == self.scene.max_z
def test_on_view_scale_change(self):
item = BeePixmapItem(QtGui.QImage())
with patch('beeref.items.BeePixmapItem.prepareGeometryChange') as m:
item.on_view_scale_change()
m.assert_called_once()
def test_fixed_length_for_viewport_when_default_scales(self):
self.view.get_scale = MagicMock(return_value=1)
assert self.item.fixed_length_for_viewport(100) == 100
assert self.item._view_scale == 1
def test_fixed_length_for_viewport_when_viewport_scaled(self):
self.view.get_scale = MagicMock(return_value=2)
assert self.item.fixed_length_for_viewport(100) == 50
assert self.item._view_scale == 2
def test_fixed_length_for_viewport_when_item_scaled(self):
self.view.get_scale = MagicMock(return_value=1)
self.item.setScale(5)
assert self.item.fixed_length_for_viewport(100) == 20
assert self.item._view_scale == 1
def test_fixed_length_for_viewport_when_no_scene(self):
item = BeePixmapItem(QtGui.QImage())
item._view_scale = 0.5
assert item.fixed_length_for_viewport(100) == 200
def test_resize_size_when_scaled(self):
self.view.get_scale = MagicMock(return_value=2)
self.item.setScale(2)
self.item.SELECT_RESIZE_SIZE = 100
assert self.item.select_resize_size == 25
def test_rotate_size_when_scaled(self):
self.view.get_scale = MagicMock(return_value=2)
self.item.setScale(2)
self.item.SELECT_ROTATE_SIZE = 100
assert self.item.select_rotate_size == 25
@patch('beeref.items.BeePixmapItem.draw_debug_shape')
def test_paint_when_not_selected(self, debug_mock):
painter = MagicMock()
self.item.setSelected(False)
self.item.pixmap = MagicMock(return_value='pixmap')
self.item.paint(painter, None, None)
painter.drawPixmap.assert_called_once_with(0, 0, 'pixmap')
painter.drawRect.assert_not_called()
painter.drawPoint.assert_not_called()
debug_mock.assert_not_called()
def test_paint_when_selected_single_selection(self):
painter = MagicMock()
self.item.setSelected(True)
self.item.paint(painter, None, None)
painter.drawPixmap.assert_called_once()
painter.drawRect.assert_called_once()
assert painter.drawPoint.call_count == 4
def test_paint_when_selected_multi_selection(self):
item2 = BeePixmapItem(QtGui.QImage())
item2.setSelected(True)
self.scene.addItem(item2)
painter = MagicMock()
self.item.setSelected(True)
self.item.paint(painter, None, None)
painter.drawPixmap.assert_called_once()
painter.drawRect.assert_called_once()
painter.drawPoint.assert_not_called()
def test_paint_when_draw_debug_shapes(self):
with patch('beeref.selection.commandline_args') as args_mock:
with patch('beeref.items.BeePixmapItem.draw_debug_shape') as m:
args_mock.draw_debug_shapes = True
item = BeePixmapItem(QtGui.QImage())
item.paint(MagicMock(), None, None)
m.assert_called()
def test_corners(self):
assert len(self.item.corners) == 4
assert QtCore.QPointF(0, 0) in self.item.corners
assert QtCore.QPointF(100, 0) in self.item.corners
assert QtCore.QPointF(0, 80) in self.item.corners
assert QtCore.QPointF(100, 80) in self.item.corners
def test_corners_scene_coords_translated(self):
self.item.setPos(5, 5)
corners = self.item.corners_scene_coords
assert len(self.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
def test_corners_scene_coords_scaled(self):
self.item.setScale(2)
corners = self.item.corners_scene_coords
assert len(self.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
def test_get_scale_bounds(self):
self.view.get_scale = MagicMock(return_value=1)
self.item.SELECT_RESIZE_SIZE = 10
rect = self.item.get_scale_bounds(QtCore.QPointF(100, 100))
assert rect.topLeft().x() == 95
assert rect.topLeft().y() == 95
assert rect.bottomRight().x() == 105
assert rect.bottomRight().y() == 105
def test_bottom_right_rotate_bounds(self):
self.view.get_scale = MagicMock(return_value=1)
self.item.SELECT_RESIZE_SIZE = 10
self.item.SELECT_ROTATE_SIZE = 10
rect = self.item.bottom_right_rotate_bounds
assert rect.topLeft().x() == 105
assert rect.topLeft().y() == 85
assert rect.bottomRight().x() == 115
assert rect.bottomRight().y() == 95
def test_bounding_rect_when_not_selected(self):
self.view.get_scale = MagicMock(return_value=1)
self.item.setSelected(False)
with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.boundingRect',
return_value=QtCore.QRectF(0, 0, 100, 80)):
rect = self.item.boundingRect()
assert rect.topLeft().x() == 0
assert rect.topLeft().y() == 0
assert rect.bottomRight().x() == 100
assert rect.bottomRight().y() == 80
def test_bounding_rect_when_selected(self):
self.item.SELECT_RESIZE_SIZE = 10
self.item.SELECT_ROTATE_SIZE = 10
self.view.get_scale = MagicMock(return_value=1)
self.item.setSelected(True)
with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.boundingRect',
return_value=QtCore.QRectF(0, 0, 100, 80)):
rect = self.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(self):
self.view.get_scale = MagicMock(return_value=1)
self.item.setSelected(False)
path = QtGui.QPainterPath()
path.addRect(QtCore.QRectF(0, 0, 100, 80))
with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.shape',
return_value=path):
shape = self.item.shape().boundingRect()
assert shape.topLeft().x() == 0
assert shape.topLeft().y() == 0
assert shape.bottomRight().x() == 100
assert shape.bottomRight().y() == 80
def test_shape_when_selected(self):
self.item.SELECT_RESIZE_SIZE = 10
self.item.SELECT_ROTATE_SIZE = 10
self.view.get_scale = MagicMock(return_value=1)
self.item.setSelected(True)
path = QtGui.QPainterPath()
path.addRect(QtCore.QRectF(0, 0, 100, 80))
with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.shape',
return_value=path):
shape = self.item.shape().boundingRect()
assert shape.topLeft().x() == -5
assert shape.topLeft().y() == -5
assert shape.bottomRight().x() == 115
assert shape.bottomRight().y() == 95
class SelectableMixinScalingTestCase(SelectableMixinBaseTestCase):
def test_get_scale_factor_bottomright(self):
self.item.scale_start = QtCore.QPoint(10, 10)
self.item.scale_direction = (1, 1)
self.item.scale_orig_factor = 1
event = MagicMock()
event.scenePos = MagicMock(return_value=QtCore.QPoint(20, 90))
assert self.item.get_scale_factor(event) == 1.5
def test_get_scale_factor_topleft(self):
self.item.scale_start = QtCore.QPoint(10, 10)
self.item.scale_direction = (-1, -1)
self.item.scale_orig_factor = 0.5
event = MagicMock()
event.scenePos = MagicMock(return_value=QtCore.QPoint(-10, -60))
assert self.item.get_scale_factor(event) == 2
def test_get_scale_anchor_topleft(self):
anchor = self.item.get_scale_anchor(self.item, QtCore.QPointF(0, 0))
assert anchor.x() == 100
assert anchor.y() == 80
def test_get_scale_anchor_bottomright(self):
anchor = self.item.get_scale_anchor(self.item, QtCore.QPointF(100, 80))
assert anchor.x() == 0
assert anchor.y() == 0
def test_get_scale_anchor_topright(self):
anchor = self.item.get_scale_anchor(self.item, QtCore.QPointF(100, 0))
assert anchor.x() == 0
assert anchor.y() == 80
def test_get_scale_anchor_bottomleft(self):
anchor = self.item.get_scale_anchor(self.item, QtCore.QPointF(0, 80))
assert anchor.x() == 100
assert anchor.y() == 0
def test_get_scale_anchor_topleft_when_anchor_other_item(self):
item2 = BeePixmapItem(QtGui.QImage())
item2.setPos(22, 33)
anchor = self.item.get_scale_anchor(item2, QtCore.QPointF(0, 0))
assert anchor.x() == 78
assert anchor.y() == 47
def test_get_scale_direction_topleft(self):
assert self.item.get_scale_direction(QtCore.QPointF(0, 0)) == (-1, -1)
def test_get_scale_direction_bottomright(self):
assert self.item.get_scale_direction(QtCore.QPointF(100, 80)) == (1, 1)
def test_get_scale_direction_topright(self):
assert self.item.get_scale_direction(QtCore.QPointF(100, 0)) == (1, -1)
def test_get_scale_direction_bottomleft(self):
assert self.item.get_scale_direction(QtCore.QPointF(0, 80)) == (-1, 1)
def test_translate_for_scale_anchor(self):
self.item.scale_orig_pos = QtCore.QPoint(50, 70)
self.item.scale_anchor = QtCore.QPoint(100, 80)
self.item.scale_orig_factor = 1
self.item.translate_for_scale_anchor(3)
assert self.item.pos().x() == -150
assert self.item.pos().y() == -90
class SelectableMixinMouseEventsTestCase(SelectableMixinBaseTestCase):
def setUp(self):
super().setUp()
self.event = MagicMock()
self.item.setCursor = MagicMock()
def test_hover_move_event_no_selection(self):
self.event.pos = MagicMock(return_value=QtCore.QPointF(0, 0))
self.item.hoverMoveEvent(self.event)
self.item.setCursor.assert_not_called()
def test_hover_move_event_topleft_scale(self):
self.item.setSelected(True)
self.event.pos = MagicMock(return_value=QtCore.QPointF(0, 0))
self.item.hoverMoveEvent(self.event)
self.item.setCursor.assert_called_once_with(
Qt.CursorShape.SizeFDiagCursor)
def test_hover_move_event_bottomright_scale(self):
self.item.setSelected(True)
self.event.pos = MagicMock(return_value=QtCore.QPointF(100, 80))
self.item.hoverMoveEvent(self.event)
self.item.setCursor.assert_called_once_with(
Qt.CursorShape.SizeFDiagCursor)
def test_hover_move_event_topright_scale(self):
self.item.setSelected(True)
self.event.pos = MagicMock(return_value=QtCore.QPointF(100, 0))
self.item.hoverMoveEvent(self.event)
self.item.setCursor.assert_called_once_with(
Qt.CursorShape.SizeBDiagCursor)
def test_hover_move_event_not_in_handles(self):
self.item.setSelected(True)
self.event.pos = MagicMock(return_value=QtCore.QPointF(50, 50))
self.item.hoverMoveEvent(self.event)
self.item.setCursor.assert_called_once_with(
Qt.CursorShape.ArrowCursor)
def test_hover_enter_event_when_selected(self):
self.item.setSelected(True)
self.item.hoverEnterEvent(self.event)
self.item.setCursor.assert_not_called()
def test_hover_enter_event_when_not_selected(self):
self.item.setSelected(False)
self.item.hoverEnterEvent(self.event)
self.item.setCursor.assert_called_once_with(
Qt.CursorShape.ArrowCursor)
def test_mouse_press_event_topleft_scale(self):
self.item.setSelected(True)
self.event.pos = MagicMock(return_value=QtCore.QPointF(2, 2))
self.event.scenePos = MagicMock(return_value=QtCore.QPointF(66, 99))
self.event.button = MagicMock(
return_value=Qt.MouseButtons.LeftButton)
self.item.mousePressEvent(self.event)
assert self.item.scale_active is True
assert self.item.scale_start == QtCore.QPointF(66, 99)
assert self.item.scale_direction == (-1, -1)
assert self.item.scale_orig_factor == 1
assert self.item.scale_orig_pos == QtCore.QPointF(0, 0)
def test_mouse_press_event_bottomright_scale(self):
self.item.setSelected(True)
self.event.pos = MagicMock(return_value=QtCore.QPointF(99, 79))
self.event.scenePos = MagicMock(return_value=QtCore.QPointF(66, 99))
self.event.button = MagicMock(
return_value=Qt.MouseButtons.LeftButton)
self.item.mousePressEvent(self.event)
assert self.item.scale_active is True
assert self.item.scale_direction == (1, 1)
assert self.item.scale_start == QtCore.QPointF(66, 99)
assert self.item.scale_orig_factor == 1
assert self.item.scale_orig_pos == QtCore.QPointF(0, 0)
def test_mouse_press_event_not_selected(self):
self.item.setSelected(False)
with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.mousePressEvent') as m:
self.item.mousePressEvent(self.event)
m.assert_called_once_with(self.event)
assert self.item.scale_active is False
def test_mouse_press_not_in_handles(self):
self.item.setSelected(True)
self.event.pos = MagicMock(return_value=QtCore.QPointF(50, 40))
self.event.button = MagicMock(
return_value=Qt.MouseButtons.LeftButton)
with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.mousePressEvent') as m:
self.item.mousePressEvent(self.event)
m.assert_called_once_with(self.event)
assert self.item.scale_active is False
def test_mouse_move_event_when_no_action(self):
with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.mouseMoveEvent') as m:
self.item.mouseMoveEvent(self.event)
m.assert_called_once_with(self.event)
def test_move_event_when_scale_action(self):
self.event.scenePos = MagicMock(return_value=QtCore.QPointF(20, 90))
self.item.scale_active = True
self.item.scale_direction = (1, 1)
self.item.scale_anchor = QtCore.QPointF(100, 80)
self.item.scale_start = QtCore.QPointF(10, 10)
self.item.scale_orig_factor = 1
self.item.scale_orig_pos = QtCore.QPointF(0, 0)
self.item.mouseMoveEvent(self.event)
assert self.item.scale() == 1.5
def test_mouse_release_event_when_no_action(self):
with patch('PyQt6.QtWidgets.QGraphicsPixmapItem'
'.mouseReleaseEvent') as m:
self.item.mouseReleaseEvent(self.event)
m.assert_called_once_with(self.event)
def test_mouse_release_event_when_scale_action(self):
self.event.scenePos = MagicMock(return_value=QtCore.QPointF(20, 90))
self.item.scale_active = True
self.item.scale_direction = (1, 1)
self.item.scale_anchor = QtCore.QPointF(100, 80)
self.item.scale_start = QtCore.QPointF(10, 10)
self.item.scale_orig_factor = 1
self.item.scale_orig_pos = QtCore.QPointF(0, 0)
self.scene.undo_stack = MagicMock(push=MagicMock())
self.item.mouseReleaseEvent(self.event)
self.scene.undo_stack.push.assert_called_once()
args = self.scene.undo_stack.push.call_args_list[0][0]
cmd = args[0]
assert cmd.items == [self.item]
assert cmd.factor == 1.5
assert cmd.item_data == [{
'anchor': QtCore.QPointF(100, 80),
'orig_factor': 1,
'orig_pos': QtCore.QPointF(0, 0)}]
assert cmd.ignore_first_redo is True
assert self.item.scale_active is False
class MultiSelectItemItemTestCase(BeeTestCase):
def setUp(self):
self.scene = BeeGraphicsScene(None)
@patch('beeref.selection.SelectableMixin.init_selectable')
def test_init(self, selectable_mock):
item = MultiSelectItem()
assert hasattr(item, 'save_id') is False
selectable_mock.assert_called_once()
def test_width(self):
item = MultiSelectItem()
item.setRect(0, 0, 50, 100)
assert item.width == 50
def test_height(self):
item = MultiSelectItem()
item.setRect(0, 0, 50, 100)
assert item.height == 100
def test_paint(self):
item = MultiSelectItem()
item.paint_selectable = MagicMock()
painter = MagicMock()
item.paint(painter, None, None)
item.paint_selectable.assert_called_once()
painter.drawRect.assert_not_called()
def test_has_selection_outline(self):
item = MultiSelectItem()
item.has_selection_outline() is True
def test_has_selection_handles(self):
item = MultiSelectItem()
item.has_selection_handles() is True
def test_selection_action_items(self):
view = MagicMock(get_scale=MagicMock(return_value=1))
with patch('beeref.scene.BeeGraphicsScene.views',
return_value=[view]):
img1 = BeePixmapItem(QtGui.QImage())
self.scene.addItem(img1)
img1.setSelected(True)
img2 = BeePixmapItem(QtGui.QImage())
self.scene.addItem(img2)
img2.setSelected(False)
item = MultiSelectItem()
self.scene.addItem(item)
item.setSelected(True)
action_items = item.selection_action_items()
assert img1 in action_items
assert item in action_items
assert img2 not in action_items
def test_fit_selection_area(self):
item = MultiSelectItem()
item.setScale(5)
item.setRotation(-20)
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.scale() == 1
assert item.rotation() == 0
assert item.isSelected() is True
@patch('PyQt6.QtWidgets.QGraphicsRectItem.mousePressEvent')
def test_mouse_press_event_when_ctrl_leftclick(self, mouse_mock):
item = MultiSelectItem()
item.fit_selection_area(QtCore.QRectF(0, 0, 100, 80))
event = MagicMock(
button=MagicMock(return_value=Qt.MouseButtons.LeftButton),
modifiers=MagicMock(
return_value=Qt.KeyboardModifiers.ControlModifier))
item.mousePressEvent(event)
event.ignore.assert_called_once()
mouse_mock.assert_not_called()
@patch('beeref.selection.SelectableMixin.mousePressEvent')
def test_mouse_press_event_when_leftclick(self, mouse_mock):
item = MultiSelectItem()
item.fit_selection_area(QtCore.QRectF(0, 0, 100, 80))
event = MagicMock(
button=MagicMock(return_value=Qt.MouseButtons.LeftButton))
item.mousePressEvent(event)
event.ignore.assert_not_called()
mouse_mock.assert_called_once_with(event)