From 4e31ec8bac86a2c567d75e9d77cb77b853fb4123 Mon Sep 17 00:00:00 2001 From: Rebecca Breu Date: Sat, 10 Apr 2021 19:35:47 +0200 Subject: [PATCH] Add flip --- beeref/assets.py | 7 +- beeref/assets/cursor_flip_h.png | Bin 0 -> 1766 bytes beeref/assets/cursor_flip_h.svg | 140 +++++++++++++++++ beeref/assets/cursor_flip_v.png | Bin 0 -> 1741 bytes beeref/assets/cursor_flip_v.svg | 141 +++++++++++++++++ beeref/assets/cursor_rotate.svg | 6 +- beeref/assets/cursor_rotate_2.png | Bin 0 -> 2382 bytes beeref/commands.py | 25 ++- beeref/config.py | 14 +- beeref/fileio/__init__.py | 2 +- beeref/fileio/schema.py | 3 +- beeref/fileio/sql.py | 24 +-- beeref/selection.py | 186 +++++++++++++++++++---- beeref/view.py | 2 +- tests/assets/test1item.bee | Bin 16384 -> 16384 bytes tests/fileio/test_sql.py | 27 ++-- tests/test_commands.py | 65 +++++++- tests/test_selection.py | 244 ++++++++++++++++++++++++++---- 18 files changed, 785 insertions(+), 101 deletions(-) create mode 100644 beeref/assets/cursor_flip_h.png create mode 100644 beeref/assets/cursor_flip_h.svg create mode 100644 beeref/assets/cursor_flip_v.png create mode 100644 beeref/assets/cursor_flip_v.svg create mode 100644 beeref/assets/cursor_rotate_2.png diff --git a/beeref/assets.py b/beeref/assets.py index 2da5b00..d2174fe 100644 --- a/beeref/assets.py +++ b/beeref/assets.py @@ -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)) diff --git a/beeref/assets/cursor_flip_h.png b/beeref/assets/cursor_flip_h.png new file mode 100644 index 0000000000000000000000000000000000000000..3802218b6558a09888f5da218109ebd45568fc29 GIT binary patch literal 1766 zcmVQ`Z^5@&EO;oe`X7*y3QNYdW`SwJ~J(Bqj6%%3!_BrRtA z4t@k!fyEXf{^j%eZvIk$K~;T5!1G4k?RFOex1_eItE*#ZXb6CF=g!RoW=t67PB|7k zHa0dk`?>;ynzmKIpEIOprdH0LJ&WJ(2SALEodgOY0FWeUp(z?Y=J)&OTo+(KQJxb* zYy;@+?d9OXgUR>I)ZTzFVFUVnJ{%4QLI@;DDkT!>(%tT_e<5H%QMO5v^sIras;WX! zl!=hEh{xmO1545*eqHx?Zp$#t)q%RI{#BBstpF!ap5$jM zSE8zFGM{6$=64=B@{ll_S7ZWywYYe0UvKX!A;ev&`KG2O91aKZcpOQRZWks~SJ3PI zMmC2z3vN5`G{9RYPVn>UYQ_@JmNY&y6ciMEd+CxT4XOQv5J}+IZoTzx;O^91Dl!(0 z^1{xYm`tX5ap(#vO4*E}q7Nrp(q>QyA?cd7!@&Lf_;DV2_~9fD;O*0=GcNDo;2?c{ zeROnm5RFFDazY5&4({Cj*c40PlOOTCL}4jlY^lUvDh^qIIdfS!eL^GPl0GOnzLog zmRy7oCTCyYA!8O>Uwu{Fl#_GAXK#6Vx$wGNLRVFxj*hBNZroS{{15<(#d1f_sZ;F+ z@}IB0c0-Ys6&0yCWm8SfFM*!|CB|2goR^n(*U2~E{LAh0=l=wty}ccq&Bm20S0*h* zNl6L0xw&cATrL+bmkW!T*8>B%w{v8Q0Ve+YHK(DWAEON6S(uKxNJaxcq9_(S-f=Va|W`!qJri< zd&o)O9PjUGY*CWd^dNAnPuyNz&1Ffy2 zz%cNiOgOmukw32Mde1!%Jg~LDzrO>(Y|eg9bLPw;CnqN}RF;utnSicedSb%{19$PW za8m)o_%r!VDl%I%G&E3JTADQ<2n0U1tzZB9*0#1$;6uRqS-7bJ4P?)vd++^|)9E}6 zpm@C(0cI2zdt$Medt!&7#P3f@i1UE(`~AbeShsHTp^gqWFbG_@8V;`guMZixYW#ZAX_Ove=BzsmY{?_;4huK)l507*qo IM6N<$f|}7>VgLXD literal 0 HcmV?d00001 diff --git a/beeref/assets/cursor_flip_h.svg b/beeref/assets/cursor_flip_h.svg new file mode 100644 index 0000000..6a261c5 --- /dev/null +++ b/beeref/assets/cursor_flip_h.svg @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/beeref/assets/cursor_flip_v.png b/beeref/assets/cursor_flip_v.png new file mode 100644 index 0000000000000000000000000000000000000000..68bb61cdc898edc6b54db341ad2750b0cff8150b GIT binary patch literal 1741 zcmV;;1~U1HP)f zx!ZPU_V}SYvn_T9+AVOBo6O!bd*-(@clO>HV1yzN2xJE(>1&1aDk8FtRNYY{QABBP z&ta;1kQggw0D`hShKLq2#?*Lgvv>=L&dxkotqlMQuS#umq@+C07z@W!-xAN4gOW53 zR_l)d3W*4BEnkkEJ9p~oXNBLtk%;0tMm)Fz!TCRebL#5sq-Bd1kqsb+)k?LSHj&`G zuSwGSn~v-D`-=qU|LVBs7cL|lV6>!!1ghV$BZ`#ek8c{T$L}u|vV1{sPI7xYJ@xqG z18{mvw%h5e-MbBBP?q1j;kbY#l?cwS2+oOLxk68tmJW*3TdKoBKYaUb6v_EJHw>pp z(p`e{cELGGZEf_#oH?dAy`?87(=QDT{m0}RBah?!9>IC1;G8-xU7}gjr(YLNZy6~m zbokIA9T`?Qe`&;U!tbA|a4rkZsr}+bn)%Q}L&E7TCq11SfB#)aYC&0kHf~(d=X+3a z?iZZXl?xYW#%^75F)T`m{atXYGMj0^xkSkvH4PR5ieQ}kVWEx===Q&WE!GB7C1&k<2= z?B50i6a~3%cg*~}dGkZ$n@YAOLUfNJ$LR9>o>L9Y&Hh~eBaRUjaiMDz{n*369D_wxv%Q$ zpK#~ocr?{~u@VkP*9i~KnDJ3ZN5^1a2o4KK7cVCIZMMmw-d>m9r-s7;B2q`&Y}o*y zX__gpf4*wN23z5T30(gi3WXHEEca~Pwd-_cWo677F~^{gq&LCXYBNCF<;y54E{>Tm zEiJ{?ty|4XfB{vG)YKPa{;AVRD`tjbfeGVaKLW2 zqpYk9^XJb;cXzjSSU^2DY0@M#H8l;ogV*atSy@@k0&ce(b#-;f&dwgt&pF3!X=wle zA~IcZ$3BBZr-+$95qLbNR|g)C2gQSo(bUvbW4oujyK7iL$CxqI%a$x*>1k<+0KhC3 zmX(o_1ORhq&z@CKQ1I}z?&5?|edURWemi`)`s%!O zX(a%b4IQYeYkO36k=<_30{Z^bjvhUV1e*;>iHT5E71f(InF0?SIDm$O2aP+{<#NI0 zav?qXw?q5)?>}c+#4MIBUc5wVYwOUTLFUYy8P|F+H!F*dA30*ItiRl|=TiVPhYbXf z`NGnrFGy`|K7F}A-QyV+ZoJz~e>65madma;0XzaAGaeuSWWDz4t1rv#?S8>IT|R%F z?!Wu)A>&RRKW^aackf;g;86f$1m|?AwUzEIDY-7(gxp*Mr$|!x zi`v@NBaJhVlD5O^)p0o^jngB0&8k&z2YkLB z!8x62X`!O=;|Ik}%*&&e6DJJZ_RX6=ir{i@6wW|?vUcr?fX~+}IH$i)oub10`~kSa z{CxWJjdXvu-f z@qPq1{#N1i$op>Jz6u=|@cBZOpMPE+!4=$Mn-Tfn?c2BE^7-?vpRHT>9!BuY#r+b! jUyK7#6xlu#X>rGY)npM5k*c2%00000NkvXXu0mjfj&eZ7 literal 0 HcmV?d00001 diff --git a/beeref/assets/cursor_flip_v.svg b/beeref/assets/cursor_flip_v.svg new file mode 100644 index 0000000..16ae3b6 --- /dev/null +++ b/beeref/assets/cursor_flip_v.svg @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/beeref/assets/cursor_rotate.svg b/beeref/assets/cursor_rotate.svg index 06fc6d3..868754f 100644 --- a/beeref/assets/cursor_rotate.svg +++ b/beeref/assets/cursor_rotate.svg @@ -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"> }t9IMCw(AeX zD!N;nG}zj>+q!Mjx^1#qQ>s?&E{0~UZ6b;IYrERjrtNOjnAKJxf~8t4{=jOhSQrEx zD8n%Coq?GS_Dqn~J}UzQ)g zo!HWF6NJa}D46#HNb&l7Sozv(ICSXHE%Rp2o{i_9e;#_h9!8@Psi~>R%*=#dukSnU zoCLgSFqMU9f;Z+O>G za@~HVGxIp1C?%T1BL@$1lEpGAxPkh!yYJ?mTeq@LQMh-hd@DLTQ*H-N?;|SL;5KdC z$OgUsj^O(0lXK^?qp^_%5qHb7-Rt!xjS5hfWs@NKNf7am&CQ%QYu1>-X_`H065Fp_ zVL`+mm+Q>;-+v!HD#%v5Jy&r$+qJ6QTwKh?@#Du7a?0e%>~uI-5OI4;%Lk(Z1Q3-w zd-nV*-+p^s`_yN@{+b`o&W;$+78l1ercdX4)6;L+f+FeirAt{5u~(LFG+e&?;E2eW zSrSD1eX@LXO=02u&-U#r_PAW#f`}ETlmE1G<)8ouKK(RYrOiM5z*{zL;*6A(VIj*7 zA7(+sP4)E$F@kLm!sB@?yd5YCT{v^*mF3Hp6;IEzI^MZ=#w!sB^g5OKHL9S-E_?A((#XU-E9$Bymv%5p#uaa&Up z7c5y44pejUBnu+`Y~Q}(-8*)?)7I2fr`f-{V#SbR!E@rT>CZLryFzf4O@ z%dP$9n}dRgyE;3ET)%%SDq=yzbr&xlAF`L^j*d7%6cR-I^VC$11~@-2k6Y^NwNKvD z-rW4ww49vTy}{L9$K2A;a1(IXu61U!*$f~mH8nM>`Rdhg1QGB55OAlZr6D090RRvRg}B#U(BW{{uhiGC1%SD8=fY?-4w{&Th6VtD!C*)-8Vm^$ zGwAE;S|o`0Ty^y=Zy6&=yy}fN!e?r?ELX(G$0saWuwZe+<;!1bpS-lBq|joqnEJSt zy4`NKAmYcSP8~FZiDq+nz0a95C$BG4RNn`QIUWGIyKn7@nHeP|C17S06%|2|AYze6=jN!j!p+q+d*{kmDgYI1aP08Ssxo48&DJlz|Esmr_-eX2rrv~Vl@3P z;f?k6J$=XetwBkrYX^X&q{!!YO-&6{RfXAXp0wfZw==bObGzNG7K`N+>-E(Rhr_SU zA1_IN05HyOx1+JKaZsNb>FEG~Kp@c7;dI`J2xtff>i~poHXG7=jRs0nQxlFJJqiHQ zvjqjkvfUnD`3(&XJua8at37*Vc?u-y9RS$2eS5^0o;qzB0Knt%*i}^x^d0M;L7S~B z7z|zpZg$9#WYwxwAR^plFl6cC;<5QHUbUIxnfVk?@r?F@E?uh;t=H~+d zt~E7XV7=ZSaUuYCT&@j*h)-5j47vNym_D7`TUxa0>k&lz{T|OcL9|&AUC^rXbX65k zOh}06C+GhAwZlMN`O-@-+HAJTLjt-Tj#Qtb1O*W}=%b<{~F22Rhx& z!t{E*j{h7v^7qxNSD%(7DWnU9>VG6^0l*Q1k2I4LCr-Spr>7@-{hBquernO8f`r7x zX_6$3$iAzqtD)k=iGROYSXdS4>0vN?gOYSfmgTPDY)AXuC9Bn%qEpq(#rgU76g>Ov zjI7Me9JAS+5fdG4*6DOHfj~g<`~B{2uh)6CuCDgLfdgkhKYFwS08~}|DGF$ACvpG? zjws+L0X5NEt=79GNlMW(YrWE7zBnXFsw7FIs;X0#VwLn_YQ389753*~P`h8C%&WU+4S96`!Ht z=@;Va?iw^XfnS6#Ehn=ez6@Dt@>_mMmYEFvGdBww^z-wiGV(A;va_>ua&T~LUMhct VpUX_az|zXp$ja1obFqG}006M&A>sf4 diff --git a/tests/fileio/test_sql.py b/tests/fileio/test_sql.py index acb25f2..42804d9 100644 --- a/tests/fileio/test_sql.py +++ b/tests/fileio/test_sql.py @@ -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 diff --git a/tests/test_commands.py b/tests/test_commands.py index a01247d..ca76b00 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -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) diff --git a/tests/test_selection.py b/tests/test_selection.py index 6348856..e36dbd7 100644 --- a/tests/test_selection.py +++ b/tests/test_selection.py @@ -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')