This commit is contained in:
DarkDefender 2024-06-09 12:58:07 +00:00 committed by GitHub
commit 66e1ea8331
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 88 additions and 23 deletions

View file

@ -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
-----

View file

@ -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):
@ -539,9 +542,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 within 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 +585,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 +622,39 @@ class BeePixmapItem(BeeItemMixin, QtWidgets.QGraphicsPixmapItem):
def mouseMoveEvent(self, event):
if self.crop_mode and self.crop_mode_event_start:
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())

View file

@ -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:

View file

@ -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):

View file

@ -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):