Add flip
|
|
@ -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))
|
||||
|
|
|
|||
BIN
beeref/assets/cursor_flip_h.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
140
beeref/assets/cursor_flip_h.svg
Normal 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 |
BIN
beeref/assets/cursor_flip_v.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
141
beeref/assets/cursor_flip_v.svg
Normal 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 |
|
|
@ -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 |
BIN
beeref/assets/cursor_rotate_2.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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!')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
""",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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│
|
||||
──┼─┬─┤
|
||||
FF│S│R│
|
||||
FF├─┘ │
|
||||
FF│R 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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||