mirror of
https://github.com/rbreu/beeref.git
synced 2026-03-11 08:54:28 +00:00
Add ability to zoom with ctrl + middle mouse drag
This commit is contained in:
parent
eee27fa441
commit
d25f911561
3 changed files with 292 additions and 56 deletions
|
|
@ -2,13 +2,21 @@
|
|||
|
||||
<p>
|
||||
Middle Click + Drag<br/>
|
||||
Left Click + Alt + Drag
|
||||
Alt + Left Click + Drag
|
||||
</p>
|
||||
|
||||
<h4>Zoom Canvas</h4>
|
||||
|
||||
<p>
|
||||
Mouse wheel up/down
|
||||
Mouse wheel up/down<br/>
|
||||
Ctrl + Middle Click + Drag
|
||||
</p>
|
||||
|
||||
<h4>Zoom in on Item</h4>
|
||||
|
||||
<p>
|
||||
Double-click on item<br/>
|
||||
Double-click again to get back to previous view
|
||||
</p>
|
||||
|
||||
<h4>Select Items</h4>
|
||||
|
|
@ -30,10 +38,3 @@
|
|||
<p>
|
||||
Shift or Ctrl while rotating
|
||||
</p>
|
||||
|
||||
<h4>Zoom in on Item</h4>
|
||||
|
||||
<p>
|
||||
Double-click on item<br/>
|
||||
Double-click again to get back to previous view
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -40,8 +40,6 @@ class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin):
|
|||
self.app = app
|
||||
self.settings = BeeSettings()
|
||||
self.setBackgroundBrush(QtGui.QBrush(QtGui.QColor(60, 60, 60)))
|
||||
self.setTransformationAnchor(
|
||||
QtWidgets.QGraphicsView.ViewportAnchor.AnchorUnderMouse)
|
||||
|
||||
self.undo_stack = QtGui.QUndoStack(self)
|
||||
self.undo_stack.setUndoLimit(100)
|
||||
|
|
@ -57,6 +55,7 @@ class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin):
|
|||
|
||||
self.previous_transform = None
|
||||
self.pan_active = False
|
||||
self.zoom_active = False
|
||||
self.scene.changed.connect(self.on_scene_changed)
|
||||
self.scene.selectionChanged.connect(self.on_selection_changed)
|
||||
|
||||
|
|
@ -473,6 +472,7 @@ class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin):
|
|||
self.setSceneRect(QtCore.QRectF(topleft, bottomright))
|
||||
except OverflowError:
|
||||
logger.info('Maximum scene size reached')
|
||||
logger.debug('Done recalculating scene rectangle')
|
||||
|
||||
def get_zoom_size(self, func):
|
||||
"""Calculates the size of all items' bounding box in the view's
|
||||
|
|
@ -501,60 +501,102 @@ class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin):
|
|||
def get_scale(self):
|
||||
return self.transform().m11()
|
||||
|
||||
def wheelEvent(self, event):
|
||||
def pan(self, delta):
|
||||
if not self.scene.items():
|
||||
logger.debug('No items in scene; ignore pan')
|
||||
return
|
||||
|
||||
hscroll = self.horizontalScrollBar()
|
||||
hscroll.setValue(hscroll.value() + delta.x())
|
||||
vscroll = self.verticalScrollBar()
|
||||
vscroll.setValue(vscroll.value() + delta.y())
|
||||
|
||||
def zoom(self, delta, anchor):
|
||||
if not self.scene.items():
|
||||
logger.debug('No items in scene; ignore zoom')
|
||||
return
|
||||
|
||||
factor = 1 + abs(event.angleDelta().y() / 1000)
|
||||
if event.angleDelta().y() > 0:
|
||||
# We caculate where the anchor is before and after the zoom
|
||||
# and then move the view accordingly to keep the anchor fixed
|
||||
# We can't use QGraphicsView's AnchorUnderMouse since it
|
||||
# uses the current cursor position while we need the inital mouse
|
||||
# press position for zooming with Ctrl + Middle Drag
|
||||
anchor = QtCore.QPoint(round(anchor.x()),
|
||||
round(anchor.y()))
|
||||
ref_point = self.mapToScene(anchor)
|
||||
if delta == 0:
|
||||
return
|
||||
factor = 1 + abs(delta / 1000)
|
||||
if delta > 0:
|
||||
if self.get_zoom_size(max) < 10000000:
|
||||
self.scale(factor, factor)
|
||||
else:
|
||||
logger.debug('Maximum zoom size reached')
|
||||
return
|
||||
else:
|
||||
if self.get_zoom_size(min) > 50:
|
||||
self.scale(1/factor, 1/factor)
|
||||
else:
|
||||
logger.debug('Minimum zoom size reached')
|
||||
return
|
||||
|
||||
self.pan(self.mapFromScene(ref_point) - anchor)
|
||||
self.reset_previous_transform()
|
||||
|
||||
def wheelEvent(self, event):
|
||||
self.zoom(event.angleDelta().y(), event.position())
|
||||
event.accept()
|
||||
|
||||
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())
|
||||
vscroll = self.verticalScrollBar()
|
||||
vscroll.setValue(vscroll.value() + self.pan_start.y() - point.y())
|
||||
self.pan_start = point
|
||||
event.accept()
|
||||
return
|
||||
|
||||
super().mouseMoveEvent(event)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if (event.button() == Qt.MouseButton.MiddleButton
|
||||
and event.modifiers() == Qt.KeyboardModifier.ControlModifier):
|
||||
self.zoom_active = True
|
||||
self.event_start = event.position()
|
||||
self.event_anchor = event.position()
|
||||
event.accept()
|
||||
return
|
||||
|
||||
if (event.button() == Qt.MouseButton.MiddleButton
|
||||
or (event.button() == Qt.MouseButton.LeftButton
|
||||
and event.modifiers() == Qt.KeyboardModifier.AltModifier)):
|
||||
if not self.scene.items():
|
||||
logger.debug('No items in scene; ignore pan')
|
||||
return
|
||||
|
||||
self.pan_active = True
|
||||
self.pan_start = event.position()
|
||||
self.event_start = event.position()
|
||||
self.setCursor(Qt.CursorShape.ClosedHandCursor)
|
||||
event.accept()
|
||||
return
|
||||
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
if self.pan_active:
|
||||
self.reset_previous_transform()
|
||||
pos = event.position()
|
||||
self.pan(self.event_start - pos)
|
||||
self.event_start = pos
|
||||
event.accept()
|
||||
return
|
||||
|
||||
if self.zoom_active:
|
||||
self.reset_previous_transform()
|
||||
pos = event.position()
|
||||
delta = (self.event_start - pos).y()
|
||||
self.event_start = pos
|
||||
self.zoom(delta * 20, self.event_anchor)
|
||||
event.accept()
|
||||
return
|
||||
|
||||
super().mouseMoveEvent(event)
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if self.pan_active:
|
||||
self.setCursor(Qt.CursorShape.ArrowCursor)
|
||||
self.pan_active = False
|
||||
event.accept()
|
||||
return
|
||||
if self.zoom_active:
|
||||
self.zoom_active = False
|
||||
event.accept()
|
||||
return
|
||||
|
||||
super().mouseReleaseEvent(event)
|
||||
|
||||
|
|
|
|||
|
|
@ -415,41 +415,236 @@ class UpdateWindowTitleTestCase(ViewBaseTestCase):
|
|||
self.view.update_window_title()
|
||||
assert self.parent.windowTitle() == 'test.bee* - BeeRef'
|
||||
|
||||
@patch('beeref.view.BeeGraphicsView.recalc_scene_rect')
|
||||
@patch('beeref.scene.BeeGraphicsScene.on_view_scale_change')
|
||||
def test_scale(self, view_scale_mock, recalc_mock):
|
||||
self.view.scale(3.3, 3.3)
|
||||
view_scale_mock.assert_called_once_with()
|
||||
recalc_mock.assert_called_once_with()
|
||||
assert self.view.get_scale() == 3.3
|
||||
|
||||
@patch('PyQt6.QtWidgets.QScrollBar.setValue')
|
||||
def test_pan(self, scroll_value_mock):
|
||||
item = BeePixmapItem(QtGui.QImage())
|
||||
self.view.scene.addItem(item)
|
||||
self.view.pan(QtCore.QPointF(5, 10))
|
||||
assert scroll_value_mock.call_count == 2
|
||||
|
||||
@patch('PyQt6.QtWidgets.QScrollBar.setValue')
|
||||
def test_pan_when_no_items(self, scroll_value_mock):
|
||||
self.view.pan(QtCore.QPointF(5, 10))
|
||||
scroll_value_mock.assert_not_called()
|
||||
|
||||
|
||||
class ZoomTestCase(ViewBaseTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
pan_patcher = patch('beeref.view.BeeGraphicsView.pan')
|
||||
self.pan_mock = pan_patcher.start()
|
||||
self.addCleanup(pan_patcher.stop)
|
||||
reset_patcher = patch(
|
||||
'beeref.view.BeeGraphicsView.reset_previous_transform')
|
||||
self.reset_mock = reset_patcher.start()
|
||||
self.addCleanup(reset_patcher.stop)
|
||||
self.item = BeePixmapItem(QtGui.QImage(self.imgfilename3x3))
|
||||
|
||||
def test_zoom_in(self):
|
||||
self.view.scene.addItem(self.item)
|
||||
self.view.zoom(40, QtCore.QPointF(10, 10))
|
||||
assert self.view.get_scale() == 1.04
|
||||
self.reset_mock.assert_called_once_with()
|
||||
self.pan_mock.assert_called_once_with(QtCore.QPoint(-52, -15))
|
||||
|
||||
def test_zoom_in_max_zoom_size(self):
|
||||
self.view.scale(10000000, 10000000)
|
||||
self.view.scene.addItem(self.item)
|
||||
self.view.zoom(40, QtCore.QPointF(10, 10))
|
||||
assert self.view.get_scale() == 10000000
|
||||
self.reset_mock.assert_not_called()
|
||||
self.pan_mock.assert_not_called()
|
||||
|
||||
def test_zoom_out(self):
|
||||
self.view.scale(100, 100)
|
||||
self.view.scene.addItem(self.item)
|
||||
self.view.zoom(-40, QtCore.QPointF(10, 10))
|
||||
assert self.view.get_scale() == 100 / 1.04
|
||||
self.reset_mock.assert_called_once_with()
|
||||
self.pan_mock.assert_called_once_with(QtCore.QPoint(49, 14))
|
||||
|
||||
def test_zoom_out_min_zoom_size(self):
|
||||
self.view.scene.addItem(self.item)
|
||||
self.view.zoom(-40, QtCore.QPointF(10, 10))
|
||||
assert self.view.get_scale() == 1
|
||||
self.reset_mock.assert_not_called()
|
||||
self.pan_mock.assert_not_called()
|
||||
|
||||
def test_no_items(self):
|
||||
self.view.zoom(40, QtCore.QPointF(10, 10))
|
||||
assert self.view.get_scale() == 1
|
||||
self.reset_mock.assert_not_called()
|
||||
self.pan_mock.assert_not_called()
|
||||
|
||||
def test_delta_zero(self):
|
||||
self.view.scene.addItem(self.item)
|
||||
self.view.zoom(0, QtCore.QPointF(10, 10))
|
||||
assert self.view.get_scale() == 1
|
||||
self.reset_mock.assert_not_called()
|
||||
self.pan_mock.assert_not_called()
|
||||
|
||||
|
||||
class MouseEventsTestCase(ViewBaseTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.event = MagicMock()
|
||||
|
||||
@patch('beeref.view.BeeGraphicsView.zoom')
|
||||
def test_wheel_event(self, zoom_mock):
|
||||
self.event.angleDelta.return_value = QtCore.QPointF(0, 40)
|
||||
self.event.position.return_value = QtCore.QPointF(10, 20)
|
||||
self.view.wheelEvent(self.event)
|
||||
zoom_mock.assert_called_once_with(40, QtCore.QPointF(10, 20))
|
||||
self.event.accept.assert_called_once_with()
|
||||
|
||||
@patch('PyQt6.QtWidgets.QGraphicsView.mousePressEvent')
|
||||
def test_mouse_press_zoom(self, mouse_event_mock):
|
||||
self.event.position.return_value = QtCore.QPointF(10, 20)
|
||||
self.event.button.return_value = Qt.MouseButton.MiddleButton
|
||||
self.event.modifiers.return_value = Qt.KeyboardModifier.ControlModifier
|
||||
self.view.mousePressEvent(self.event)
|
||||
assert self.view.zoom_active is True
|
||||
assert self.view.pan_active is False
|
||||
assert self.view.event_start == QtCore.QPointF(10, 20)
|
||||
assert self.view.event_anchor == QtCore.QPointF(10, 20)
|
||||
mouse_event_mock.assert_not_called()
|
||||
self.event.accept.assert_called_once_with()
|
||||
|
||||
@patch('PyQt6.QtWidgets.QGraphicsView.mousePressEvent')
|
||||
def test_mouse_press_pan_middle_drag(self, mouse_event_mock):
|
||||
self.event.position.return_value = QtCore.QPointF(10, 20)
|
||||
self.event.button.return_value = Qt.MouseButton.MiddleButton
|
||||
self.event.modifiers.return_value = None
|
||||
self.view.mousePressEvent(self.event)
|
||||
assert self.view.pan_active is True
|
||||
assert self.view.zoom_active is False
|
||||
assert self.view.event_start == QtCore.QPointF(10, 20)
|
||||
mouse_event_mock.assert_not_called()
|
||||
self.view.cursor() == Qt.CursorShape.ClosedHandCursor
|
||||
self.event.accept.assert_called_once_with()
|
||||
|
||||
@patch('PyQt6.QtWidgets.QGraphicsView.mousePressEvent')
|
||||
def test_mouse_press_pan_alt_left_drag(self, mouse_event_mock):
|
||||
self.event.position.return_value = QtCore.QPointF(10, 20)
|
||||
self.event.button.return_value = Qt.MouseButton.LeftButton
|
||||
self.event.modifiers.return_value = Qt.KeyboardModifier.AltModifier
|
||||
self.view.mousePressEvent(self.event)
|
||||
assert self.view.pan_active is True
|
||||
assert self.view.zoom_active is False
|
||||
assert self.view.event_start == QtCore.QPointF(10, 20)
|
||||
mouse_event_mock.assert_not_called()
|
||||
self.view.cursor() == Qt.CursorShape.ClosedHandCursor
|
||||
self.event.accept.assert_called_once_with()
|
||||
|
||||
@patch('PyQt6.QtWidgets.QGraphicsView.mousePressEvent')
|
||||
def test_mouse_press_unhandled(self, mouse_event_mock):
|
||||
self.event.button.return_value = Qt.MouseButton.LeftButton
|
||||
self.event.modifiers.return_value = None
|
||||
self.view.mousePressEvent(self.event)
|
||||
assert self.view.pan_active is False
|
||||
assert self.view.zoom_active is False
|
||||
mouse_event_mock.assert_called_once_with(self.event)
|
||||
self.event.accept.assert_not_called()
|
||||
|
||||
@patch('PyQt6.QtWidgets.QGraphicsView.mouseMoveEvent')
|
||||
@patch('beeref.view.BeeGraphicsView.pan')
|
||||
def test_mouse_move_pan(self, pan_mock, mouse_event_mock):
|
||||
self.view.pan_active = True
|
||||
self.view.event_start = QtCore.QPointF(55, 66)
|
||||
self.event.position.return_value = QtCore.QPointF(10, 20)
|
||||
self.view.mouseMoveEvent(self.event)
|
||||
pan_mock.assert_called_once_with(QtCore.QPointF(45, 46))
|
||||
mouse_event_mock.assert_not_called()
|
||||
self.event.accept.assert_called_once_with()
|
||||
|
||||
@patch('PyQt6.QtWidgets.QGraphicsView.mouseMoveEvent')
|
||||
@patch('beeref.view.BeeGraphicsView.zoom')
|
||||
def test_mouse_move_zoom(self, zoom_mock, mouse_event_mock):
|
||||
self.view.zoom_active = True
|
||||
self.view.event_anchor = QtCore.QPointF(55, 66)
|
||||
self.view.event_start = QtCore.QPointF(10, 20)
|
||||
self.event.position.return_value = QtCore.QPointF(10, 18)
|
||||
self.view.mouseMoveEvent(self.event)
|
||||
zoom_mock.assert_called_once_with(40, QtCore.QPointF(55, 66))
|
||||
mouse_event_mock.assert_not_called()
|
||||
self.event.accept.assert_called_once_with()
|
||||
|
||||
@patch('PyQt6.QtWidgets.QGraphicsView.mouseMoveEvent')
|
||||
def test_mouse_move_unhandled(self, mouse_event_mock):
|
||||
self.event.position.return_value = QtCore.QPointF(10, 20)
|
||||
self.view.mouseMoveEvent(self.event)
|
||||
mouse_event_mock.assert_called_once_with(self.event)
|
||||
self.event.accept.assert_not_called()
|
||||
|
||||
@patch('PyQt6.QtWidgets.QGraphicsView.mouseReleaseEvent')
|
||||
def test_mouse_release_pan(self, mouse_event_mock):
|
||||
self.view.pan_active = True
|
||||
self.view.setCursor(Qt.CursorShape.ClosedHandCursor)
|
||||
self.view.mouseReleaseEvent(self.event)
|
||||
mouse_event_mock.assert_not_called()
|
||||
assert self.view.pan_active is False
|
||||
self.event.accept.assert_called_once_with()
|
||||
self.view.cursor() == Qt.CursorShape.ArrowCursor
|
||||
|
||||
@patch('PyQt6.QtWidgets.QGraphicsView.mouseReleaseEvent')
|
||||
def test_mouse_release_zoom(self, mouse_event_mock):
|
||||
self.view.zoom_active = True
|
||||
self.view.mouseReleaseEvent(self.event)
|
||||
mouse_event_mock.assert_not_called()
|
||||
assert self.view.zoom_active is False
|
||||
self.event.accept.assert_called_once_with()
|
||||
|
||||
@patch('PyQt6.QtWidgets.QGraphicsView.mouseReleaseEvent')
|
||||
def test_mouse_release_unhandled(self, mouse_event_mock):
|
||||
self.view.mouseReleaseEvent(self.event)
|
||||
mouse_event_mock.assert_called_once_with(self.event)
|
||||
self.event.accept.assert_not_called()
|
||||
|
||||
|
||||
class DragDropTestCase(ViewBaseTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.event = MagicMock()
|
||||
|
||||
def test_drag_enter_when_url(self):
|
||||
url = QtCore.QUrl()
|
||||
url.fromLocalFile(self.imgfilename3x3)
|
||||
mimedata = QtCore.QMimeData()
|
||||
mimedata.setUrls([url])
|
||||
event = MagicMock()
|
||||
event.mimeData.return_value = mimedata
|
||||
self.event.mimeData.return_value = mimedata
|
||||
|
||||
self.view.dragEnterEvent(event)
|
||||
event.acceptProposedAction.assert_called_once()
|
||||
self.view.dragEnterEvent(self.event)
|
||||
self.event.acceptProposedAction.assert_called_once()
|
||||
|
||||
def test_drag_enter_when_img(self):
|
||||
mimedata = QtCore.QMimeData()
|
||||
mimedata.setImageData(QtGui.QImage(self.imgfilename3x3))
|
||||
event = MagicMock()
|
||||
event.mimeData.return_value = mimedata
|
||||
self.event.mimeData.return_value = mimedata
|
||||
|
||||
self.view.dragEnterEvent(event)
|
||||
event.acceptProposedAction.assert_called_once()
|
||||
self.view.dragEnterEvent(self.event)
|
||||
self.event.acceptProposedAction.assert_called_once()
|
||||
|
||||
def test_drag_enter_when_unsupported(self):
|
||||
mimedata = QtCore.QMimeData()
|
||||
event = MagicMock()
|
||||
event.mimeData.return_value = mimedata
|
||||
self.event.mimeData.return_value = mimedata
|
||||
|
||||
self.view.dragEnterEvent(event)
|
||||
event.acceptProposedAction.assert_not_called()
|
||||
self.view.dragEnterEvent(self.event)
|
||||
self.event.acceptProposedAction.assert_not_called()
|
||||
|
||||
def test_drag_move(self):
|
||||
event = MagicMock()
|
||||
self.view.dragMoveEvent(event)
|
||||
event.acceptProposedAction.assert_called_once()
|
||||
self.view.dragMoveEvent(self.event)
|
||||
self.event.acceptProposedAction.assert_called_once()
|
||||
|
||||
@patch('beeref.view.BeeGraphicsView.do_insert_images')
|
||||
def test_drop_when_url(self, insert_mock):
|
||||
|
|
@ -457,20 +652,18 @@ class DragDropTestCase(ViewBaseTestCase):
|
|||
url.fromLocalFile(self.imgfilename3x3)
|
||||
mimedata = QtCore.QMimeData()
|
||||
mimedata.setUrls([url])
|
||||
event = MagicMock()
|
||||
event.mimeData.return_value = mimedata
|
||||
event.position.return_value = QtCore.QPointF(10, 20)
|
||||
self.event.mimeData.return_value = mimedata
|
||||
self.event.position.return_value = QtCore.QPointF(10, 20)
|
||||
|
||||
self.view.dropEvent(event)
|
||||
self.view.dropEvent(self.event)
|
||||
insert_mock.assert_called_once_with([url], QtCore.QPoint(10, 20))
|
||||
|
||||
def test_drop_when_img(self):
|
||||
mimedata = QtCore.QMimeData()
|
||||
mimedata.setImageData(QtGui.QImage(self.imgfilename3x3))
|
||||
event = MagicMock()
|
||||
event.mimeData.return_value = mimedata
|
||||
event.position.return_value = QtCore.QPointF(10, 20)
|
||||
self.event.mimeData.return_value = mimedata
|
||||
self.event.position.return_value = QtCore.QPointF(10, 20)
|
||||
|
||||
self.view.dropEvent(event)
|
||||
self.view.dropEvent(self.event)
|
||||
assert len(self.view.scene.items()) == 1
|
||||
assert self.view.scene.items()[0].isSelected() is True
|
||||
|
|
|
|||
Loading…
Reference in a new issue