mirror of
https://github.com/rbreu/beeref.git
synced 2026-03-11 08:54:28 +00:00
Add handle-free center so that small items can always be moved
This commit is contained in:
parent
f0aa1e86f7
commit
333515913a
3 changed files with 96 additions and 27 deletions
|
|
@ -14,6 +14,8 @@ Fixed
|
|||
-----
|
||||
|
||||
* Various typos (by luzpaz)
|
||||
* Ensure that small items always have an area in the middle for
|
||||
moving/editing that doesn't trigger transform actions
|
||||
|
||||
|
||||
0.2.0 - 2021-09-06
|
||||
|
|
|
|||
|
|
@ -126,6 +126,7 @@ class SelectableMixin(BaseItemMixin):
|
|||
SELECT_HANDLE_SIZE = 15 # size of selection handles for scaling
|
||||
SELECT_RESIZE_SIZE = 20 # size of hover area for scaling
|
||||
SELECT_ROTATE_SIZE = 10 # size of hover area for rotating
|
||||
SELECT_FREE_CENTER = 20 # size of handle-free area in the center
|
||||
|
||||
def init_selectable(self):
|
||||
self.setAcceptHoverEvents(True)
|
||||
|
|
@ -170,6 +171,18 @@ class SelectableMixin(BaseItemMixin):
|
|||
def select_rotate_size(self):
|
||||
return self.fixed_length_for_viewport(self.SELECT_ROTATE_SIZE)
|
||||
|
||||
def select_handle_free_center(self):
|
||||
"""This area should always trigger regular move operations,
|
||||
even if it is covered by selection scale/flip/... handles.
|
||||
This ensures that small items can always still be moved/edited.
|
||||
"""
|
||||
size = self.fixed_length_for_viewport(self.SELECT_FREE_CENTER)
|
||||
return QtCore.QRectF(
|
||||
self.center.x() - size/2,
|
||||
self.center.y() - size/2,
|
||||
size,
|
||||
size)
|
||||
|
||||
def draw_debug_shape(self, painter, shape, r, g, b):
|
||||
color = QtGui.QColor(r, g, b, 50)
|
||||
if isinstance(shape, QtCore.QRectF):
|
||||
|
|
@ -190,6 +203,8 @@ class SelectableMixin(BaseItemMixin):
|
|||
painter, self.get_rotate_bounds(corner), 0, 255, 255)
|
||||
for edge in self.get_flip_bounds():
|
||||
self.draw_debug_shape(painter, edge['rect'], 255, 255, 0)
|
||||
self.draw_debug_shape(
|
||||
painter, self.select_handle_free_center(), 255, 0, 255)
|
||||
|
||||
def paint_selectable(self, painter, option, widget):
|
||||
self.paint_debug(painter, option, widget)
|
||||
|
|
@ -345,6 +360,13 @@ class SelectableMixin(BaseItemMixin):
|
|||
if not self.has_selection_handles():
|
||||
return
|
||||
|
||||
if event.pos() in self.select_handle_free_center():
|
||||
# This area should always trigger regular move operations,
|
||||
# even if it is covered by selection scale/flip/... handles.
|
||||
# This ensures that small items can always still be moved/edited.
|
||||
self.setCursor(Qt.CursorShape.ArrowCursor)
|
||||
return
|
||||
|
||||
for corner in self.corners:
|
||||
# See if we need to change the cursor for interactable areas
|
||||
if self.get_scale_bounds(corner).contains(event.pos()):
|
||||
|
|
@ -373,6 +395,18 @@ class SelectableMixin(BaseItemMixin):
|
|||
self.scene().views()[0].reset_previous_transform(toggle_item=self)
|
||||
if not self.isSelected():
|
||||
self.just_selected = True
|
||||
super().mousePressEvent(event)
|
||||
return
|
||||
|
||||
self.just_selected = False
|
||||
|
||||
if event.pos() in self.select_handle_free_center():
|
||||
# This area should always trigger regular move operations,
|
||||
# even if it is covered by selection scale/flip/... handles.
|
||||
# This ensures that small items can always still be moved/edited.
|
||||
super().mousePressEvent(event)
|
||||
return
|
||||
|
||||
if (event.button() == Qt.MouseButton.LeftButton
|
||||
and self.has_selection_handles()):
|
||||
for corner in self.corners:
|
||||
|
|
|
|||
|
|
@ -75,6 +75,16 @@ def test_rotate_size_when_scaled(view, item):
|
|||
assert item.select_rotate_size == 25
|
||||
|
||||
|
||||
def test_select_handle_free_center(view, item):
|
||||
view.scene.addItem(item)
|
||||
view.scale(0.5, 0.5)
|
||||
item.SELECT_FREE_CENTER = 10
|
||||
with patch.object(item, 'bounding_rect_unselected',
|
||||
return_value=QtCore.QRectF(0, 0, 100, 80)):
|
||||
assert item.select_handle_free_center() == QtCore.QRectF(
|
||||
40, 30, 20, 20)
|
||||
|
||||
|
||||
def test_draw_debug_shape_rect(view, item):
|
||||
view.scene.addItem(item)
|
||||
painter = MagicMock()
|
||||
|
|
@ -605,6 +615,18 @@ def test_hover_move_event_no_selection(view, item):
|
|||
item.setCursor.assert_not_called()
|
||||
|
||||
|
||||
def test_hover_move_event_small_item_inside_handle_free_center(view, item):
|
||||
view.scene.addItem(item)
|
||||
item.setSelected(True)
|
||||
event = MagicMock()
|
||||
event.pos.return_value = QtCore.QPointF(10, 10)
|
||||
item.setCursor = MagicMock()
|
||||
with patch.object(item, 'bounding_rect_unselected',
|
||||
return_value=QtCore.QRectF(0, 0, 20, 20)):
|
||||
item.hoverMoveEvent(event)
|
||||
item.setCursor.assert_called_once_with(Qt.CursorShape.ArrowCursor)
|
||||
|
||||
|
||||
@mark.parametrize('pos,flipped,rotation, expected',
|
||||
[((0, 0), False, 0, 'SizeFDiagCursor'),
|
||||
((100, 80), False, 0, 'SizeFDiagCursor'),
|
||||
|
|
@ -735,7 +757,7 @@ def test_hover_move_event_not_in_handles(view, item):
|
|||
event = MagicMock()
|
||||
event.pos.return_value = QtCore.QPointF(50, 50)
|
||||
with patch.object(item, 'bounding_rect_unselected',
|
||||
return_value=QtCore.QRectF(0, 0, 100, 80)):
|
||||
return_value=QtCore.QRectF(0, 0, 1000, 800)):
|
||||
item.hoverMoveEvent(event)
|
||||
assert item.cursor() == Qt.CursorShape.ArrowCursor
|
||||
|
||||
|
|
@ -759,23 +781,46 @@ def test_hover_enter_event_when_not_selected(view, item):
|
|||
|
||||
def test_mouse_press_event_just_selected(view, item):
|
||||
view.scene.addItem(item)
|
||||
item.just_selected = False
|
||||
event = MagicMock()
|
||||
event.pos.return_value = QtCore.QPointF(-100, -100)
|
||||
with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.mousePressEvent'):
|
||||
item.mousePressEvent(event)
|
||||
event.pos.return_value = QtCore.QPointF(0, 0)
|
||||
event.button.return_value = Qt.MouseButton.LeftButton
|
||||
with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.mousePressEvent') as m:
|
||||
with patch.object(item, 'bounding_rect_unselected',
|
||||
return_value=QtCore.QRectF(0, 0, 100, 80)):
|
||||
item.mousePressEvent(event)
|
||||
assert item.just_selected is True
|
||||
event.accept.assert_not_called()
|
||||
m.assert_called_once_with(event)
|
||||
|
||||
|
||||
def test_mouse_press_event_previously_selected(view, item):
|
||||
view.scene.addItem(item)
|
||||
item.setSelected(True)
|
||||
item.just_selected = True
|
||||
event = MagicMock()
|
||||
event.pos.return_value = QtCore.QPointF(-100, -100)
|
||||
with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.mousePressEvent'):
|
||||
event.pos.return_value = QtCore.QPointF(0, 0)
|
||||
event.scenePos.return_value = QtCore.QPointF(-1, -1)
|
||||
event.button.return_value = Qt.MouseButton.LeftButton
|
||||
with patch.object(item, 'bounding_rect_unselected',
|
||||
return_value=QtCore.QRectF(0, 0, 100, 80)):
|
||||
item.mousePressEvent(event)
|
||||
assert item.just_selected is False
|
||||
|
||||
|
||||
def test_mouse_press_event_small_item_inside_handle_free_center(view, item):
|
||||
view.scene.addItem(item)
|
||||
item.setSelected(True)
|
||||
event = MagicMock()
|
||||
event.pos.return_value = QtCore.QPointF(10, 10)
|
||||
event.button.return_value = Qt.MouseButton.LeftButton
|
||||
with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.mousePressEvent') as m:
|
||||
with patch.object(item, 'bounding_rect_unselected',
|
||||
return_value=QtCore.QRectF(0, 0, 20, 20)):
|
||||
item.mousePressEvent(event)
|
||||
|
||||
event.accept.assert_not_called()
|
||||
m.assert_called_once_with(event)
|
||||
|
||||
|
||||
def test_mouse_press_event_topleft_scale(view, item):
|
||||
|
|
@ -785,13 +830,15 @@ def test_mouse_press_event_topleft_scale(view, item):
|
|||
event.pos.return_value = QtCore.QPointF(2, 2)
|
||||
event.scenePos.return_value = QtCore.QPointF(-1, -1)
|
||||
event.button.return_value = Qt.MouseButton.LeftButton
|
||||
item.mousePressEvent(event)
|
||||
assert item.scale_active is True
|
||||
assert item.event_start == QtCore.QPointF(-1, -1)
|
||||
assert item.event_direction.x() < 0
|
||||
assert item.event_direction.y() < 0
|
||||
assert item.scale_orig_factor == 1
|
||||
event.accept.assert_called_once_with()
|
||||
with patch.object(item, 'bounding_rect_unselected',
|
||||
return_value=QtCore.QRectF(0, 0, 100, 80)):
|
||||
item.mousePressEvent(event)
|
||||
assert item.scale_active is True
|
||||
assert item.event_start == QtCore.QPointF(-1, -1)
|
||||
assert item.event_direction.x() < 0
|
||||
assert item.event_direction.y() < 0
|
||||
assert item.scale_orig_factor == 1
|
||||
event.accept.assert_called_once_with()
|
||||
|
||||
|
||||
def test_mouse_press_event_bottomright_scale(view, item):
|
||||
|
|
@ -850,20 +897,6 @@ def test_mouse_press_event_flip(view, item):
|
|||
event.accept.assert_called_once_with()
|
||||
|
||||
|
||||
def test_mouse_press_event_not_selected(view, item):
|
||||
view.scene.addItem(item)
|
||||
view.reset_previous_transform = MagicMock()
|
||||
item.setSelected(False)
|
||||
event = MagicMock()
|
||||
with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.mousePressEvent') as m:
|
||||
item.mousePressEvent(event)
|
||||
m.assert_called_once_with(event)
|
||||
assert item.scale_active is False
|
||||
assert item.rotate_active is False
|
||||
assert item.flip_active is False
|
||||
event.accept.assert_not_called()
|
||||
|
||||
|
||||
def test_mouse_press_event_not_in_handles(view, item):
|
||||
view.scene.addItem(item)
|
||||
view.reset_previous_transform = MagicMock()
|
||||
|
|
|
|||
Loading…
Reference in a new issue