Merge pull request #710 from campagnola/roi-mouse-interaction
Roi mouse interaction
This commit is contained in:
commit
00cccb556d
@ -138,7 +138,7 @@ label4 = w4.addLabel(text, row=0, col=0)
|
|||||||
v4 = w4.addViewBox(row=1, col=0, lockAspect=True)
|
v4 = w4.addViewBox(row=1, col=0, lockAspect=True)
|
||||||
g = pg.GridItem()
|
g = pg.GridItem()
|
||||||
v4.addItem(g)
|
v4.addItem(g)
|
||||||
r4 = pg.ROI([0,0], [100,100], removable=True)
|
r4 = pg.ROI([0,0], [100,100], resizable=False, removable=True)
|
||||||
r4.addRotateHandle([1,0], [0.5, 0.5])
|
r4.addRotateHandle([1,0], [0.5, 0.5])
|
||||||
r4.addRotateHandle([0,1], [0.5, 0.5])
|
r4.addRotateHandle([0,1], [0.5, 0.5])
|
||||||
img4 = pg.ImageItem(arr)
|
img4 = pg.ImageItem(arr)
|
||||||
|
@ -44,8 +44,6 @@ class ROI(GraphicsObject):
|
|||||||
any of the built-in subclasses) and any combination of draggable handles
|
any of the built-in subclasses) and any combination of draggable handles
|
||||||
that allow the user to manipulate the ROI.
|
that allow the user to manipulate the ROI.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
================ ===========================================================
|
================ ===========================================================
|
||||||
**Arguments**
|
**Arguments**
|
||||||
pos (length-2 sequence) Indicates the position of the ROI's
|
pos (length-2 sequence) Indicates the position of the ROI's
|
||||||
@ -68,13 +66,16 @@ class ROI(GraphicsObject):
|
|||||||
to be integer multiples of *snapSize* when being resized
|
to be integer multiples of *snapSize* when being resized
|
||||||
by the user. Default is False.
|
by the user. Default is False.
|
||||||
rotateSnap (bool) If True, the ROI angle is forced to a multiple of
|
rotateSnap (bool) If True, the ROI angle is forced to a multiple of
|
||||||
15 degrees when rotated by the user. Default is False.
|
the ROI's snap angle (default is 15 degrees) when rotated
|
||||||
|
by the user. Default is False.
|
||||||
parent (QGraphicsItem) The graphics item parent of this ROI. It
|
parent (QGraphicsItem) The graphics item parent of this ROI. It
|
||||||
is generally not necessary to specify the parent.
|
is generally not necessary to specify the parent.
|
||||||
pen (QPen or argument to pg.mkPen) The pen to use when drawing
|
pen (QPen or argument to pg.mkPen) The pen to use when drawing
|
||||||
the shape of the ROI.
|
the shape of the ROI.
|
||||||
movable (bool) If True, the ROI can be moved by dragging anywhere
|
movable (bool) If True, the ROI can be moved by dragging anywhere
|
||||||
inside the ROI. Default is True.
|
inside the ROI. Default is True.
|
||||||
|
rotatable (bool) If True, the ROI can be rotated by mouse drag + ALT
|
||||||
|
resizable (bool) If True, the ROI can be resized by mouse drag + SHIFT
|
||||||
removable (bool) If True, the ROI will be given a context menu with
|
removable (bool) If True, the ROI will be given a context menu with
|
||||||
an option to remove the ROI. The ROI emits
|
an option to remove the ROI. The ROI emits
|
||||||
sigRemoveRequested when this menu action is selected.
|
sigRemoveRequested when this menu action is selected.
|
||||||
@ -112,14 +113,18 @@ class ROI(GraphicsObject):
|
|||||||
sigClicked = QtCore.Signal(object, object)
|
sigClicked = QtCore.Signal(object, object)
|
||||||
sigRemoveRequested = QtCore.Signal(object)
|
sigRemoveRequested = QtCore.Signal(object)
|
||||||
|
|
||||||
def __init__(self, pos, size=Point(1, 1), angle=0.0, invertible=False, maxBounds=None, snapSize=1.0, scaleSnap=False, translateSnap=False, rotateSnap=False, parent=None, pen=None, movable=True, removable=False):
|
def __init__(self, pos, size=Point(1, 1), angle=0.0, invertible=False, maxBounds=None,
|
||||||
|
snapSize=1.0, scaleSnap=False, translateSnap=False, rotateSnap=False,
|
||||||
|
parent=None, pen=None, movable=True, rotatable=True, resizable=True,
|
||||||
|
removable=False):
|
||||||
GraphicsObject.__init__(self, parent)
|
GraphicsObject.__init__(self, parent)
|
||||||
self.setAcceptedMouseButtons(QtCore.Qt.NoButton)
|
self.setAcceptedMouseButtons(QtCore.Qt.NoButton)
|
||||||
pos = Point(pos)
|
pos = Point(pos)
|
||||||
size = Point(size)
|
size = Point(size)
|
||||||
self.aspectLocked = False
|
self.aspectLocked = False
|
||||||
self.translatable = movable
|
self.translatable = movable
|
||||||
self.rotateAllowed = True
|
self.rotatable = rotatable
|
||||||
|
self.resizable = resizable
|
||||||
self.removable = removable
|
self.removable = removable
|
||||||
self.menu = None
|
self.menu = None
|
||||||
|
|
||||||
@ -146,7 +151,12 @@ class ROI(GraphicsObject):
|
|||||||
self.snapSize = snapSize
|
self.snapSize = snapSize
|
||||||
self.translateSnap = translateSnap
|
self.translateSnap = translateSnap
|
||||||
self.rotateSnap = rotateSnap
|
self.rotateSnap = rotateSnap
|
||||||
|
self.rotateSnapAngle = 15.0
|
||||||
self.scaleSnap = scaleSnap
|
self.scaleSnap = scaleSnap
|
||||||
|
self.scaleSnapSize = snapSize
|
||||||
|
|
||||||
|
# Implement mouse handling in a separate class to allow easier customization
|
||||||
|
self.mouseDragHandler = MouseDragHandler(self)
|
||||||
|
|
||||||
def getState(self):
|
def getState(self):
|
||||||
return self.stateCopy()
|
return self.stateCopy()
|
||||||
@ -246,45 +256,93 @@ class ROI(GraphicsObject):
|
|||||||
if update:
|
if update:
|
||||||
self.stateChanged(finish=finish)
|
self.stateChanged(finish=finish)
|
||||||
|
|
||||||
def setSize(self, size, update=True, finish=True):
|
def setSize(self, size, center=None, centerLocal=None, snap=False, update=True, finish=True):
|
||||||
"""Set the size of the ROI. May be specified as a QPoint, Point, or list of two values.
|
"""
|
||||||
See setPos() for an explanation of the update and finish arguments.
|
Set the ROI's size.
|
||||||
|
|
||||||
|
=============== ==========================================================================
|
||||||
|
**Arguments**
|
||||||
|
size (Point | QPointF | sequence) The final size of the ROI
|
||||||
|
center (None | Point) Optional center point around which the ROI is scaled,
|
||||||
|
expressed as [0-1, 0-1] over the size of the ROI.
|
||||||
|
centerLocal (None | Point) Same as *center*, but the position is expressed in the
|
||||||
|
local coordinate system of the ROI
|
||||||
|
snap (bool) If True, the final size is snapped to the nearest increment (see
|
||||||
|
ROI.scaleSnapSize)
|
||||||
|
update (bool) See setPos()
|
||||||
|
finish (bool) See setPos()
|
||||||
|
=============== ==========================================================================
|
||||||
"""
|
"""
|
||||||
if update not in (True, False):
|
if update not in (True, False):
|
||||||
raise TypeError("update argument must be bool")
|
raise TypeError("update argument must be bool")
|
||||||
size = Point(size)
|
size = Point(size)
|
||||||
|
if snap:
|
||||||
|
size[0] = round(size[0] / self.scaleSnapSize) * self.scaleSnapSize
|
||||||
|
size[1] = round(size[1] / self.scaleSnapSize) * self.scaleSnapSize
|
||||||
|
|
||||||
|
if centerLocal is not None:
|
||||||
|
oldSize = Point(self.state['size'])
|
||||||
|
oldSize[0] = 1 if oldSize[0] == 0 else oldSize[0]
|
||||||
|
oldSize[1] = 1 if oldSize[1] == 0 else oldSize[1]
|
||||||
|
center = Point(centerLocal) / oldSize
|
||||||
|
|
||||||
|
if center is not None:
|
||||||
|
center = Point(center)
|
||||||
|
c = self.mapToParent(Point(center) * self.state['size'])
|
||||||
|
c1 = self.mapToParent(Point(center) * size)
|
||||||
|
newPos = self.state['pos'] + c - c1
|
||||||
|
self.setPos(newPos, update=False, finish=False)
|
||||||
|
|
||||||
self.prepareGeometryChange()
|
self.prepareGeometryChange()
|
||||||
self.state['size'] = size
|
self.state['size'] = size
|
||||||
if update:
|
if update:
|
||||||
self.stateChanged(finish=finish)
|
self.stateChanged(finish=finish)
|
||||||
|
|
||||||
def setAngle(self, angle, update=True, finish=True):
|
def setAngle(self, angle, center=None, centerLocal=None, snap=False, update=True, finish=True):
|
||||||
"""Set the angle of rotation (in degrees) for this ROI.
|
"""
|
||||||
See setPos() for an explanation of the update and finish arguments.
|
Set the ROI's rotation angle.
|
||||||
|
|
||||||
|
=============== ==========================================================================
|
||||||
|
**Arguments**
|
||||||
|
angle (float) The final ROI angle in degrees
|
||||||
|
center (None | Point) Optional center point around which the ROI is rotated,
|
||||||
|
expressed as [0-1, 0-1] over the size of the ROI.
|
||||||
|
centerLocal (None | Point) Same as *center*, but the position is expressed in the
|
||||||
|
local coordinate system of the ROI
|
||||||
|
snap (bool) If True, the final ROI angle is snapped to the nearest increment
|
||||||
|
(default is 15 degrees; see ROI.rotateSnapAngle)
|
||||||
|
update (bool) See setPos()
|
||||||
|
finish (bool) See setPos()
|
||||||
|
=============== ==========================================================================
|
||||||
"""
|
"""
|
||||||
if update not in (True, False):
|
if update not in (True, False):
|
||||||
raise TypeError("update argument must be bool")
|
raise TypeError("update argument must be bool")
|
||||||
|
|
||||||
|
if snap is True:
|
||||||
|
angle = round(angle / self.rotateSnapAngle) * self.rotateSnapAngle
|
||||||
|
|
||||||
self.state['angle'] = angle
|
self.state['angle'] = angle
|
||||||
tr = QtGui.QTransform()
|
tr = QtGui.QTransform() # note: only rotation is contained in the transform
|
||||||
tr.rotate(angle)
|
tr.rotate(angle)
|
||||||
|
if center is not None:
|
||||||
|
centerLocal = Point(center) * self.state['size']
|
||||||
|
if centerLocal is not None:
|
||||||
|
centerLocal = Point(centerLocal)
|
||||||
|
# rotate to new angle, keeping a specific point anchored as the center of rotation
|
||||||
|
cc = self.mapToParent(centerLocal) - (tr.map(centerLocal) + self.state['pos'])
|
||||||
|
self.translate(cc, update=False)
|
||||||
|
|
||||||
self.setTransform(tr)
|
self.setTransform(tr)
|
||||||
if update:
|
if update:
|
||||||
self.stateChanged(finish=finish)
|
self.stateChanged(finish=finish)
|
||||||
|
|
||||||
def scale(self, s, center=[0,0], update=True, finish=True):
|
def scale(self, s, center=None, centerLocal=None, snap=False, update=True, finish=True):
|
||||||
"""
|
"""
|
||||||
Resize the ROI by scaling relative to *center*.
|
Resize the ROI by scaling relative to *center*.
|
||||||
See setPos() for an explanation of the *update* and *finish* arguments.
|
See setPos() for an explanation of the *update* and *finish* arguments.
|
||||||
"""
|
"""
|
||||||
c = self.mapToParent(Point(center) * self.state['size'])
|
|
||||||
self.prepareGeometryChange()
|
|
||||||
newSize = self.state['size'] * s
|
newSize = self.state['size'] * s
|
||||||
c1 = self.mapToParent(Point(center) * newSize)
|
self.setSize(newSize, center=center, centerLocal=centerLocal, snap=snap, update=update, finish=finish)
|
||||||
newPos = self.state['pos'] + c - c1
|
|
||||||
|
|
||||||
self.setSize(newSize, update=False)
|
|
||||||
self.setPos(newPos, update=update, finish=finish)
|
|
||||||
|
|
||||||
|
|
||||||
def translate(self, *args, **kargs):
|
def translate(self, *args, **kargs):
|
||||||
"""
|
"""
|
||||||
@ -336,14 +394,22 @@ class ROI(GraphicsObject):
|
|||||||
finish = kargs.get('finish', True)
|
finish = kargs.get('finish', True)
|
||||||
self.setPos(newState['pos'], update=update, finish=finish)
|
self.setPos(newState['pos'], update=update, finish=finish)
|
||||||
|
|
||||||
def rotate(self, angle, update=True, finish=True):
|
def rotate(self, angle, center=None, snap=False, update=True, finish=True):
|
||||||
"""
|
"""
|
||||||
Rotate the ROI by *angle* degrees.
|
Rotate the ROI by *angle* degrees.
|
||||||
|
|
||||||
Also accepts *update* and *finish* arguments (see setPos() for a
|
=============== ==========================================================================
|
||||||
description of these).
|
**Arguments**
|
||||||
|
angle (float) The angle in degrees to rotate
|
||||||
|
center (None | Point) Optional center point around which the ROI is rotated, in
|
||||||
|
the local coordinate system of the ROI
|
||||||
|
snap (bool) If True, the final ROI angle is snapped to the nearest increment
|
||||||
|
(default is 15 degrees; see ROI.rotateSnapAngle)
|
||||||
|
update (bool) See setPos()
|
||||||
|
finish (bool) See setPos()
|
||||||
|
=============== ==========================================================================
|
||||||
"""
|
"""
|
||||||
self.setAngle(self.angle()+angle, update=update, finish=finish)
|
self.setAngle(self.angle()+angle, center=center, snap=snap, update=update, finish=finish)
|
||||||
|
|
||||||
def handleMoveStarted(self):
|
def handleMoveStarted(self):
|
||||||
self.preMoveState = self.getState()
|
self.preMoveState = self.getState()
|
||||||
@ -683,33 +749,7 @@ class ROI(GraphicsObject):
|
|||||||
QtCore.QTimer.singleShot(0, lambda: self.sigRemoveRequested.emit(self))
|
QtCore.QTimer.singleShot(0, lambda: self.sigRemoveRequested.emit(self))
|
||||||
|
|
||||||
def mouseDragEvent(self, ev):
|
def mouseDragEvent(self, ev):
|
||||||
if ev.isStart():
|
self.mouseDragHandler.mouseDragEvent(ev)
|
||||||
#p = ev.pos()
|
|
||||||
#if not self.isMoving and not self.shape().contains(p):
|
|
||||||
#ev.ignore()
|
|
||||||
#return
|
|
||||||
if ev.button() == QtCore.Qt.LeftButton:
|
|
||||||
self.setSelected(True)
|
|
||||||
if self.translatable:
|
|
||||||
self.isMoving = True
|
|
||||||
self.preMoveState = self.getState()
|
|
||||||
self.cursorOffset = self.pos() - self.mapToParent(ev.buttonDownPos())
|
|
||||||
self.sigRegionChangeStarted.emit(self)
|
|
||||||
ev.accept()
|
|
||||||
else:
|
|
||||||
ev.ignore()
|
|
||||||
|
|
||||||
elif ev.isFinish():
|
|
||||||
if self.translatable:
|
|
||||||
if self.isMoving:
|
|
||||||
self.stateChangeFinished()
|
|
||||||
self.isMoving = False
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.translatable and self.isMoving and ev.buttons() == QtCore.Qt.LeftButton:
|
|
||||||
snap = True if (ev.modifiers() & QtCore.Qt.ControlModifier) else None
|
|
||||||
newPos = self.mapToParent(ev.pos()) + self.cursorOffset
|
|
||||||
self.translate(newPos - self.pos(), snap=snap, finish=False)
|
|
||||||
|
|
||||||
def mouseClickEvent(self, ev):
|
def mouseClickEvent(self, ev):
|
||||||
if ev.button() == QtCore.Qt.RightButton and self.isMoving:
|
if ev.button() == QtCore.Qt.RightButton and self.isMoving:
|
||||||
@ -724,6 +764,16 @@ class ROI(GraphicsObject):
|
|||||||
else:
|
else:
|
||||||
ev.ignore()
|
ev.ignore()
|
||||||
|
|
||||||
|
def _moveStarted(self):
|
||||||
|
self.isMoving = True
|
||||||
|
self.preMoveState = self.getState()
|
||||||
|
self.sigRegionChangeStarted.emit(self)
|
||||||
|
|
||||||
|
def _moveFinished(self):
|
||||||
|
if self.isMoving:
|
||||||
|
self.stateChangeFinished()
|
||||||
|
self.isMoving = False
|
||||||
|
|
||||||
def cancelMove(self):
|
def cancelMove(self):
|
||||||
self.isMoving = False
|
self.isMoving = False
|
||||||
self.setState(self.preMoveState)
|
self.setState(self.preMoveState)
|
||||||
@ -777,8 +827,8 @@ class ROI(GraphicsObject):
|
|||||||
|
|
||||||
## snap
|
## snap
|
||||||
if self.scaleSnap or (modifiers & QtCore.Qt.ControlModifier):
|
if self.scaleSnap or (modifiers & QtCore.Qt.ControlModifier):
|
||||||
lp1[0] = round(lp1[0] / self.snapSize) * self.snapSize
|
lp1[0] = round(lp1[0] / self.scaleSnapSize) * self.scaleSnapSize
|
||||||
lp1[1] = round(lp1[1] / self.snapSize) * self.snapSize
|
lp1[1] = round(lp1[1] / self.scaleSnapSize) * self.scaleSnapSize
|
||||||
|
|
||||||
## preserve aspect ratio (this can override snapping)
|
## preserve aspect ratio (this can override snapping)
|
||||||
if h['lockAspect'] or (modifiers & QtCore.Qt.AltModifier):
|
if h['lockAspect'] or (modifiers & QtCore.Qt.AltModifier):
|
||||||
@ -826,7 +876,7 @@ class ROI(GraphicsObject):
|
|||||||
if h['type'] == 'rf':
|
if h['type'] == 'rf':
|
||||||
self.freeHandleMoved = True
|
self.freeHandleMoved = True
|
||||||
|
|
||||||
if not self.rotateAllowed:
|
if not self.rotatable:
|
||||||
return
|
return
|
||||||
## If the handle is directly over its center point, we can't compute an angle.
|
## If the handle is directly over its center point, we can't compute an angle.
|
||||||
try:
|
try:
|
||||||
@ -840,7 +890,7 @@ class ROI(GraphicsObject):
|
|||||||
if ang is None: ## this should never happen..
|
if ang is None: ## this should never happen..
|
||||||
return
|
return
|
||||||
if self.rotateSnap or (modifiers & QtCore.Qt.ControlModifier):
|
if self.rotateSnap or (modifiers & QtCore.Qt.ControlModifier):
|
||||||
ang = round(ang / 15.) * 15. ## 180/12 = 15
|
ang = round(ang / self.rotateSnapAngle) * self.rotateSnapAngle
|
||||||
|
|
||||||
## create rotation transform
|
## create rotation transform
|
||||||
tr = QtGui.QTransform()
|
tr = QtGui.QTransform()
|
||||||
@ -882,7 +932,7 @@ class ROI(GraphicsObject):
|
|||||||
if ang is None:
|
if ang is None:
|
||||||
return
|
return
|
||||||
if self.rotateSnap or (modifiers & QtCore.Qt.ControlModifier):
|
if self.rotateSnap or (modifiers & QtCore.Qt.ControlModifier):
|
||||||
ang = round(ang / 15.) * 15.
|
ang = round(ang / self.rotateSnapAngle) * self.rotateSnapAngle
|
||||||
|
|
||||||
hs = abs(h['pos'][scaleAxis] - c[scaleAxis])
|
hs = abs(h['pos'][scaleAxis] - c[scaleAxis])
|
||||||
newState['size'][scaleAxis] = lp1.length() / hs
|
newState['size'][scaleAxis] = lp1.length() / hs
|
||||||
@ -1381,6 +1431,71 @@ class Handle(UIGraphicsItem):
|
|||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
|
|
||||||
|
class MouseDragHandler(object):
|
||||||
|
"""Implements default mouse drag behavior for ROI (not for ROI handles).
|
||||||
|
"""
|
||||||
|
def __init__(self, roi):
|
||||||
|
self.roi = roi
|
||||||
|
self.dragMode = None
|
||||||
|
self.startState = None
|
||||||
|
self.snapModifier = QtCore.Qt.ControlModifier
|
||||||
|
self.translateModifier = QtCore.Qt.NoModifier
|
||||||
|
self.rotateModifier = QtCore.Qt.AltModifier
|
||||||
|
self.scaleModifier = QtCore.Qt.ShiftModifier
|
||||||
|
self.rotateSpeed = 0.7
|
||||||
|
self.scaleSpeed = 1.01
|
||||||
|
|
||||||
|
def mouseDragEvent(self, ev):
|
||||||
|
roi = self.roi
|
||||||
|
|
||||||
|
if ev.isStart():
|
||||||
|
if ev.button() == QtCore.Qt.LeftButton:
|
||||||
|
roi.setSelected(True)
|
||||||
|
mods = ev.modifiers() & ~self.snapModifier
|
||||||
|
if roi.translatable and mods == self.translateModifier:
|
||||||
|
self.dragMode = 'translate'
|
||||||
|
elif roi.rotatable and mods == self.rotateModifier:
|
||||||
|
self.dragMode = 'rotate'
|
||||||
|
elif roi.resizable and mods == self.scaleModifier:
|
||||||
|
self.dragMode = 'scale'
|
||||||
|
else:
|
||||||
|
self.dragMode = None
|
||||||
|
|
||||||
|
if self.dragMode is not None:
|
||||||
|
roi._moveStarted()
|
||||||
|
self.startPos = roi.mapToParent(ev.buttonDownPos())
|
||||||
|
self.startState = roi.saveState()
|
||||||
|
self.cursorOffset = roi.pos() - self.startPos
|
||||||
|
ev.accept()
|
||||||
|
else:
|
||||||
|
ev.ignore()
|
||||||
|
else:
|
||||||
|
self.dragMode = None
|
||||||
|
ev.ignore()
|
||||||
|
|
||||||
|
|
||||||
|
if ev.isFinish() and self.dragMode is not None:
|
||||||
|
roi._moveFinished()
|
||||||
|
return
|
||||||
|
|
||||||
|
# roi.isMoving becomes False if the move was cancelled by right-click
|
||||||
|
if not roi.isMoving or self.dragMode is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
snap = True if (ev.modifiers() & self.snapModifier) else None
|
||||||
|
pos = roi.mapToParent(ev.pos())
|
||||||
|
if self.dragMode == 'translate':
|
||||||
|
newPos = pos + self.cursorOffset
|
||||||
|
roi.translate(newPos - roi.pos(), snap=snap, finish=False)
|
||||||
|
elif self.dragMode == 'rotate':
|
||||||
|
diff = self.rotateSpeed * (ev.scenePos() - ev.buttonDownScenePos()).x()
|
||||||
|
angle = self.startState['angle'] - diff
|
||||||
|
roi.setAngle(angle, centerLocal=ev.buttonDownPos(), snap=snap, finish=False)
|
||||||
|
elif self.dragMode == 'scale':
|
||||||
|
diff = self.scaleSpeed ** -(ev.scenePos() - ev.buttonDownScenePos()).y()
|
||||||
|
roi.setSize(Point(self.startState['size']) * diff, centerLocal=ev.buttonDownPos(), snap=snap, finish=False)
|
||||||
|
|
||||||
|
|
||||||
class TestROI(ROI):
|
class TestROI(ROI):
|
||||||
def __init__(self, pos, size, **args):
|
def __init__(self, pos, size, **args):
|
||||||
ROI.__init__(self, pos, size, **args)
|
ROI.__init__(self, pos, size, **args)
|
||||||
|
Loading…
Reference in New Issue
Block a user