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 0000000..3802218
Binary files /dev/null and b/beeref/assets/cursor_flip_h.png differ
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 @@
+
+
+
+
diff --git a/beeref/assets/cursor_flip_v.png b/beeref/assets/cursor_flip_v.png
new file mode 100644
index 0000000..68bb61c
Binary files /dev/null and b/beeref/assets/cursor_flip_v.png differ
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 @@
+
+
+
+
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">
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')