Add hover areas for rotating items

This commit is contained in:
Rebecca Breu 2021-04-04 22:10:07 +02:00
parent 476c061071
commit 1ff957bca4
4 changed files with 140 additions and 53 deletions

View file

@ -23,6 +23,7 @@ from PyQt6.QtWidgets import QGraphicsItem
from beeref import commands
from beeref.config import CommandlineArgs
from beeref import utils
commandline_args = CommandlineArgs()
@ -133,21 +134,39 @@ class SelectableMixin(BaseItemMixin):
return [self.mapToScene(corner) for corner in self.corners]
def get_scale_bounds(self, center):
def get_scale_bounds(self, corner, margin=0):
"""The interactable shape of the scale handles."""
return QtCore.QRectF(
center.x() - self.select_resize_size/2,
center.y() - self.select_resize_size/2,
self.select_resize_size,
self.select_resize_size)
path = QtGui.QPainterPath()
path.addRect(QtCore.QRectF(
corner.x() - self.select_resize_size/2 - margin,
corner.y() - self.select_resize_size/2 - margin,
self.select_resize_size + 2 * margin,
self.select_resize_size + 2 * margin))
return path
@property
def bottom_right_rotate_bounds(self):
"""The interactable shape of the bottom right rotate handle"""
return QtCore.QRectF(
self.width + self.select_resize_size / 2,
self.height + self.select_resize_size / 2,
self.select_rotate_size, self.select_rotate_size)
def get_rotate_bounds(self, corner):
"""The interactable shape of the rotation area. It sits around the
scale area like an L shape, e.g. for the bottom right corner:
SR
R R
"""
path = QtGui.QPainterPath()
# The whole square containing the rotate area:
d = self.get_corner_direction(corner)
p1 = corner - d * self.select_resize_size / 2
p2 = p1 + d * (self.select_resize_size + self.select_rotate_size)
path.addRect(utils.get_rect_from_points(p1, p2))
# Substract the scale area:
# We need to make the substracted shape slighty bigger due to this bug:
# https://bugreports.qt.io/browse/QTBUG-57567
return path - self.get_scale_bounds(corner, margin=0.001)
def boundingRect(self):
bounds = super().boundingRect()
@ -163,15 +182,13 @@ class SelectableMixin(BaseItemMixin):
bounds.bottomRight().y() + 2 * margin)
def shape(self):
shape_ = super().shape()
path = super().shape()
if self.has_selection_handles():
# Add extra space for scale and rotate interactive areas
path = QtGui.QPainterPath()
for corner in self.corners:
path.addRect(self.get_scale_bounds(corner))
path.addRect(self.bottom_right_rotate_bounds)
shape_ = shape_ + path
return shape_
path += self.get_scale_bounds(corner)
path += self.get_rotate_bounds(corner)
return path
def hoverMoveEvent(self, event):
if not self.has_selection_handles():
@ -180,16 +197,15 @@ class SelectableMixin(BaseItemMixin):
for corner in self.corners:
# See if we need to change the cursor for scale areas
if self.get_scale_bounds(corner).contains(event.pos()):
direction = self.get_scale_direction(corner)
direction = self.get_corner_direction(corner)
if direction.x() == direction.y():
self.setCursor(Qt.CursorShape.SizeFDiagCursor)
else:
self.setCursor(Qt.CursorShape.SizeBDiagCursor)
return
if self.bottom_right_rotate_bounds.contains(event.pos()):
self.setCursor(Qt.CursorShape.ForbiddenCursor)
return
if self.get_rotate_bounds(corner).contains(event.pos()):
self.setCursor(Qt.CursorShape.ForbiddenCursor)
return
self.setCursor(Qt.CursorShape.ArrowCursor)
@ -207,7 +223,7 @@ class SelectableMixin(BaseItemMixin):
# Start scale action for this corner
self.scale_active = True
self.scale_start = event.scenePos()
self.scale_direction = self.get_scale_direction(corner)
self.scale_direction = self.get_corner_direction(corner)
for item in self.selection_action_items():
item.scale_anchor = self.get_scale_anchor(item, corner)
item.scale_orig_factor = item.scale()
@ -229,8 +245,9 @@ class SelectableMixin(BaseItemMixin):
return item.mapFromScene(
self.mapToScene(self.width - corner.x(), self.height - corner.y()))
def get_scale_direction(self, corner):
"""Get the direction in which the scale for this corner increases"""
def get_corner_direction(self, corner):
"""Get the direction facing away from the center, e.g. the direction
in which the scale for this corner increases."""
return QtCore.QPointF(1 if corner.x() > 0 else -1,
1 if corner.y() > 0 else -1)
@ -358,11 +375,5 @@ class RubberbandItem(BaseItemMixin, QtWidgets.QGraphicsRectItem):
def fit(self, point1, point2):
"""Updates itself to fit the two given points."""
topleft = QtCore.QPointF(
min(point1.x(), point2.x()),
min(point1.y(), point2.y()))
bottomright = QtCore.QPointF(
max(point1.x(), point2.x()),
max(point1.y(), point2.y()))
self.setRect(QtCore.QRectF(topleft, bottomright))
self.setRect(utils.get_rect_from_points(point1, point2))
logger.debug(f'Updated rubberband {self}')

30
beeref/utils.py Normal file
View file

@ -0,0 +1,30 @@
# This file is part of BeeRef.
#
# BeeRef is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BeeRef is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BeeRef. If not, see <https://www.gnu.org/licenses/>.
from PyQt6 import QtCore
def get_rect_from_points(point1, point2):
"""Constructs a QRectF from the given QPointF. The points can be *any*
two opposing corners of the rectangle."""
topleft = QtCore.QPointF(
min(point1.x(), point2.x()),
min(point1.y(), point2.y()))
bottomright = QtCore.QPointF(
max(point1.x(), point2.x()),
max(point1.y(), point2.y()))
return QtCore.QRectF(topleft, bottomright)

View file

@ -190,21 +190,44 @@ class SelectableMixinTestCase(SelectableMixinBaseTestCase):
def test_get_scale_bounds(self):
self.view.get_scale = MagicMock(return_value=1)
self.item.SELECT_RESIZE_SIZE = 10
rect = self.item.get_scale_bounds(QtCore.QPointF(100, 100))
rect = self.item.get_scale_bounds(
QtCore.QPointF(100, 80)).boundingRect()
assert rect.topLeft().x() == 95
assert rect.topLeft().y() == 95
assert rect.topLeft().y() == 75
assert rect.bottomRight().x() == 105
assert rect.bottomRight().y() == 105
assert rect.bottomRight().y() == 85
def test_bottom_right_rotate_bounds(self):
def test_get_scale_bounds_with_margin(self):
self.view.get_scale = MagicMock(return_value=1)
self.item.SELECT_RESIZE_SIZE = 10
rect = self.item.get_scale_bounds(
QtCore.QPointF(100, 80), margin=1).boundingRect()
assert rect.topLeft().x() == 94
assert rect.topLeft().y() == 74
assert rect.bottomRight().x() == 106
assert rect.bottomRight().y() == 86
def test_rotate_bounds_bottomright(self):
self.view.get_scale = MagicMock(return_value=1)
self.item.SELECT_RESIZE_SIZE = 10
self.item.SELECT_ROTATE_SIZE = 10
rect = self.item.bottom_right_rotate_bounds
assert rect.topLeft().x() == 105
assert rect.topLeft().y() == 85
assert rect.bottomRight().x() == 115
assert rect.bottomRight().y() == 95
path = self.item.get_rotate_bounds(QtCore.QPointF(100, 80))
assert path.boundingRect().topLeft().x() == 95
assert path.boundingRect().topLeft().y() == 75
assert path.boundingRect().bottomRight().x() == 115
assert path.boundingRect().bottomRight().y() == 95
assert path.contains(QtCore.QPointF(104, 84)) is False
def test_rotate_bounds_topleft(self):
self.view.get_scale = MagicMock(return_value=1)
self.item.SELECT_RESIZE_SIZE = 10
self.item.SELECT_ROTATE_SIZE = 10
path = self.item.get_rotate_bounds(QtCore.QPointF(0, 0))
assert path.boundingRect().topLeft().x() == -15
assert path.boundingRect().topLeft().y() == -15
assert path.boundingRect().bottomRight().x() == 5
assert path.boundingRect().bottomRight().y() == 5
assert path.contains(QtCore.QPointF(-4, -4)) is False
def test_bounding_rect_when_not_selected(self):
self.view.get_scale = MagicMock(return_value=1)
@ -255,8 +278,8 @@ class SelectableMixinTestCase(SelectableMixinBaseTestCase):
with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.shape',
return_value=path):
shape = self.item.shape().boundingRect()
assert shape.topLeft().x() == -5
assert shape.topLeft().y() == -5
assert shape.topLeft().x() == -15
assert shape.topLeft().y() == -15
assert shape.bottomRight().x() == 115
assert shape.bottomRight().y() == 95
@ -325,20 +348,20 @@ class SelectableMixinScalingTestCase(SelectableMixinBaseTestCase):
assert anchor.x() == 78
assert anchor.y() == 47
def test_get_scale_direction_topleft(self):
assert self.item.get_scale_direction(
def test_get_corner_direction_topleft(self):
assert self.item.get_corner_direction(
QtCore.QPointF(0, 0)) == QtCore.QPointF(-1, -1)
def test_get_scale_direction_bottomright(self):
assert self.item.get_scale_direction(
def test_get_corner_direction_bottomright(self):
assert self.item.get_corner_direction(
QtCore.QPointF(100, 80)) == QtCore.QPointF(1, 1)
def test_get_scale_direction_topright(self):
assert self.item.get_scale_direction(
def test_get_corner_direction_topright(self):
assert self.item.get_corner_direction(
QtCore.QPointF(100, 0)) == QtCore.QPointF(1, -1)
def test_get_scale_direction_bottomleft(self):
assert self.item.get_scale_direction(
def test_get_corner_direction_bottomleft(self):
assert self.item.get_corner_direction(
QtCore.QPointF(0, 80)) == QtCore.QPointF(-1, 1)
def test_translate_for_scale_anchor(self):

23
tests/test_utils.py Normal file
View file

@ -0,0 +1,23 @@
from PyQt6 import QtCore
from beeref import utils
from .base import BeeTestCase
class GetRectFromPointsTestCase(BeeTestCase):
def test_given_topleft_bottomright(self):
rect = utils.get_rect_from_points(QtCore.QPointF(-10, -20),
QtCore.QPointF(30, 40))
assert rect.topLeft().x() == -10
assert rect.topLeft().y() == -20
assert rect.bottomRight().x() == 30
assert rect.bottomRight().y() == 40
def test_given_topright_bottomleft(self):
rect = utils.get_rect_from_points(QtCore.QPointF(50, -20),
QtCore.QPointF(-30, 40))
assert rect.topLeft().x() == -30
assert rect.topLeft().y() == -20
assert rect.bottomRight().x() == 50
assert rect.bottomRight().y() == 40