From 1f47e979f0428fc26e107ac72ce2a72f3f9b67bc Mon Sep 17 00:00:00 2001 From: Sebastian Parborg Date: Fri, 31 May 2024 16:19:03 +0200 Subject: [PATCH 1/3] Make moving the crop rectangle possible, tweak accept/cancel behaviour --- beeref/items.py | 68 +++++++++++++++++++++++++++++++++++++------------ beeref/scene.py | 6 ++++- 2 files changed, 57 insertions(+), 17 deletions(-) diff --git a/beeref/items.py b/beeref/items.py index 238b9ef..f09d2e3 100644 --- a/beeref/items.py +++ b/beeref/items.py @@ -539,9 +539,42 @@ class BeePixmapItem(BeeItemMixin, QtWidgets.QGraphicsPixmapItem): self.crop_mode_event_start = event.pos() self.crop_mode_move = edge return + if self.crop_temp.contains(event.pos()): + self.crop_mode_event_start = event.pos() + self.crop_mode_move = self.crop_temp + return # Click not in handle, end cropping mode: - self.exit_crop_mode( - confirm=self.crop_temp.contains(event.pos())) + self.exit_crop_mode(confirm=True) + + def mouseDoubleClickEvent(self, event): + if not self.crop_mode: + return super().mouseDoubleClickEvent(event) + + event.accept() + if self.crop_temp.contains(event.pos()): + self.exit_crop_mode(confirm=True) + + def ensure_crop_box_is_inside(self, point): + """Returns the modified point that ensures that the crop rectangle is + still withint the pixmap. + + The point passed is assumed to be the top + left crop rectangle position. + """ + + max_x_pos = self.pixmap().size().width() - self.crop_temp.width() + max_y_pos = self.pixmap().size().height() - self.crop_temp.height() + + if point.x() < 0: + point.setX(0) + elif point.x() > max_x_pos: + point.setX(max_x_pos) + + if point.y() < 0: + point.setY(0) + elif point.y() > max_y_pos: + point.setY(max_y_pos) + return point def ensure_point_within_crop_bounds(self, point, handle): """Returns the point, or the nearest point within the pixmap.""" @@ -549,31 +582,31 @@ class BeePixmapItem(BeeItemMixin, QtWidgets.QGraphicsPixmapItem): if handle == self.crop_handle_topleft: topleft = QtCore.QPointF(0, 0) bottomright = self.crop_temp.bottomRight() - if handle == self.crop_handle_bottomleft: + elif handle == self.crop_handle_bottomleft: topleft = QtCore.QPointF(0, self.crop_temp.top()) bottomright = QtCore.QPointF( self.crop_temp.right(), self.pixmap().size().height()) - if handle == self.crop_handle_bottomright: + elif handle == self.crop_handle_bottomright: topleft = self.crop_temp.topLeft() bottomright = QtCore.QPointF( self.pixmap().size().width(), self.pixmap().size().height()) - if handle == self.crop_handle_topright: + elif handle == self.crop_handle_topright: topleft = QtCore.QPointF(self.crop_temp.left(), 0) bottomright = QtCore.QPointF( self.pixmap().size().width(), self.crop_temp.bottom()) - if handle == self.crop_edge_top: + elif handle == self.crop_edge_top: topleft = QtCore.QPointF(0, 0) bottomright = QtCore.QPointF( self.pixmap().size().width(), self.crop_temp.bottom()) - if handle == self.crop_edge_bottom: + elif handle == self.crop_edge_bottom: topleft = QtCore.QPointF(0, self.crop_temp.top()) bottomright = QtCore.QPointF( self.pixmap().size().width(), self.pixmap().size().height()) - if handle == self.crop_edge_left: + elif handle == self.crop_edge_left: topleft = QtCore.QPointF(0, 0) bottomright = QtCore.QPointF( self.crop_temp.right(), self.pixmap().size().height()) - if handle == self.crop_edge_right: + elif handle == self.crop_edge_right: topleft = QtCore.QPointF(self.crop_temp.left(), 0) bottomright = QtCore.QPointF( self.pixmap().size().width(), self.pixmap().size().height()) @@ -586,35 +619,38 @@ class BeePixmapItem(BeeItemMixin, QtWidgets.QGraphicsPixmapItem): def mouseMoveEvent(self, event): if self.crop_mode: diff = event.pos() - self.crop_mode_event_start + if self.crop_mode_move == self.crop_temp: + new = self.ensure_crop_box_is_inside(self.crop_temp.topLeft() + diff) + self.crop_temp.moveTo(new) if self.crop_mode_move == self.crop_handle_topleft: new = self.ensure_point_within_crop_bounds( self.crop_temp.topLeft() + diff, self.crop_mode_move) self.crop_temp.setTopLeft(new) - if self.crop_mode_move == self.crop_handle_bottomleft: + elif self.crop_mode_move == self.crop_handle_bottomleft: new = self.ensure_point_within_crop_bounds( self.crop_temp.bottomLeft() + diff, self.crop_mode_move) self.crop_temp.setBottomLeft(new) - if self.crop_mode_move == self.crop_handle_bottomright: + elif self.crop_mode_move == self.crop_handle_bottomright: new = self.ensure_point_within_crop_bounds( self.crop_temp.bottomRight() + diff, self.crop_mode_move) self.crop_temp.setBottomRight(new) - if self.crop_mode_move == self.crop_handle_topright: + elif self.crop_mode_move == self.crop_handle_topright: new = self.ensure_point_within_crop_bounds( self.crop_temp.topRight() + diff, self.crop_mode_move) self.crop_temp.setTopRight(new) - if self.crop_mode_move == self.crop_edge_top: + elif self.crop_mode_move == self.crop_edge_top: new = self.ensure_point_within_crop_bounds( self.crop_temp.topLeft() + diff, self.crop_mode_move) self.crop_temp.setTop(new.y()) - if self.crop_mode_move == self.crop_edge_left: + elif self.crop_mode_move == self.crop_edge_left: new = self.ensure_point_within_crop_bounds( self.crop_temp.topLeft() + diff, self.crop_mode_move) self.crop_temp.setLeft(new.x()) - if self.crop_mode_move == self.crop_edge_bottom: + elif self.crop_mode_move == self.crop_edge_bottom: new = self.ensure_point_within_crop_bounds( self.crop_temp.bottomLeft() + diff, self.crop_mode_move) self.crop_temp.setBottom(new.y()) - if self.crop_mode_move == self.crop_edge_right: + elif self.crop_mode_move == self.crop_edge_right: new = self.ensure_point_within_crop_bounds( self.crop_temp.topRight() + diff, self.crop_mode_move) self.crop_temp.setRight(new.x()) diff --git a/beeref/scene.py b/beeref/scene.py index 56325c2..9a3e456 100644 --- a/beeref/scene.py +++ b/beeref/scene.py @@ -87,7 +87,7 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene): def cancel_crop_mode(self): """Cancels an ongoing crop mode, if there is any.""" if self.crop_item: - self.crop_item.exit_crop_mode(confirm=False) + self.crop_item.exit_crop_mode(confirm=True) def copy_selection_to_internal_clipboard(self): self.internal_clipboard = [] @@ -394,6 +394,10 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene): super().mousePressEvent(event) def mouseDoubleClickEvent(self, event): + if self.crop_item: + super().mouseDoubleClickEvent(event) + return + self.cancel_active_modes() item = self.itemAt(event.scenePos(), self.views()[0].transform()) if item: From 9a6f7c8812e50575676b90ec625f609ecee2a390 Mon Sep 17 00:00:00 2001 From: Sebastian Parborg Date: Mon, 3 Jun 2024 14:25:50 +0200 Subject: [PATCH 2/3] Make cursor change to indicate move action --- beeref/items.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/beeref/items.py b/beeref/items.py index f09d2e3..638a3da 100644 --- a/beeref/items.py +++ b/beeref/items.py @@ -520,6 +520,9 @@ class BeePixmapItem(BeeItemMixin, QtWidgets.QGraphicsPixmapItem): if edge().contains(event.pos()): self.set_cursor(self.get_crop_edge_cursor(edge)) return + if self.crop_temp.contains(event.pos()): + self.set_cursor(Qt.CursorShape.SizeAllCursor) + return self.unset_cursor() def mousePressEvent(self, event): From e610333f6079ffecaee69889705381034a577e5a Mon Sep 17 00:00:00 2001 From: Sebastian Parborg Date: Sun, 9 Jun 2024 14:54:00 +0200 Subject: [PATCH 3/3] Fixed tests and flake8 errors. Updated changelog. --- CHANGELOG.rst | 5 +++++ beeref/items.py | 5 +++-- tests/items/test_pixmapitem.py | 26 +++++++++++++++++++++----- tests/test_scene.py | 2 +- 4 files changed, 30 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b8beee5..fd1a60f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -22,6 +22,11 @@ Added further files will be ignored, as previously. If the first argument isn't a bee file, all files will be treated as images and inserted as if opened with "Insert -> Images". +* It is now possible to shift around the crop rectagle by clicking and + dragging. To confirm the crop, you can now either click outside the + crop rectagle or double click inside the crop rectangle. Canceling + the crop operation is now only possible by pressing the escape key. + (by DarkDefender) Fixed ----- diff --git a/beeref/items.py b/beeref/items.py index 638a3da..706882c 100644 --- a/beeref/items.py +++ b/beeref/items.py @@ -559,7 +559,7 @@ class BeePixmapItem(BeeItemMixin, QtWidgets.QGraphicsPixmapItem): def ensure_crop_box_is_inside(self, point): """Returns the modified point that ensures that the crop rectangle is - still withint the pixmap. + still within the pixmap. The point passed is assumed to be the top left crop rectangle position. @@ -623,7 +623,8 @@ class BeePixmapItem(BeeItemMixin, QtWidgets.QGraphicsPixmapItem): if self.crop_mode: diff = event.pos() - self.crop_mode_event_start if self.crop_mode_move == self.crop_temp: - new = self.ensure_crop_box_is_inside(self.crop_temp.topLeft() + diff) + new = self.ensure_crop_box_is_inside( + self.crop_temp.topLeft() + diff) self.crop_temp.moveTo(new) if self.crop_mode_move == self.crop_handle_topleft: new = self.ensure_point_within_crop_bounds( diff --git a/tests/items/test_pixmapitem.py b/tests/items/test_pixmapitem.py index acfa7f7..1354cfd 100644 --- a/tests/items/test_pixmapitem.py +++ b/tests/items/test_pixmapitem.py @@ -734,8 +734,8 @@ def test_mouse_press_event_crop_mode_inside_edge(mouse_mock, qapp, item): event.accept.assert_called_once_with() -@patch('beeref.selection.SelectableMixin.mousePressEvent') -def test_mouse_press_event_crop_mode_outside_handle_inside_crop( +@patch('PyQt6.QtWidgets.QGraphicsScene.mouseDoubleClickEvent') +def test_mouse_doubleclick_event_crop_mode_outside_handle_inside_crop( mouse_mock, qapp, item): item.crop_mode = True item.crop_temp = QtCore.QRectF(0, 0, 100, 80) @@ -744,7 +744,7 @@ def test_mouse_press_event_crop_mode_outside_handle_inside_crop( event = MagicMock() event.pos.return_value = QtCore.QPointF(50, 50) - item.mousePressEvent(event) + item.mouseDoubleClickEvent(event) assert item.crop_mode_move is None item.exit_crop_mode.assert_called_once_with(confirm=True) mouse_mock.assert_not_called() @@ -763,7 +763,7 @@ def test_mouse_press_event_crop_mode_outside_handle_outside_crop( item.mousePressEvent(event) assert item.crop_mode_move is None - item.exit_crop_mode.assert_called_once_with(confirm=False) + item.exit_crop_mode.assert_called_once_with(confirm=True) mouse_mock.assert_not_called() event.accept.assert_called_once_with() @@ -820,6 +820,21 @@ def test_ensure_point_within_crop_bounds( assert result == QtCore.QPointF(*expected) +@pytest.mark.parametrize('point,expected', + [((-10, -10), (0, 0)), + ((-10, 80), (0, 40)), + ((100, 80), (70, 40)), + ((100, -10), (70, 0))]) +def test_ensure_crop_box_is_inside( + point, expected, qapp, item): + pixmap = MagicMock() + pixmap.size.return_value = QtCore.QRectF(0, 0, 100, 80) + item.pixmap = MagicMock(return_value=pixmap) + item.crop_temp = QtCore.QRectF(10, 20, 30, 40) + result = item.ensure_crop_box_is_inside(QtCore.QPointF(*point)) + assert result == QtCore.QPointF(*expected) + + @pytest.mark.parametrize( 'start,pos,handle,expected', [[(10, 10), (5, 5), 'crop_handle_topleft', (5, 15, 35, 45)], @@ -829,7 +844,8 @@ def test_ensure_point_within_crop_bounds( [(25, 10), (20, 5), 'crop_edge_top', (10, 15, 30, 45)], [(10, 40), (5, 35), 'crop_edge_left', (5, 20, 35, 40)], [(35, 25), (30, 20), 'crop_edge_bottom', (10, 20, 30, 35)], - [(40, 40), (35, 35), 'crop_edge_right', (10, 20, 25, 40)]]) + [(40, 40), (35, 35), 'crop_edge_right', (10, 20, 25, 40)], + [(15, 30), (5, 10), 'crop_temp', (0, 0, 30, 40)]]) @patch('beeref.selection.SelectableMixin.mouseMoveEvent') def test_mouse_move_when_crop_mode_inside_handle( mouse_mock, start, pos, handle, expected, qapp, item): diff --git a/tests/test_scene.py b/tests/test_scene.py index 6bbed5a..ee39ad8 100644 --- a/tests/test_scene.py +++ b/tests/test_scene.py @@ -22,7 +22,7 @@ def test_cancel_crop_mode_when_crop(view, item): view.scene.crop_item = item item.exit_crop_mode = MagicMock() view.scene.cancel_crop_mode() - item.exit_crop_mode.assert_called_once_with(confirm=False) + item.exit_crop_mode.assert_called_once_with(confirm=True) def test_cancel_crop_mode_when_no_crop(view, item):