This commit is contained in:
Rebecca Breu 2021-04-10 19:35:47 +02:00
parent fe9de9bcdf
commit 4e31ec8bac
18 changed files with 785 additions and 101 deletions

View file

@ -38,7 +38,12 @@ class BeeAssets:
logger.debug(f'Assets path: {self.PATH}')
self.logo = QtGui.QIcon(os.path.join(self.PATH, 'logo.png'))
self.cursor_rotate = self.cursor_from_image('cursor_rotate', (6, 10))
self.cursor_rotate = self.cursor_from_image(
'cursor_rotate.png', (20, 20))
self.cursor_flip_h = self.cursor_from_image(
'cursor_flip_h.png', (20, 20))
self.cursor_flip_v = self.cursor_from_image(
'cursor_flip_v.png', (20, 20))
def cursor_from_image(self, filename, hotspot):
img = QtGui.QImage(os.path.join(self.PATH, filename))

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -0,0 +1,140 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="39.130039mm"
height="39.130039mm"
viewBox="0 0 39.13004 39.130039"
version="1.1"
id="svg8"
sodipodi:docname="cursor_flip_h.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
inkscape:export-filename="/home/rbreu/code/python/beeref/git/beeref/assets/cursor_flip_h.png"
inkscape:export-xdpi="25.959999"
inkscape:export-ydpi="25.959999">
<defs
id="defs2">
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter908"
x="-0.019904368"
width="1.0398088"
y="-0.030217774"
height="1.0604355">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="0.2951548"
id="feGaussianBlur910" />
</filter>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#8affc7"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="4.1593846"
inkscape:cx="18.681535"
inkscape:cy="50.821397"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="2560"
inkscape:window-height="1311"
inkscape:window-x="0"
inkscape:window-y="55"
inkscape:window-maximized="1"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="-0.3"
showguides="true"
inkscape:guide-bbox="true">
<sodipodi:guide
position="13.291247,31.177997"
orientation="1,0"
id="guide846"
inkscape:locked="false" />
<sodipodi:guide
position="-5.3488289,24.631702"
orientation="0,1"
id="guide848"
inkscape:locked="false" />
<sodipodi:guide
position="6.615822,31.243746"
orientation="0,1"
id="guide850"
inkscape:locked="false" />
<sodipodi:guide
position="17.680875,32.458203"
orientation="1,0"
id="guide852"
inkscape:locked="false" />
<sodipodi:guide
position="20.919427,27.105596"
orientation="1,0"
id="guide854"
inkscape:locked="false" />
<sodipodi:guide
position="25.199192,33.420932"
orientation="1,0"
id="guide856"
inkscape:locked="false" />
<sodipodi:guide
position="-5.2138892,19.548974"
orientation="0,1"
id="guide858"
inkscape:locked="false" />
<sodipodi:guide
position="-9.0236177,14.703346"
orientation="0,1"
id="guide860"
inkscape:locked="false" />
<sodipodi:guide
position="-6.6063933,7.8015327"
orientation="0,1"
id="guide862"
inkscape:locked="false" />
</sodipodi:namedview>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-52.761406,-65.201222)">
<path
style="display:inline;opacity:0.28000004;fill:#060000;fill-opacity:1;stroke:#000000;stroke-width:2.3101306;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter908)"
d="m 66.738216,81.605241 0.01691,-6.649632 -11.777794,11.629023 11.777794,11.747442 v -6.901812 h 4.389628 v 6.901812 h 3.238552 v -6.901812 h 4.279765 v 6.901812 L 90.566079,86.584632 78.663072,74.88986 v 6.612044 h -4.279765 l -0.03955,-6.612044 h -3.199005 v 6.612044 l -4.406533,0.103337 0.0169,-0.103337 z"
id="path895-6"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccccccccccccccc"
inkscape:label="arrow" />
<path
style="display:inline;fill:#060000;fill-opacity:1;stroke:#f6f0f0;stroke-width:2.3101306;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 66.035745,79.802895 0.01691,-6.649632 -11.777793,11.629023 11.777793,11.747442 v -6.901812 h 4.389628 v 6.901812 h 3.238552 V 89.627916 H 77.9606 v 6.901812 L 89.863611,84.782286 77.9606,73.087514 v 6.612044 h -4.279765 l -0.03955,-6.612044 h -3.199006 v 6.612044 l -4.406532,0.103337 0.0169,-0.103337 z"
id="path895"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccccccccccccccc"
inkscape:label="arrow" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -0,0 +1,141 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="39.130039mm"
height="39.130039mm"
viewBox="0 0 39.13004 39.130039"
version="1.1"
id="svg8"
sodipodi:docname="cursor_flip_v.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
inkscape:export-filename="/home/rbreu/code/python/beeref/git/beeref/assets/cursor_flip_v.png"
inkscape:export-xdpi="25.959999"
inkscape:export-ydpi="25.959999">
<defs
id="defs2">
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter908"
x="-0.019904368"
width="1.0398088"
y="-0.030217774"
height="1.0604355">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="0.2951548"
id="feGaussianBlur910" />
</filter>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#8affc7"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="4.1593846"
inkscape:cx="-19.846889"
inkscape:cy="77.277503"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="2560"
inkscape:window-height="1311"
inkscape:window-x="0"
inkscape:window-y="55"
inkscape:window-maximized="1"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
showguides="true"
inkscape:guide-bbox="true">
<sodipodi:guide
position="13.291248,31.177997"
orientation="1,0"
id="guide846"
inkscape:locked="false" />
<sodipodi:guide
position="-5.3488284,24.631702"
orientation="0,1"
id="guide848"
inkscape:locked="false" />
<sodipodi:guide
position="6.6158224,31.243746"
orientation="0,1"
id="guide850"
inkscape:locked="false" />
<sodipodi:guide
position="17.680876,32.458203"
orientation="1,0"
id="guide852"
inkscape:locked="false" />
<sodipodi:guide
position="20.919428,27.105596"
orientation="1,0"
id="guide854"
inkscape:locked="false" />
<sodipodi:guide
position="25.199193,33.420932"
orientation="1,0"
id="guide856"
inkscape:locked="false" />
<sodipodi:guide
position="-5.2138887,19.548974"
orientation="0,1"
id="guide858"
inkscape:locked="false" />
<sodipodi:guide
position="-9.0236173,14.703345"
orientation="0,1"
id="guide860"
inkscape:locked="false" />
<sodipodi:guide
position="-6.6063928,7.8015324"
orientation="0,1"
id="guide862"
inkscape:locked="false" />
</sodipodi:namedview>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-52.761406,-65.201222)">
<path
style="display:inline;opacity:0.28000004;fill:#060000;fill-opacity:1;stroke:#000000;stroke-width:2.3101306;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter908)"
d="m 66.738216,81.605241 0.01691,-6.649632 -11.777794,11.629023 11.777794,11.747442 v -6.901812 h 4.389628 v 6.901812 h 3.238552 v -6.901812 h 4.279765 v 6.901812 L 90.566079,86.584632 78.663072,74.88986 v 6.612044 h -4.279765 l -0.03955,-6.612044 h -3.199005 v 6.612044 l -4.406533,0.103337 0.0169,-0.103337 z"
id="path895-6"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccccccccccccccc"
inkscape:label="arrow"
transform="rotate(90,72.930734,86.006662)" />
<path
style="display:inline;fill:#060000;fill-opacity:1;stroke:#f6f0f0;stroke-width:2.3101306;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 77.050746,78.340488 6.648751,0.109583 -11.46374,-11.938731 -11.910454,11.612919 6.901141,0.09619 -0.06118,4.389201 -6.901142,-0.09619 -0.04514,3.238238 6.901141,0.09619 -0.05965,4.279349 -6.901142,-0.09619 11.580401,12.065583 11.859536,-11.738854 -6.611402,-0.09216 0.05965,-4.279349 6.611953,0.0526 0.04459,-3.198695 -6.611402,-0.09216 -0.04191,-4.407545 0.103091,0.01834 z"
id="path895"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccccccccccccccc"
inkscape:label="arrow" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 5 KiB

View file

@ -14,11 +14,11 @@
viewBox="0 0 43.466164 43.466164"
version="1.1"
id="svg8"
sodipodi:docname="cursor_rotate_2.svg"
sodipodi:docname="cursor_rotate.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
inkscape:export-filename="/home/rbreu/code/python/beeref/git/beeref/assets/cursor_rotate_2.png"
inkscape:export-xdpi="19.870001"
inkscape:export-ydpi="19.870001">
inkscape:export-xdpi="23.370001"
inkscape:export-ydpi="23.370001">
<defs
id="defs2">
<filter

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -101,7 +101,7 @@ class RotateItemsBy(QtGui.QUndoCommand):
"""Rotate items by a given delta around the given anchor."""
def __init__(self, items, delta, anchor, ignore_first_redo=False):
super().__init__('Scale items')
super().__init__('Rotate items')
self.ignore_first_redo = ignore_first_redo
self.items = items
self.delta = delta
@ -112,12 +112,13 @@ class RotateItemsBy(QtGui.QUndoCommand):
self.ignore_first_redo = False
return
for item in self.items:
item.setRotation(item.rotation() + self.delta,
item.mapFromScene(self.anchor))
item.setRotation(
item.rotation() + self.delta * item.flip(),
item.mapFromScene(self.anchor))
def undo(self):
for item in self.items:
item.setRotation(item.rotation() - self.delta,
item.setRotation(item.rotation() - self.delta * item.flip(),
item.mapFromScene(self.anchor))
@ -139,3 +140,19 @@ class NormalizeItems(QtGui.QUndoCommand):
for item, factor in zip(self.items, self.old_scale_factors):
item.setScale(factor,
QtCore.QPointF(item.width, item.height) / 2)
class FlipItems(QtGui.QUndoCommand):
def __init__(self, items, anchor, vertical):
super().__init__('Flip items')
self.items = items
self.anchor = anchor
self.vertical = vertical
def redo(self):
for item in self.items:
item.do_flip(self.vertical, item.mapFromScene(self.anchor))
def undo(self):
self.redo()

View file

@ -29,10 +29,20 @@ parser.add_argument(
choices=list(logging._nameToLevel.keys()),
help='log level for console output')
parser.add_argument(
'--draw-debug-shapes',
'--debug-boundingrects',
default=False,
action='store_true',
help='draw debug shapes for bounding rects and interactable areas')
help='draw item\'s bounding rects for debugging')
parser.add_argument(
'--debug-shapes',
default=False,
action='store_true',
help='draw item\'s mouse event shapes for debugging')
parser.add_argument(
'--debug-handles',
default=False,
action='store_true',
help='draw item\'s transform handle areas for debugging')
class CommandlineArgs:

View file

@ -34,4 +34,4 @@ def save(filename, scene, create_new=False, progress=None):
logger.debug(f'Create new: {create_new}')
io = SQLiteIO(filename, scene, create_new, progress=progress)
io.write()
logger.debug('Saved!')
logger.info('Saved!')

View file

@ -8,8 +8,7 @@ SCHEMA = [
z REAL DEFAULT 0,
scale REAL DEFAULT 1,
rotation REAL DEFAULT 0,
flip_h INTEGER DEFAULT 0,
flip_v INTEGER DEFAULT 0,
flip INTEGER DEFAULT 1,
filename TEXT
)
""",

View file

@ -127,21 +127,23 @@ class SQLiteIO:
@handle_sqlite_errors
def read(self):
rows = self.fetchall(
'SELECT items.id, x, y, z, scale, rotation, filename, sqlar.data '
'FROM items '
'INNER JOIN sqlar on sqlar.item_id = items.id')
'SELECT items.id, x, y, z, scale, rotation, flip, filename, '
'sqlar.data '
'FROM items INNER JOIN sqlar on sqlar.item_id = items.id')
if self.progress:
self.progress.setMaximum(len(rows))
for i, row in enumerate(rows):
item = BeePixmapItem(QtGui.QImage(), filename=row[6])
item = BeePixmapItem(QtGui.QImage(), filename=row[7])
item.save_id = row[0]
item.pixmap_from_bytes(row[7])
item.pixmap_from_bytes(row[8])
item.setPos(row[1], row[2])
self.scene.addItem(item)
item.setZValue(row[3])
item.setScale(row[4])
item.setRotation(row[5])
if row[6] == -1:
item.do_flip()
if self.progress:
self.progress.setValue(i)
if self.progress.wasCanceled():
@ -188,10 +190,11 @@ class SQLiteIO:
def insert_item(self, item):
self.ex(
'INSERT INTO items (type, x, y, z, scale, rotation, filename) '
'VALUES (?, ?, ?, ?, ?, ?, ?) ',
'INSERT INTO items (type, x, y, z, scale, rotation, flip, '
'filename) '
'VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
('pixmap', item.pos().x(), item.pos().y(), item.zValue(),
item.scale(), item.rotation(), item.filename))
item.scale(), item.rotation(), item.flip(), item.filename))
item.save_id = self.cursor.lastrowid
pixmap = item.pixmap_to_bytes()
@ -214,8 +217,9 @@ class SQLiteIO:
data never changes and is also time-consuming to save.
"""
self.ex(
'UPDATE items SET x=?, y=?, z=?, scale=?, rotation=?, filename=? '
'UPDATE items SET x=?, y=?, z=?, scale=?, rotation=?, flip=?, '
'filename=? '
'WHERE id=?',
(item.pos().x(), item.pos().y(), item.zValue(), item.scale(),
item.rotation(), item.filename, item.save_id))
item.rotation(), item.flip(), item.filename, item.save_id))
self.connection.commit()

View file

@ -39,13 +39,19 @@ def with_anchor(func):
The anchor is given in item coordinates.
"""
def wrapper(self, value, anchor=None):
def wrapper(self, *args, **kwargs):
# We caculate where the anchor is before and after the transformation
# and then move the item accordingly to keep the anchor fixed
anchor = anchor if anchor else QtCore.QPointF(0, 0)
anchor = kwargs.pop('anchor', None)
if not anchor:
if args and isinstance(args[-1], QtCore.QPointF):
anchor = args[-1]
args = args[:-1]
anchor = anchor or QtCore.QPointF(0, 0)
prev = self.mapToScene(anchor)
func(self, value)
func(self, *args, **kwargs)
diff = self.mapToScene(anchor) - prev
self.setPos(self.pos() - diff)
@ -74,7 +80,20 @@ class BaseItemMixin:
@with_anchor
def setRotation(self, value):
logger.debug(f'Setting rotation for {self} to {value}')
super().setRotation(value)
super().setRotation(value % 360)
def flip(self):
"""Returns the flip value (1 or -1)"""
# We use the transformation matrix only for flipping, so checking
# the x scale is enough
return self.transform().m11()
@with_anchor
def do_flip(self, vertical=False):
"""Flips the item."""
self.setTransform(QtGui.QTransform.fromScale(-self.flip(), 1))
if vertical:
self.setRotation(self.rotation() + 180)
@property
def center_scene_coords(self):
@ -98,8 +117,9 @@ class SelectableMixin(BaseItemMixin):
self.scale_active = False
self.rotate_active = False
self.flip_active = False
self.just_selected = False
self.viewport_scale = 1
self.conf_debug_shapes = commandline_args.draw_debug_shapes
def fixed_length_for_viewport(self, value):
"""The interactable areas need to stay the same size on the
@ -131,9 +151,18 @@ class SelectableMixin(BaseItemMixin):
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)
if commandline_args.debug_shapes:
self.draw_debug_shape(painter, self.shape(), 255, 0, 0)
if commandline_args.debug_boundingrects:
self.draw_debug_shape(painter, self.boundingRect(), 0, 255, 0)
if (commandline_args.debug_handles and self.has_selection_handles()):
for corner in self.corners:
self.draw_debug_shape(
painter, self.get_scale_bounds(corner), 0, 0, 255)
self.draw_debug_shape(
painter, self.get_rotate_bounds(corner), 0, 255, 255)
for edge in self.get_flip_bounds():
self.draw_debug_shape(painter, edge['rect'], 255, 255, 0)
if not self.has_selection_outline():
return
@ -158,9 +187,9 @@ class SelectableMixin(BaseItemMixin):
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))
QtCore.QPointF(self.width, self.height),
QtCore.QPointF(0, self.height))
@property
def corners_scene_coords(self):
@ -169,7 +198,8 @@ class SelectableMixin(BaseItemMixin):
return [self.mapToScene(corner) for corner in self.corners]
def get_scale_bounds(self, corner, margin=0):
"""The interactable shape of the scale handles."""
"""The interactable shape of the scale handles. The scale handles sit
centered around the visible handle."""
path = QtGui.QPainterPath()
path.addRect(QtCore.QRectF(
corner.x() - self.select_resize_size/2 - margin,
@ -202,26 +232,75 @@ class SelectableMixin(BaseItemMixin):
# https://bugreports.qt.io/browse/QTBUG-57567
return path - self.get_scale_bounds(corner, margin=0.001)
def boundingRect(self):
bounds = super().boundingRect()
if not self.has_selection_outline():
return bounds
def get_flip_bounds(self):
"""The interactactable shape of the flip handles.
# Add extra space for scale and rotate interactive areas
These stretch around the edge of the item filling the areas
between the scale and rotate handles, e.g. for the bottom right corner:
FFF
FFSR
FF
FFR R
"""
outer_margin = self.select_resize_size / 2 + self.select_rotate_size
inner_margin = self.select_resize_size / 2
return [
# top:
{
'rect': QtCore.QRectF(inner_margin,
-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),
'flip_v': True,
},
# left:
{
'rect': QtCore.QRectF(-outer_margin,
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),
'flip_v': False,
}
]
def boundingRect(self):
if not self.has_selection_outline():
return super().boundingRect()
# Add extra space for the 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)
-margin, -margin,
self.width + 2 * margin,
self.height + 2 * margin)
def shape(self):
path = super().shape()
path = QtGui.QPainterPath()
if self.has_selection_handles():
# Add extra space for scale and rotate interactive areas
for corner in self.corners:
path += self.get_scale_bounds(corner)
path += self.get_rotate_bounds(corner)
rect = self.boundingRect()
else:
rect = super().boundingRect()
path.addRect(rect)
return path
def hoverMoveEvent(self, event):
@ -245,6 +324,13 @@ class SelectableMixin(BaseItemMixin):
elif self.get_rotate_bounds(corner).contains(event.pos()):
self.setCursor(BeeAssets().cursor_rotate)
return
for edge in self.get_flip_bounds():
if edge['rect'].contains(event.pos()):
if self.get_edge_flips_v(edge):
self.setCursor(BeeAssets().cursor_flip_v)
else:
self.setCursor(BeeAssets().cursor_flip_h)
return
self.setCursor(Qt.CursorShape.ArrowCursor)
@ -254,6 +340,8 @@ class SelectableMixin(BaseItemMixin):
self.setCursor(Qt.CursorShape.ArrowCursor)
def mousePressEvent(self, event):
if not self.isSelected():
self.just_selected = True
if (event.button() == Qt.MouseButtons.LeftButton
and self.has_selection_handles()):
for corner in self.corners:
@ -281,6 +369,10 @@ class SelectableMixin(BaseItemMixin):
item.rotate_orig_degrees = item.rotation()
event.accept()
return
# Check if we are in one of the flip edges:
for edge in self.get_flip_bounds():
if edge['rect'].contains(event.pos()):
self.flip_active = True
super().mousePressEvent(event)
@ -327,6 +419,15 @@ class SelectableMixin(BaseItemMixin):
return delta
def get_edge_flips_v(self, edge):
"""Returns ``True`` if the given edge invokes a horizontal flip,
``False`` if it invokes a vertacal flip."""
if 45 < self.rotation() < 135 or 225 < self.rotation() < 315:
return not edge['flip_v']
else:
return edge['flip_v']
def mouseMoveEvent(self, event):
if self.scale_active:
factor = self.get_scale_factor(event)
@ -334,17 +435,26 @@ class SelectableMixin(BaseItemMixin):
item.setScale(item.scale_orig_factor * factor,
item.mapFromScene(self.event_anchor))
event.accept()
elif self.rotate_active:
return
if self.rotate_active:
snap = (event.modifiers() == Qt.KeyboardModifiers.ControlModifier
or event.modifiers() == Qt.KeyboardModifiers.ShiftModifier)
delta = self.get_rotate_delta(event.scenePos(), snap)
for item in self.selection_action_items():
item.setRotation(item.rotate_orig_degrees + delta,
item.mapFromScene(self.event_anchor))
else:
super().mouseMoveEvent(event)
item.setRotation(
item.rotate_orig_degrees + delta * item.flip(),
item.mapFromScene(self.event_anchor))
event.accept()
return
if self.flip_active:
event.accept()
return
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
just_selected = self.just_selected
self.just_selected = False
if self.scale_active:
self.scene().undo_stack.push(
commands.ScaleItemsBy(
@ -354,6 +464,7 @@ class SelectableMixin(BaseItemMixin):
ignore_first_redo=True))
self.scale_active = False
event.accept()
return
elif self.rotate_active:
self.scene().on_selection_change()
self.scene().undo_stack.push(
@ -364,8 +475,19 @@ class SelectableMixin(BaseItemMixin):
ignore_first_redo=True))
self.rotate_active = False
event.accept()
else:
super().mouseReleaseEvent(event)
return
elif not just_selected:
for edge in self.get_flip_bounds():
if edge['rect'].contains(event.pos()):
self.scene().undo_stack.push(
commands.FlipItems(
self.selection_action_items(),
self.center_scene_coords,
vertical=self.get_edge_flips_v(edge)))
event.accept()
self.flip_active = False
return
super().mouseReleaseEvent(event)
def on_view_scale_change(self):
self.prepareGeometryChange()
@ -428,6 +550,8 @@ class MultiSelectItem(SelectableMixin,
self.setRotation(0)
if not self.isSelected():
self.setSelected(True)
if self.flip() == -1:
self.setTransform(QtGui.QTransform.fromScale(1, 1))
def mousePressEvent(self, event):
if (event.button() == Qt.MouseButtons.LeftButton

View file

@ -38,7 +38,7 @@ class BeeGraphicsView(QtWidgets.QGraphicsView):
self.setBackgroundBrush(QtGui.QBrush(QtGui.QColor(60, 60, 60)))
self.undo_stack = QtGui.QUndoStack(self)
self.undo_stack.setUndoLimit(10)
self.undo_stack.setUndoLimit(100)
self.undo_stack.canRedoChanged.connect(self.on_can_redo_changed)
self.undo_stack.canUndoChanged.connect(self.on_can_undo_changed)

Binary file not shown.

View file

@ -93,12 +93,13 @@ class SQLiteIOWriteTestCase(BeeTestCase):
item.setPos(44, 55)
item.setZValue(0.22)
item.setRotation(33)
item.do_flip()
item.pixmap_to_bytes = MagicMock(return_value=b'abc')
self.io.write()
assert item.save_id == 1
result = self.io.fetchone(
'SELECT x, y, z, scale, rotation, filename, type, '
'SELECT x, y, z, scale, rotation, flip, filename, type, '
'sqlar.data, sqlar.name '
'FROM items '
'INNER JOIN sqlar on sqlar.item_id = items.id')
@ -107,10 +108,11 @@ class SQLiteIOWriteTestCase(BeeTestCase):
assert result[2] == 0.22
assert result[3] == 1.3
assert result[4] == 33
assert result[5] == 'bee.jpg'
assert result[6] == 'pixmap'
assert result[7] == b'abc'
assert result[8] == '0001-bee.png'
assert result[5] == -1
assert result[6] == 'bee.jpg'
assert result[7] == 'pixmap'
assert result[8] == b'abc'
assert result[9] == '0001-bee.png'
def test_inserts_new_item_without_filename(self):
item = BeePixmapItem(QtGui.QImage())
@ -138,6 +140,7 @@ class SQLiteIOWriteTestCase(BeeTestCase):
item.setPos(20, 30)
item.setZValue(0.33)
item.setRotation(100)
item.do_flip()
item.filename = 'new.png'
item.pixmap_to_bytes.return_value = b'updated'
self.io.create_new = False
@ -145,7 +148,7 @@ class SQLiteIOWriteTestCase(BeeTestCase):
assert self.io.fetchone('SELECT COUNT(*) from items') == (1,)
result = self.io.fetchone(
'SELECT x, y, z, scale, rotation, filename, sqlar.data '
'SELECT x, y, z, scale, rotation, flip, filename, sqlar.data '
'FROM items '
'INNER JOIN sqlar on sqlar.item_id = items.id')
assert result[0] == 20
@ -153,8 +156,9 @@ class SQLiteIOWriteTestCase(BeeTestCase):
assert result[2] == 0.33
assert result[3] == 0.7
assert result[4] == 100
assert result[5] == 'new.png'
assert result[6] == b'abc'
assert result[5] == -1
assert result[6] == 'new.png'
assert result[7] == b'abc'
def test_removes_nonexisting_item(self):
item = BeePixmapItem(QtGui.QImage(), filename='bee.png')
@ -206,9 +210,9 @@ class SQLiteIOReadTestCase(BeeTestCase):
io = SQLiteIO(fname, self.scene, create_new=True)
io.create_schema_on_new()
io.ex('INSERT INTO items '
'(type, x, y, z, scale, rotation, filename) '
'VALUES (?, ?, ?, ?, ?, ?, ?) ',
('pixmap', 22.2, 33.3, 0.22, 3.4, 45, 'bee.png'))
'(type, x, y, z, scale, rotation, flip, filename) '
'VALUES (?, ?, ?, ?, ?, ?, ?, ?) ',
('pixmap', 22.2, 33.3, 0.22, 3.4, 45, -1, 'bee.png'))
io.ex('INSERT INTO sqlar (item_id, data) VALUES (?, ?)',
(1, self.imgdata3x3))
io.connection.commit()
@ -224,6 +228,7 @@ class SQLiteIOReadTestCase(BeeTestCase):
assert item.zValue() == 0.22
assert item.scale() == 3.4
assert item.rotation() == 45
assert item.flip() == -1
assert item.filename == 'bee.png'
assert item.width == 3
assert item.height == 3

View file

@ -149,13 +149,14 @@ class RotateItemsByTestCase(BeeTestCase):
item2 = BeePixmapItem(QtGui.QImage())
item2.setRotation(30)
item2.setPos(100, 100)
item2.do_flip()
command = commands.RotateItemsBy([item1, item2], -90,
QtCore.QPointF(100, 100))
command.redo()
assert item1.rotation() == -90
assert item1.rotation() == 270
assert item1.pos().x() == 0
assert item1.pos().y() == 200
assert item2.rotation() == -60
assert item2.rotation() == 120
assert item2.pos().x() == 100
assert item2.pos().y() == 100
command.undo()
@ -173,6 +174,7 @@ class RotateItemsByTestCase(BeeTestCase):
item2 = BeePixmapItem(QtGui.QImage())
item2.setRotation(30)
item2.setPos(100, 100)
item2.do_flip()
command = commands.RotateItemsBy([item1, item2], -90,
QtCore.QPointF(100, 100),
ignore_first_redo=True)
@ -184,10 +186,10 @@ class RotateItemsByTestCase(BeeTestCase):
assert item2.pos().x() == 100
assert item2.pos().y() == 100
command.redo()
assert item1.rotation() == -90
assert item1.rotation() == 270
assert item1.pos().x() == 0
assert item1.pos().y() == 200
assert item2.rotation() == -60
assert item2.rotation() == 120
assert item2.pos().x() == 100
assert item2.pos().y() == 100
@ -214,3 +216,58 @@ class NormalizeItemsTestCase(BeeTestCase):
assert item1.pos() == QtCore.QPointF(0, 0)
assert item2.scale() == 3
assert item2.pos() == QtCore.QPointF(0, 0)
class FlipItemsTestCase(BeeTestCase):
def test_redo_undo_horizontal(self):
item1 = BeePixmapItem(QtGui.QImage())
item1.setRotation(0)
item2 = BeePixmapItem(QtGui.QImage())
item2.setRotation(30)
item2.setPos(100, 100)
item2.do_flip()
command = commands.FlipItems([item1, item2],
QtCore.QPointF(100, 100),
vertical=False)
command.redo()
assert item1.flip() == -1
assert item1.rotation() == 0
assert item1.pos() == QtCore.QPointF(200, 0)
assert item2.flip() == 1
assert item2.rotation() == 30
assert item2.pos() == QtCore.QPointF(100, 100)
command.undo()
assert item1.flip() == 1
assert item1.rotation() == 0
assert item1.pos() == QtCore.QPointF(0, 0)
assert item2.flip() == -1
assert item2.rotation() == 30
assert item2.pos() == QtCore.QPointF(100, 100)
def test_redo_undo_vertical(self):
item1 = BeePixmapItem(QtGui.QImage())
item1.setRotation(0)
item2 = BeePixmapItem(QtGui.QImage())
item2.setRotation(30)
item2.setPos(100, 100)
item2.do_flip()
command = commands.FlipItems([item1, item2],
QtCore.QPointF(100, 100),
vertical=True)
command.redo()
assert item1.flip() == -1
assert item1.rotation() == 180
assert item1.pos() == QtCore.QPointF(0, 200)
assert item2.flip() == 1
assert item2.rotation() == 210
assert item2.pos() == QtCore.QPointF(100, 100)
command.undo()
assert item1.flip() == 1
assert item1.rotation() == 0
assert item1.pos() == QtCore.QPointF(0, 0)
assert item2.flip() == -1
assert item2.rotation() == 30
assert item2.pos() == QtCore.QPointF(100, 100)

View file

@ -68,7 +68,7 @@ class BaseItemMixinTestCase(BeeTestCase):
assert item2.zValue() > item1.zValue()
assert item2.zValue() == self.scene.max_z
def test_set_rotation_anchor(self):
def test_set_rotation_no_anchor(self):
item = BeePixmapItem(QtGui.QImage())
item.setRotation(45)
assert item.rotation() == 45
@ -85,10 +85,50 @@ class BaseItemMixinTestCase(BeeTestCase):
def test_set_rotation_anchor_bottomright_ccw(self):
item = BeePixmapItem(QtGui.QImage())
item.setRotation(-90, QtCore.QPointF(100, 100))
assert item.rotation() == -90
assert item.rotation() == 270
assert item.pos().x() == 0
assert item.pos().y() == 200
def test_set_rotation_caps_above_360(self):
item = BeePixmapItem(QtGui.QImage())
item.setRotation(400)
assert item.rotation() == 40
def test_flip(self):
item = BeePixmapItem(QtGui.QImage())
assert item.flip() == 1
def test_flip_when_flipped(self):
item = BeePixmapItem(QtGui.QImage())
item.do_flip()
assert item.flip() == -1
def test_do_flip_no_anchor(self):
item = BeePixmapItem(QtGui.QImage())
item.do_flip()
assert item.flip() == -1
item.do_flip()
assert item.flip() == 1
def test_do_flip_vertical(self):
item = BeePixmapItem(QtGui.QImage())
item.do_flip(vertical=True)
assert item.flip() == -1
assert item.rotation() == 180
def test_do_flip_anchor(self):
item = BeePixmapItem(QtGui.QImage())
item.do_flip(anchor=QtCore.QPointF(100, 100))
assert item.flip() == -1
assert item.pos() == QtCore.QPointF(200, 0)
def test_do_flip_vertical_anchor(self):
item = BeePixmapItem(QtGui.QImage())
item.do_flip(vertical=True, anchor=QtCore.QPointF(100, 100))
assert item.flip() == -1
assert item.rotation() == 180
assert item.pos() == QtCore.QPointF(0, 200)
def test_center_scene_coords(self):
item = BeePixmapItem(QtGui.QImage())
self.scene.addItem(item)
@ -195,12 +235,36 @@ class SelectableMixinTestCase(SelectableMixinBaseTestCase):
painter.drawRect.assert_called_once()
painter.drawPoint.assert_not_called()
def test_paint_when_draw_debug_shapes(self):
def test_paint_when_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
args_mock.debug_shapes = True
args_mock.debug_boundingrects = False
args_mock.debug_handles = False
item = BeePixmapItem(QtGui.QImage())
item.paint(MagicMock(), None, None)
m.assert_called_once()
def test_paint_when_debug_boundingrects(self):
with patch('beeref.selection.commandline_args') as args_mock:
with patch('beeref.items.BeePixmapItem.draw_debug_shape') as m:
args_mock.debug_shapes = False
args_mock.debug_boundingrects = True
args_mock.debug_handles = False
item = BeePixmapItem(QtGui.QImage())
item.paint(MagicMock(), None, None)
m.assert_called_once()
def test_paint_when_debug_handles(self):
with patch('beeref.selection.commandline_args') as args_mock:
with patch('beeref.items.BeePixmapItem.draw_debug_shape') as m:
args_mock.debug_shapes = False
args_mock.debug_boundingrects = False
args_mock.debug_handles = True
item = BeePixmapItem(QtGui.QImage())
self.scene.addItem(item)
item.setSelected(True)
item.paint(MagicMock(), None, None)
m.assert_called()
def test_corners(self):
@ -270,6 +334,24 @@ class SelectableMixinTestCase(SelectableMixinBaseTestCase):
assert path.boundingRect().bottomRight().y() == 5
assert path.contains(QtCore.QPointF(-4, -4)) is False
def test_get_flip_bounds(self):
self.view.get_scale = MagicMock(return_value=1)
self.item.SELECT_RESIZE_SIZE = 10
self.item.SELECT_ROTATE_SIZE = 10
edges = self.item.get_flip_bounds()
assert edges[0]['rect'].topLeft() == QtCore.QPointF(5, -15)
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, 95)
assert edges[1]['flip_v'] is True
assert edges[2]['rect'].topLeft() == QtCore.QPointF(-15, 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(115, 75)
assert edges[3]['flip_v'] is False
def test_bounding_rect_when_not_selected(self):
self.view.get_scale = MagicMock(return_value=1)
self.item.setSelected(False)
@ -286,22 +368,17 @@ class SelectableMixinTestCase(SelectableMixinBaseTestCase):
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
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):
with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.boundingRect',
return_value=QtCore.QRectF(0, 0, 100, 80)):
shape = self.item.shape().boundingRect()
assert shape.topLeft().x() == 0
assert shape.topLeft().y() == 0
@ -332,11 +409,9 @@ class SelectableMixinTestCase(SelectableMixinBaseTestCase):
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):
with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.boundingRect',
return_value=QtCore.QRectF(0, 0, 100, 80)):
shape = self.item.shape().boundingRect()
assert shape.topLeft().x() == 0
assert shape.topLeft().y() == 0
@ -430,6 +505,22 @@ class SelectableMixinCalculationsTestCase(SelectableMixinBaseTestCase):
assert self.item.get_rotate_delta(
QtCore.QPointF(15, 25), snap=True) == -35
def test_edge_flips_v_when_item_horizontal(self):
self.item.setRotation(20)
assert self.item.get_edge_flips_v({'flip_v': True}) is True
def test_edge_flips_v_when_item_horizontal_upside_down(self):
self.item.setRotation(200)
assert self.item.get_edge_flips_v({'flip_v': True}) is True
def test_edge_flips_v_when_item_vertical_cw(self):
self.item.setRotation(80)
assert self.item.get_edge_flips_v({'flip_v': True}) is False
def test_edge_flips_v_when_item_vertical_ccw(self):
self.item.setRotation(120)
assert self.item.get_edge_flips_v({'flip_v': True}) is False
class SelectableMixinMouseEventsTestCase(SelectableMixinBaseTestCase):
@ -496,6 +587,50 @@ class SelectableMixinMouseEventsTestCase(SelectableMixinBaseTestCase):
self.item.hoverMoveEvent(self.event)
self.item.setCursor.assert_called_once_with(BeeAssets().cursor_rotate)
def test_hover_flip_event_top_edge(self):
self.item.setSelected(True)
self.event.pos = MagicMock(return_value=QtCore.QPointF(50, 0))
self.item.hoverMoveEvent(self.event)
self.item.setCursor.assert_called_once_with(
BeeAssets().cursor_flip_v)
def test_hover_flip_event_bottom_edge(self):
self.item.setSelected(True)
self.event.pos = MagicMock(return_value=QtCore.QPointF(50, 80))
self.item.hoverMoveEvent(self.event)
self.item.setCursor.assert_called_once_with(
BeeAssets().cursor_flip_v)
def test_hover_flip_event_left_edge(self):
self.item.setSelected(True)
self.event.pos = MagicMock(return_value=QtCore.QPointF(0, 50))
self.item.hoverMoveEvent(self.event)
self.item.setCursor.assert_called_once_with(
BeeAssets().cursor_flip_h)
def test_hover_flip_event_right_edge(self):
self.item.setSelected(True)
self.event.pos = MagicMock(return_value=QtCore.QPointF(100, 50))
self.item.hoverMoveEvent(self.event)
self.item.setCursor.assert_called_once_with(
BeeAssets().cursor_flip_h)
def test_hover_flip_event_top_edge_rotated_90(self):
self.item.setSelected(True)
self.item.setRotation(90)
self.event.pos = MagicMock(return_value=QtCore.QPointF(50, 0))
self.item.hoverMoveEvent(self.event)
self.item.setCursor.assert_called_once_with(
BeeAssets().cursor_flip_h)
def test_hover_flip_event_left_edge_when_rotated_90(self):
self.item.setSelected(True)
self.item.setRotation(90)
self.event.pos = MagicMock(return_value=QtCore.QPointF(0, 50))
self.item.hoverMoveEvent(self.event)
self.item.setCursor.assert_called_once_with(
BeeAssets().cursor_flip_v)
def test_hover_move_event_not_in_handles(self):
self.item.setSelected(True)
self.event.pos = MagicMock(return_value=QtCore.QPointF(50, 50))
@ -514,12 +649,24 @@ class SelectableMixinMouseEventsTestCase(SelectableMixinBaseTestCase):
self.item.setCursor.assert_called_once_with(
Qt.CursorShape.ArrowCursor)
def test_mouse_press_event_just_selected(self):
self.event.pos = MagicMock(return_value=QtCore.QPointF(-100, -100))
with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.mousePressEvent'):
self.item.mousePressEvent(self.event)
assert self.item.just_selected is True
def test_mouse_press_event_previously_selected(self):
self.item.setSelected(True)
self.event.pos = MagicMock(return_value=QtCore.QPointF(-100, -100))
with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.mousePressEvent'):
self.item.mousePressEvent(self.event)
assert self.item.just_selected is False
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(-1, -1))
self.event.button = MagicMock(
return_value=Qt.MouseButtons.LeftButton)
self.event.button = MagicMock(return_value=Qt.MouseButtons.LeftButton)
self.item.mousePressEvent(self.event)
assert self.item.scale_active is True
assert self.item.event_start == QtCore.QPointF(-1, -1)
@ -531,8 +678,7 @@ class SelectableMixinMouseEventsTestCase(SelectableMixinBaseTestCase):
self.item.setSelected(True)
self.event.pos = MagicMock(return_value=QtCore.QPointF(99, 79))
self.event.scenePos = MagicMock(return_value=QtCore.QPointF(101, 81))
self.event.button = MagicMock(
return_value=Qt.MouseButtons.LeftButton)
self.event.button = MagicMock(return_value=Qt.MouseButtons.LeftButton)
self.item.mousePressEvent(self.event)
assert self.item.scale_active is True
assert self.item.event_start == QtCore.QPointF(101, 81)
@ -544,29 +690,39 @@ class SelectableMixinMouseEventsTestCase(SelectableMixinBaseTestCase):
self.item.setSelected(True)
self.event.pos = MagicMock(return_value=QtCore.QPointF(111, 91))
self.event.scenePos = MagicMock(return_value=QtCore.QPointF(66, 99))
self.event.button = MagicMock(
return_value=Qt.MouseButtons.LeftButton)
self.event.button = MagicMock(return_value=Qt.MouseButtons.LeftButton)
self.item.mousePressEvent(self.event)
assert self.item.rotate_active is True
assert self.item.event_anchor == QtCore.QPointF(50, 40)
assert self.item.rotate_orig_degrees == 0
def test_mouse_press_event_flip(self):
self.item.setSelected(True)
self.event.pos = MagicMock(return_value=QtCore.QPointF(0, 40))
self.event.button = MagicMock(return_value=Qt.MouseButtons.LeftButton)
with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.mousePressEvent'):
self.item.mousePressEvent(self.event)
assert self.item.flip_active is True
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
assert self.item.rotate_active is False
assert self.item.flip_active is False
def test_mouse_press_event_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)
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
assert self.item.rotate_active is False
assert self.item.flip_active is False
def test_mouse_move_event_when_no_action(self):
with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.mouseMoveEvent') as m:
@ -581,7 +737,9 @@ class SelectableMixinMouseEventsTestCase(SelectableMixinBaseTestCase):
self.item.event_start = QtCore.QPointF(10, 10)
self.item.scale_orig_factor = 1
self.item.mouseMoveEvent(self.event)
with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.mouseMoveEvent') as m:
self.item.mouseMoveEvent(self.event)
m.assert_not_called()
assert self.item.scale() == approx(1.5, 0.01)
def test_mouse_move_event_when_rotate_action(self):
@ -590,10 +748,20 @@ class SelectableMixinMouseEventsTestCase(SelectableMixinBaseTestCase):
self.item.rotate_orig_degrees = 0
self.item.rotate_start_angle = -3
self.item.event_anchor = QtCore.QPointF(10, 20)
self.item.mouseMoveEvent(self.event)
assert self.item.rotation() == -42
with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.mouseMoveEvent') as m:
self.item.mouseMoveEvent(self.event)
m.assert_not_called()
assert self.item.rotation() == 318
def test_mouse_move_event_when_flip_action(self):
self.event.scenePos = MagicMock(return_value=QtCore.QPointF(15, 25))
self.item.flip_active = True
with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.mouseMoveEvent') as m:
self.item.mouseMoveEvent(self.event)
m.assert_not_called()
def test_mouse_release_event_when_no_action(self):
self.event.pos = MagicMock(return_value=QtCore.QPointF(-100, -100))
with patch('PyQt6.QtWidgets.QGraphicsPixmapItem'
'.mouseReleaseEvent') as m:
self.item.mouseReleaseEvent(self.event)
@ -635,6 +803,18 @@ class SelectableMixinMouseEventsTestCase(SelectableMixinBaseTestCase):
assert cmd.ignore_first_redo is True
assert self.item.rotate_active is False
def test_mouse_release_event_when_flip_action(self):
self.event.pos = MagicMock(return_value=QtCore.QPointF(0, 40))
self.item.flip_active = True
self.scene.undo_stack = MagicMock(push=MagicMock())
self.item.mouseReleaseEvent(self.event)
args = self.scene.undo_stack.push.call_args_list[0][0]
cmd = args[0]
assert cmd.items == [self.item]
assert cmd.anchor == QtCore.QPointF(50, 40)
assert cmd.vertical is False
class MultiSelectItemTestCase(BeeTestCase):
@ -695,6 +875,7 @@ class MultiSelectItemTestCase(BeeTestCase):
item = MultiSelectItem()
item.setScale(5)
item.setRotation(-20)
item.do_flip()
item.fit_selection_area(QtCore.QRectF(-10, -20, 100, 80))
assert item.pos().x() == -10
assert item.pos().y() == -20
@ -702,6 +883,7 @@ class MultiSelectItemTestCase(BeeTestCase):
assert item.height == 80
assert item.scale() == 1
assert item.rotation() == 0
assert item.flip() == 1
assert item.isSelected() is True
@patch('PyQt6.QtWidgets.QGraphicsRectItem.mousePressEvent')