From 90067f96abbfe2631ffdb8c434c8de67fb11706c Mon Sep 17 00:00:00 2001 From: Rebecca Breu Date: Wed, 14 Apr 2021 13:26:35 +0200 Subject: [PATCH] Add Fit Scene, Fit Selection and Fit Image (via doubleclick) --- beeref/actions/actions.py | 14 +++++++ beeref/actions/menu_structure.py | 9 ++++- beeref/scene.py | 10 +++++ beeref/selection.py | 3 ++ beeref/view.py | 33 ++++++++++++++++ tests/test_scene.py | 29 ++++++++++++++ tests/test_view.py | 66 +++++++++++++++++++++++++++++++- 7 files changed, 162 insertions(+), 2 deletions(-) diff --git a/beeref/actions/actions.py b/beeref/actions/actions.py index 5d394b0..a88a348 100644 --- a/beeref/actions/actions.py +++ b/beeref/actions/actions.py @@ -120,4 +120,18 @@ actions = [ 'shortcuts': ['Ctrl+N'], 'callback': 'clear_scene', }, + { + 'id': 'fit_scene', + 'text': '&Fit Scene', + 'shortcuts': ['1'], + 'callback': 'on_action_fit_scene', + }, + { + 'id': 'fit_selection', + 'text': 'Fit &Selection', + 'shortcuts': ['2'], + 'callback': 'on_action_fit_selection', + 'group': 'active_when_selection', + 'enabled': False, + }, ] diff --git a/beeref/actions/menu_structure.py b/beeref/actions/menu_structure.py index 662e646..fbbf146 100644 --- a/beeref/actions/menu_structure.py +++ b/beeref/actions/menu_structure.py @@ -28,7 +28,6 @@ menu_structure = [ 'quit', ], }, - 'insert_images', { 'menu': '&Edit', 'items': [ @@ -38,6 +37,14 @@ menu_structure = [ 'delete', ], }, + { + 'menu': '&View', + 'items': [ + 'fit_scene', + 'fit_selection', + ], + }, + 'insert_images', { 'menu': '&Transform', 'items': [ diff --git a/beeref/scene.py b/beeref/scene.py index 5e0f9e6..328ea25 100644 --- a/beeref/scene.py +++ b/beeref/scene.py @@ -125,6 +125,15 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene): super().mousePressEvent(event) + def mouseDoubleClickEvent(self, event): + item = self.itemAt(event.scenePos(), self.views()[0].transform()) + if item: + self.move_active = False + self.views()[0].fit_rect(self.get_selection_rect(), + toggle_item=item) + return + super().mouseDoubleClickEvent(event) + def mouseMoveEvent(self, event): if self.rubberband_active: if not self.rubberband_item.scene(): @@ -133,6 +142,7 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene): self.rubberband_item.bring_to_front() self.rubberband_item.fit(self.event_start, event.scenePos()) self.setSelectionArea(self.rubberband_item.shape()) + self.views()[0].reset_previous_transform() super().mouseMoveEvent(event) def mouseReleaseEvent(self, event): diff --git a/beeref/selection.py b/beeref/selection.py index 38e5220..be55f80 100644 --- a/beeref/selection.py +++ b/beeref/selection.py @@ -341,6 +341,7 @@ class SelectableMixin(BaseItemMixin): self.setCursor(Qt.CursorShape.ArrowCursor) def mousePressEvent(self, event): + self.scene().views()[0].reset_previous_transform(toggle_item=self) if not self.isSelected(): self.just_selected = True if (event.button() == Qt.MouseButtons.LeftButton @@ -430,6 +431,8 @@ class SelectableMixin(BaseItemMixin): return edge['flip_v'] def mouseMoveEvent(self, event): + self.scene().views()[0].reset_previous_transform() + if self.scale_active: factor = self.get_scale_factor(event) for item in self.selection_action_items(): diff --git a/beeref/view.py b/beeref/view.py index fd365dc..6ec8536 100644 --- a/beeref/view.py +++ b/beeref/view.py @@ -58,6 +58,7 @@ class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin): # self.setTransformationAnchor( # QtWidgets.QGraphicsView.ViewportAnchor.AnchorUnderMouse) + self.previous_transform = None self.pan_active = False self.scene.changed.connect(self.on_scene_changed) self.scene.selectionChanged.connect(self.on_selection_changed) @@ -110,6 +111,37 @@ class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin): self.filename = None self.setTransform(QtGui.QTransform()) + def reset_previous_transform(self, toggle_item=None): + if (self.previous_transform + and self.previous_transform['toggle_item'] != toggle_item): + self.previous_transform = None + + def fit_rect(self, rect, toggle_item=None): + if toggle_item and self.previous_transform: + logger.debug('Fit view: Reset to previous') + self.setTransform(self.previous_transform['transform']) + self.horizontalScrollBar().setValue( + self.previous_transform['hscroll']) + self.verticalScrollBar().setValue( + self.previous_transform['vscroll']) + self.previous_transform = None + return + if toggle_item: + self.previous_transform = { + 'toggle_item': toggle_item, + 'transform': QtGui.QTransform(self.transform()), + 'hscroll': self.horizontalScrollBar().value(), + 'vscroll': self.verticalScrollBar().value(), + } + logger.debug(f'Fit view: {rect}') + self.fitInView(rect, Qt.AspectRatioMode.KeepAspectRatio) + + def on_action_fit_scene(self): + self.fit_rect(self.scene.itemsBoundingRect()) + + def on_action_fit_selection(self): + self.fit_rect(self.scene.get_selection_rect()) + def on_action_undo(self): logger.debug('Undo: %s' % self.undo_stack.undoText()) self.undo_stack.undo() @@ -334,6 +366,7 @@ class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin): def mouseMoveEvent(self, event): if self.pan_active: + self.reset_previous_transform() point = event.position() hscroll = self.horizontalScrollBar() hscroll.setValue(hscroll.value() + self.pan_start.x() - point.x()) diff --git a/tests/test_scene.py b/tests/test_scene.py index 317a4d0..bc3ef95 100644 --- a/tests/test_scene.py +++ b/tests/test_scene.py @@ -174,6 +174,35 @@ class BeeGraphicsSceneTestCase(BeeTestCase): assert self.scene.rubberband_active is True assert self.scene.event_start == QtCore.QPointF(10, 20) + @patch('PyQt6.QtWidgets.QGraphicsScene.mouseDoubleClickEvent') + def test_mouse_doubleclick_event_when_over_item(self, mouse_mock): + event = MagicMock() + self.scene.move_active = True + item = BeePixmapItem(QtGui.QImage()) + self.scene.addItem(item) + item.setPos(30, 40) + item.setSelected(True) + self.scene.itemAt = MagicMock(return_value=item) + + with patch('beeref.items.BeePixmapItem.width', + new_callable=PropertyMock, return_value=100): + with patch('beeref.items.BeePixmapItem.height', + new_callable=PropertyMock, return_value=100): + self.scene.mouseDoubleClickEvent(event) + + assert self.scene.move_active is False + self.view.fit_rect.assert_called_once_with( + QtCore.QRectF(30, 40, 100, 100), toggle_item=item) + mouse_mock.assert_not_called() + + @patch('PyQt6.QtWidgets.QGraphicsScene.mouseDoubleClickEvent') + def test_mouse_doubleclick_event_when_not_over_item(self, mouse_mock): + event = MagicMock() + self.scene.itemAt = MagicMock(return_value=None) + self.scene.mouseDoubleClickEvent(event) + self.view.fit_rect.assert_not_called() + mouse_mock.assert_called_once_with(event) + @patch('PyQt6.QtWidgets.QGraphicsScene.mouseMoveEvent') def test_mouse_move_event_when_rubberband_new(self, mouse_mock): item = BeePixmapItem(QtGui.QImage(self.imgfilename3x3)) diff --git a/tests/test_view.py b/tests/test_view.py index ec511bc..62b83dd 100644 --- a/tests/test_view.py +++ b/tests/test_view.py @@ -4,7 +4,8 @@ from unittest.mock import MagicMock, patch from pytest import mark -from PyQt6 import QtGui, QtWidgets +from PyQt6 import QtCore, QtGui, QtWidgets +from PyQt6.QtCore import Qt from beeref.items import BeePixmapItem from beeref import fileio @@ -84,6 +85,69 @@ class BeeGraphicsViewTestCase(BeeTestCase): assert self.view.filename is None self.view.undo_stack.clear.assert_called_once_with() + def test_reset_previous_transform_when_other_item(self): + item1 = MagicMock() + item2 = MagicMock() + self.view.previous_transform = { + 'transform': 'foo', + 'toggle_item': item1, + } + self.view.reset_previous_transform(toggle_item=item2) + assert self.view.previous_transform is None + + def test_reset_previous_transform_when_same_item(self): + item = MagicMock() + self.view.previous_transform = { + 'transform': 'foo', + 'toggle_item': item, + } + self.view.reset_previous_transform(toggle_item=item) + assert self.view.previous_transform == { + 'transform': 'foo', + 'toggle_item': item, + } + + @patch('beeref.view.BeeGraphicsView.fitInView') + def test_fit_rect_no_toggle(self, fit_mock): + rect = QtCore.QRectF(30, 40, 100, 80) + self.view.fit_rect(rect) + fit_mock.assert_called_once_with( + rect, Qt.AspectRatioMode.KeepAspectRatio) + + @patch('beeref.view.BeeGraphicsView.fitInView') + def test_fit_rect_toggle_when_no_previous(self, fit_mock): + item = MagicMock() + self.view.previous_transform = None + self.view.setSceneRect(QtCore.QRectF(-2000, -2000, 4000, 4000)) + rect = QtCore.QRectF(30, 40, 100, 80) + self.view.scale(2, 2) + self.view.horizontalScrollBar().setValue(-40) + self.view.verticalScrollBar().setValue(-50) + self.view.fit_rect(rect, toggle_item=item) + fit_mock.assert_called_once_with( + rect, Qt.AspectRatioMode.KeepAspectRatio) + assert self.view.previous_transform['toggle_item'] == item + assert self.view.previous_transform['transform'].m11() == 2 + assert self.view.previous_transform['hscroll'] == -40 + assert self.view.previous_transform['vscroll'] == -50 + + @patch('beeref.view.BeeGraphicsView.fitInView') + def test_fit_rect_toggle_when_previous(self, fit_mock): + item = MagicMock() + self.view.previous_transform = { + 'toggle_item': item, + 'transform': QtGui.QTransform.fromScale(2, 2), + 'hscroll': -40, + 'vscroll': -50, + } + self.view.setSceneRect(QtCore.QRectF(-2000, -2000, 4000, 4000)) + rect = QtCore.QRectF(30, 40, 100, 80) + self.view.fit_rect(rect, toggle_item=item) + fit_mock.assert_not_called() + assert self.view.get_scale() == 2 + self.view.horizontalScrollBar().value == -40 + self.view.verticalScrollBar().value == -50 + @patch('beeref.view.BeeGraphicsView.clear_scene') def test_open_from_file(self, clear_mock): root = os.path.dirname(__file__)