2014-03-24 19:58:07 +00:00
|
|
|
from ..Qt import QtGui, QtCore, isQObjectAlive
|
2013-12-22 07:08:39 +00:00
|
|
|
from ..GraphicsScene import GraphicsScene
|
|
|
|
from ..Point import Point
|
|
|
|
from .. import functions as fn
|
2012-03-02 02:55:32 +00:00
|
|
|
import weakref
|
2014-03-04 13:03:20 +00:00
|
|
|
import operator
|
2014-03-05 15:25:55 +00:00
|
|
|
from ..util.lru_cache import LRUCache
|
2012-03-02 02:55:32 +00:00
|
|
|
|
2013-01-10 03:21:32 +00:00
|
|
|
|
2012-04-16 20:45:55 +00:00
|
|
|
class GraphicsItem(object):
|
2012-03-02 02:55:32 +00:00
|
|
|
"""
|
2012-04-16 20:45:55 +00:00
|
|
|
**Bases:** :class:`object`
|
|
|
|
|
|
|
|
Abstract class providing useful methods to GraphicsObject and GraphicsWidget.
|
|
|
|
(This is required because we cannot have multiple inheritance with QObject subclasses.)
|
2012-05-30 03:22:00 +00:00
|
|
|
|
|
|
|
A note about Qt's GraphicsView framework:
|
|
|
|
|
|
|
|
The GraphicsView system places a lot of emphasis on the notion that the graphics within the scene should be device independent--you should be able to take the same graphics and display them on screens of different resolutions, printers, export to SVG, etc. This is nice in principle, but causes me a lot of headache in practice. It means that I have to circumvent all the device-independent expectations any time I want to operate in pixel coordinates rather than arbitrary scene coordinates. A lot of the code in GraphicsItem is devoted to this task--keeping track of view widgets and device transforms, computing the size and shape of a pixel in local item coordinates, etc. Note that in item coordinates, a pixel does not have to be square or even rectangular, so just asking how to increase a bounding rect by 2px can be a rather complex task.
|
2012-03-02 02:55:32 +00:00
|
|
|
"""
|
2014-03-04 13:03:20 +00:00
|
|
|
_pixelVectorGlobalCache = LRUCache(100, 70)
|
2013-01-10 03:21:32 +00:00
|
|
|
|
2012-05-11 03:37:07 +00:00
|
|
|
def __init__(self, register=True):
|
2012-06-18 23:51:18 +00:00
|
|
|
if not hasattr(self, '_qtBaseClass'):
|
|
|
|
for b in self.__class__.__bases__:
|
|
|
|
if issubclass(b, QtGui.QGraphicsItem):
|
|
|
|
self.__class__._qtBaseClass = b
|
|
|
|
break
|
|
|
|
if not hasattr(self, '_qtBaseClass'):
|
|
|
|
raise Exception('Could not determine Qt base class for GraphicsItem: %s' % str(self))
|
|
|
|
|
2013-01-10 03:21:32 +00:00
|
|
|
self._pixelVectorCache = [None, None]
|
2012-03-02 02:55:32 +00:00
|
|
|
self._viewWidget = None
|
|
|
|
self._viewBox = None
|
2012-05-15 02:05:53 +00:00
|
|
|
self._connectedView = None
|
2012-12-26 21:29:29 +00:00
|
|
|
self._exportOpts = False ## If False, not currently exporting. Otherwise, contains dict of export options.
|
2012-05-11 03:37:07 +00:00
|
|
|
if register:
|
|
|
|
GraphicsScene.registerObject(self) ## workaround for pyqt bug in graphicsscene.items()
|
2012-06-18 23:51:18 +00:00
|
|
|
|
2012-12-26 21:29:29 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
2012-03-02 02:55:32 +00:00
|
|
|
def getViewWidget(self):
|
|
|
|
"""
|
2014-03-24 19:58:07 +00:00
|
|
|
Return the view widget for this item.
|
|
|
|
|
|
|
|
If the scene has multiple views, only the first view is returned.
|
|
|
|
The return value is cached; clear the cached value with forgetViewWidget().
|
|
|
|
If the view has been deleted by Qt, return None.
|
2012-03-02 02:55:32 +00:00
|
|
|
"""
|
|
|
|
if self._viewWidget is None:
|
|
|
|
scene = self.scene()
|
|
|
|
if scene is None:
|
|
|
|
return None
|
|
|
|
views = scene.views()
|
|
|
|
if len(views) < 1:
|
|
|
|
return None
|
|
|
|
self._viewWidget = weakref.ref(self.scene().views()[0])
|
2014-03-13 18:52:49 +00:00
|
|
|
|
|
|
|
v = self._viewWidget()
|
2014-03-24 19:58:07 +00:00
|
|
|
if v is not None and not isQObjectAlive(v):
|
|
|
|
return None
|
2014-03-13 18:52:49 +00:00
|
|
|
|
|
|
|
return v
|
2012-05-30 03:22:00 +00:00
|
|
|
|
2012-03-02 02:55:32 +00:00
|
|
|
def forgetViewWidget(self):
|
|
|
|
self._viewWidget = None
|
2012-05-30 03:22:00 +00:00
|
|
|
|
2012-03-02 02:55:32 +00:00
|
|
|
def getViewBox(self):
|
|
|
|
"""
|
|
|
|
Return the first ViewBox or GraphicsView which bounds this item's visible space.
|
|
|
|
If this item is not contained within a ViewBox, then the GraphicsView is returned.
|
|
|
|
If the item is contained inside nested ViewBoxes, then the inner-most ViewBox is returned.
|
|
|
|
The result is cached; clear the cache with forgetViewBox()
|
|
|
|
"""
|
|
|
|
if self._viewBox is None:
|
|
|
|
p = self
|
|
|
|
while True:
|
2012-12-29 07:35:45 +00:00
|
|
|
try:
|
|
|
|
p = p.parentItem()
|
|
|
|
except RuntimeError: ## sometimes happens as items are being removed from a scene and collected.
|
|
|
|
return None
|
2012-03-02 02:55:32 +00:00
|
|
|
if p is None:
|
|
|
|
vb = self.getViewWidget()
|
|
|
|
if vb is None:
|
|
|
|
return None
|
|
|
|
else:
|
|
|
|
self._viewBox = weakref.ref(vb)
|
|
|
|
break
|
|
|
|
if hasattr(p, 'implements') and p.implements('ViewBox'):
|
|
|
|
self._viewBox = weakref.ref(p)
|
|
|
|
break
|
|
|
|
return self._viewBox() ## If we made it this far, _viewBox is definitely not None
|
|
|
|
|
|
|
|
def forgetViewBox(self):
|
|
|
|
self._viewBox = None
|
|
|
|
|
|
|
|
|
|
|
|
def deviceTransform(self, viewportTransform=None):
|
|
|
|
"""
|
|
|
|
Return the transform that converts local item coordinates to device coordinates (usually pixels).
|
|
|
|
Extends deviceTransform to automatically determine the viewportTransform.
|
|
|
|
"""
|
2012-12-26 21:29:29 +00:00
|
|
|
if self._exportOpts is not False and 'painter' in self._exportOpts: ## currently exporting; device transform may be different.
|
2014-07-30 03:57:34 +00:00
|
|
|
return self._exportOpts['painter'].deviceTransform() * self.sceneTransform()
|
2012-12-26 21:29:29 +00:00
|
|
|
|
2012-03-02 02:55:32 +00:00
|
|
|
if viewportTransform is None:
|
|
|
|
view = self.getViewWidget()
|
|
|
|
if view is None:
|
|
|
|
return None
|
|
|
|
viewportTransform = view.viewportTransform()
|
2012-06-18 23:40:15 +00:00
|
|
|
dt = self._qtBaseClass.deviceTransform(self, viewportTransform)
|
2012-05-30 03:22:00 +00:00
|
|
|
|
|
|
|
#xmag = abs(dt.m11())+abs(dt.m12())
|
|
|
|
#ymag = abs(dt.m21())+abs(dt.m22())
|
|
|
|
#if xmag * ymag == 0:
|
|
|
|
if dt.determinant() == 0: ## occurs when deviceTransform is invalid because widget has not been displayed
|
|
|
|
return None
|
|
|
|
else:
|
|
|
|
return dt
|
2012-03-02 02:55:32 +00:00
|
|
|
|
|
|
|
def viewTransform(self):
|
|
|
|
"""Return the transform that maps from local coordinates to the item's ViewBox coordinates
|
|
|
|
If there is no ViewBox, return the scene transform.
|
|
|
|
Returns None if the item does not have a view."""
|
|
|
|
view = self.getViewBox()
|
|
|
|
if view is None:
|
|
|
|
return None
|
|
|
|
if hasattr(view, 'implements') and view.implements('ViewBox'):
|
2012-03-02 03:53:52 +00:00
|
|
|
tr = self.itemTransform(view.innerSceneItem())
|
|
|
|
if isinstance(tr, tuple):
|
|
|
|
tr = tr[0] ## difference between pyside and pyqt
|
|
|
|
return tr
|
2012-03-02 02:55:32 +00:00
|
|
|
else:
|
|
|
|
return self.sceneTransform()
|
|
|
|
#return self.deviceTransform(view.viewportTransform())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def getBoundingParents(self):
|
|
|
|
"""Return a list of parents to this item that have child clipping enabled."""
|
|
|
|
p = self
|
|
|
|
parents = []
|
|
|
|
while True:
|
|
|
|
p = p.parentItem()
|
|
|
|
if p is None:
|
|
|
|
break
|
|
|
|
if p.flags() & self.ItemClipsChildrenToShape:
|
|
|
|
parents.append(p)
|
|
|
|
return parents
|
|
|
|
|
|
|
|
def viewRect(self):
|
|
|
|
"""Return the bounds (in item coordinates) of this item's ViewBox or GraphicsWidget"""
|
|
|
|
view = self.getViewBox()
|
|
|
|
if view is None:
|
|
|
|
return None
|
2012-03-02 03:53:52 +00:00
|
|
|
bounds = self.mapRectFromView(view.viewRect())
|
|
|
|
if bounds is None:
|
|
|
|
return None
|
|
|
|
|
|
|
|
bounds = bounds.normalized()
|
2012-03-02 02:55:32 +00:00
|
|
|
|
|
|
|
## nah.
|
|
|
|
#for p in self.getBoundingParents():
|
|
|
|
#bounds &= self.mapRectFromScene(p.sceneBoundingRect())
|
|
|
|
|
|
|
|
return bounds
|
|
|
|
|
|
|
|
|
|
|
|
|
2012-05-30 03:22:00 +00:00
|
|
|
def pixelVectors(self, direction=None):
|
|
|
|
"""Return vectors in local coordinates representing the width and height of a view pixel.
|
|
|
|
If direction is specified, then return vectors parallel and orthogonal to it.
|
2012-03-02 02:55:32 +00:00
|
|
|
|
2012-07-12 19:35:58 +00:00
|
|
|
Return (None, None) if pixel size is not yet defined (usually because the item has not yet been displayed)
|
|
|
|
or if pixel size is below floating-point precision limit.
|
|
|
|
"""
|
2013-01-10 03:21:32 +00:00
|
|
|
|
|
|
|
## This is an expensive function that gets called very frequently.
|
|
|
|
## We have two levels of cache to try speeding things up.
|
|
|
|
|
2012-03-02 02:55:32 +00:00
|
|
|
dt = self.deviceTransform()
|
|
|
|
if dt is None:
|
2012-05-30 03:22:00 +00:00
|
|
|
return None, None
|
2013-02-10 19:10:30 +00:00
|
|
|
|
|
|
|
## Ignore translation. If the translation is much larger than the scale
|
|
|
|
## (such as when looking at unix timestamps), we can get floating-point errors.
|
|
|
|
dt.setMatrix(dt.m11(), dt.m12(), 0, dt.m21(), dt.m22(), 0, 0, 0, 1)
|
2012-05-30 03:22:00 +00:00
|
|
|
|
2013-01-10 03:21:32 +00:00
|
|
|
## check local cache
|
|
|
|
if direction is None and dt == self._pixelVectorCache[0]:
|
2013-01-12 23:07:35 +00:00
|
|
|
return tuple(map(Point, self._pixelVectorCache[1])) ## return a *copy*
|
2013-01-10 03:21:32 +00:00
|
|
|
|
|
|
|
## check global cache
|
2013-02-11 02:04:00 +00:00
|
|
|
#key = (dt.m11(), dt.m21(), dt.m31(), dt.m12(), dt.m22(), dt.m32(), dt.m31(), dt.m32())
|
|
|
|
key = (dt.m11(), dt.m21(), dt.m12(), dt.m22())
|
2013-01-10 03:21:32 +00:00
|
|
|
pv = self._pixelVectorGlobalCache.get(key, None)
|
2013-01-24 18:47:05 +00:00
|
|
|
if direction is None and pv is not None:
|
2013-01-10 03:21:32 +00:00
|
|
|
self._pixelVectorCache = [dt, pv]
|
2013-01-12 23:07:35 +00:00
|
|
|
return tuple(map(Point,pv)) ## return a *copy*
|
2013-01-10 03:21:32 +00:00
|
|
|
|
|
|
|
|
2012-05-30 03:22:00 +00:00
|
|
|
if direction is None:
|
2013-01-10 03:21:32 +00:00
|
|
|
direction = QtCore.QPointF(1, 0)
|
2012-07-12 19:35:58 +00:00
|
|
|
if direction.manhattanLength() == 0:
|
|
|
|
raise Exception("Cannot compute pixel length for 0-length vector.")
|
2012-05-30 03:22:00 +00:00
|
|
|
|
2012-07-12 19:35:58 +00:00
|
|
|
## attempt to re-scale direction vector to fit within the precision of the coordinate system
|
2013-02-10 19:10:30 +00:00
|
|
|
## Here's the problem: we need to map the vector 'direction' from the item to the device, via transform 'dt'.
|
|
|
|
## In some extreme cases, this mapping can fail unless the length of 'direction' is cleverly chosen.
|
|
|
|
## Example:
|
|
|
|
## dt = [ 1, 0, 2
|
|
|
|
## 0, 2, 1e20
|
|
|
|
## 0, 0, 1 ]
|
|
|
|
## Then we map the origin (0,0) and direction (0,1) and get:
|
|
|
|
## o' = 2,1e20
|
|
|
|
## d' = 2,1e20 <-- should be 1e20+2, but this can't be represented with a 32-bit float
|
|
|
|
##
|
|
|
|
## |o' - d'| == 0 <-- this is the problem.
|
|
|
|
|
|
|
|
## Perhaps the easiest solution is to exclude the transformation column from dt. Does this cause any other problems?
|
|
|
|
|
|
|
|
#if direction.x() == 0:
|
|
|
|
#r = abs(dt.m32())/(abs(dt.m12()) + abs(dt.m22()))
|
|
|
|
##r = 1.0/(abs(dt.m12()) + abs(dt.m22()))
|
|
|
|
#elif direction.y() == 0:
|
|
|
|
#r = abs(dt.m31())/(abs(dt.m11()) + abs(dt.m21()))
|
|
|
|
##r = 1.0/(abs(dt.m11()) + abs(dt.m21()))
|
|
|
|
#else:
|
|
|
|
#r = ((abs(dt.m32())/(abs(dt.m12()) + abs(dt.m22()))) * (abs(dt.m31())/(abs(dt.m11()) + abs(dt.m21()))))**0.5
|
|
|
|
#if r == 0:
|
|
|
|
#r = 1. ## shouldn't need to do this; probably means the math above is wrong?
|
|
|
|
#directionr = direction * r
|
|
|
|
directionr = direction
|
2012-07-12 19:35:58 +00:00
|
|
|
|
2013-01-10 03:21:32 +00:00
|
|
|
## map direction vector onto device
|
|
|
|
#viewDir = Point(dt.map(directionr) - dt.map(Point(0,0)))
|
|
|
|
#mdirection = dt.map(directionr)
|
|
|
|
dirLine = QtCore.QLineF(QtCore.QPointF(0,0), directionr)
|
|
|
|
viewDir = dt.map(dirLine)
|
|
|
|
if viewDir.length() == 0:
|
2012-07-12 19:35:58 +00:00
|
|
|
return None, None ## pixel size cannot be represented on this scale
|
2013-01-10 03:21:32 +00:00
|
|
|
|
|
|
|
## get unit vector and orthogonal vector (length of pixel)
|
|
|
|
#orthoDir = Point(viewDir[1], -viewDir[0]) ## orthogonal to line in pixel-space
|
2012-05-30 03:22:00 +00:00
|
|
|
try:
|
2013-01-10 03:21:32 +00:00
|
|
|
normView = viewDir.unitVector()
|
|
|
|
#normView = viewDir.norm() ## direction of one pixel orthogonal to line
|
|
|
|
normOrtho = normView.normalVector()
|
|
|
|
#normOrtho = orthoDir.norm()
|
2012-05-30 03:22:00 +00:00
|
|
|
except:
|
2012-11-23 21:01:25 +00:00
|
|
|
raise Exception("Invalid direction %s" %directionr)
|
2012-05-30 03:22:00 +00:00
|
|
|
|
2013-01-10 03:21:32 +00:00
|
|
|
## map back to item
|
2012-07-12 19:35:58 +00:00
|
|
|
dti = fn.invertQTransform(dt)
|
2013-01-10 03:21:32 +00:00
|
|
|
#pv = Point(dti.map(normView)-dti.map(Point(0,0))), Point(dti.map(normOrtho)-dti.map(Point(0,0)))
|
|
|
|
pv = Point(dti.map(normView).p2()), Point(dti.map(normOrtho).p2())
|
|
|
|
self._pixelVectorCache[1] = pv
|
|
|
|
self._pixelVectorCache[0] = dt
|
|
|
|
self._pixelVectorGlobalCache[key] = pv
|
|
|
|
return self._pixelVectorCache[1]
|
2012-05-30 03:22:00 +00:00
|
|
|
|
|
|
|
|
|
|
|
def pixelLength(self, direction, ortho=False):
|
|
|
|
"""Return the length of one pixel in the direction indicated (in local coordinates)
|
|
|
|
If ortho=True, then return the length of one pixel orthogonal to the direction indicated.
|
|
|
|
|
|
|
|
Return None if pixel size is not yet defined (usually because the item has not yet been displayed).
|
|
|
|
"""
|
|
|
|
normV, orthoV = self.pixelVectors(direction)
|
|
|
|
if normV == None or orthoV == None:
|
|
|
|
return None
|
|
|
|
if ortho:
|
|
|
|
return orthoV.length()
|
|
|
|
return normV.length()
|
|
|
|
|
2012-03-02 02:55:32 +00:00
|
|
|
|
|
|
|
def pixelSize(self):
|
2012-07-12 19:35:58 +00:00
|
|
|
## deprecated
|
2012-03-02 02:55:32 +00:00
|
|
|
v = self.pixelVectors()
|
2012-05-30 03:22:00 +00:00
|
|
|
if v == (None, None):
|
|
|
|
return None, None
|
2012-03-02 02:55:32 +00:00
|
|
|
return (v[0].x()**2+v[0].y()**2)**0.5, (v[1].x()**2+v[1].y()**2)**0.5
|
|
|
|
|
|
|
|
def pixelWidth(self):
|
2012-07-12 19:35:58 +00:00
|
|
|
## deprecated
|
2012-03-02 02:55:32 +00:00
|
|
|
vt = self.deviceTransform()
|
|
|
|
if vt is None:
|
|
|
|
return 0
|
2012-07-12 19:35:58 +00:00
|
|
|
vt = fn.invertQTransform(vt)
|
2013-01-10 03:21:32 +00:00
|
|
|
return vt.map(QtCore.QLineF(0, 0, 1, 0)).length()
|
2012-03-02 02:55:32 +00:00
|
|
|
|
|
|
|
def pixelHeight(self):
|
2012-07-12 19:35:58 +00:00
|
|
|
## deprecated
|
2012-03-02 02:55:32 +00:00
|
|
|
vt = self.deviceTransform()
|
|
|
|
if vt is None:
|
|
|
|
return 0
|
2012-07-12 19:35:58 +00:00
|
|
|
vt = fn.invertQTransform(vt)
|
2013-01-10 03:21:32 +00:00
|
|
|
return vt.map(QtCore.QLineF(0, 0, 0, 1)).length()
|
|
|
|
#return Point(vt.map(QtCore.QPointF(0, 1))-vt.map(QtCore.QPointF(0, 0))).length()
|
2012-03-02 02:55:32 +00:00
|
|
|
|
|
|
|
|
|
|
|
def mapToDevice(self, obj):
|
|
|
|
"""
|
|
|
|
Return *obj* mapped from local coordinates to device coordinates (pixels).
|
2012-03-11 15:59:45 +00:00
|
|
|
If there is no device mapping available, return None.
|
2012-03-02 02:55:32 +00:00
|
|
|
"""
|
|
|
|
vt = self.deviceTransform()
|
|
|
|
if vt is None:
|
|
|
|
return None
|
|
|
|
return vt.map(obj)
|
|
|
|
|
|
|
|
def mapFromDevice(self, obj):
|
|
|
|
"""
|
|
|
|
Return *obj* mapped from device coordinates (pixels) to local coordinates.
|
2012-03-11 15:59:45 +00:00
|
|
|
If there is no device mapping available, return None.
|
2012-03-02 02:55:32 +00:00
|
|
|
"""
|
|
|
|
vt = self.deviceTransform()
|
|
|
|
if vt is None:
|
|
|
|
return None
|
2014-06-27 14:55:55 +00:00
|
|
|
if isinstance(obj, QtCore.QPoint):
|
|
|
|
obj = QtCore.QPointF(obj)
|
2012-07-12 19:35:58 +00:00
|
|
|
vt = fn.invertQTransform(vt)
|
2012-03-02 02:55:32 +00:00
|
|
|
return vt.map(obj)
|
|
|
|
|
2012-03-11 15:59:45 +00:00
|
|
|
def mapRectToDevice(self, rect):
|
|
|
|
"""
|
|
|
|
Return *rect* mapped from local coordinates to device coordinates (pixels).
|
|
|
|
If there is no device mapping available, return None.
|
|
|
|
"""
|
|
|
|
vt = self.deviceTransform()
|
|
|
|
if vt is None:
|
|
|
|
return None
|
|
|
|
return vt.mapRect(rect)
|
|
|
|
|
|
|
|
def mapRectFromDevice(self, rect):
|
|
|
|
"""
|
|
|
|
Return *rect* mapped from device coordinates (pixels) to local coordinates.
|
|
|
|
If there is no device mapping available, return None.
|
|
|
|
"""
|
|
|
|
vt = self.deviceTransform()
|
|
|
|
if vt is None:
|
|
|
|
return None
|
2012-07-12 19:35:58 +00:00
|
|
|
vt = fn.invertQTransform(vt)
|
2012-03-11 15:59:45 +00:00
|
|
|
return vt.mapRect(rect)
|
|
|
|
|
2012-03-02 02:55:32 +00:00
|
|
|
def mapToView(self, obj):
|
|
|
|
vt = self.viewTransform()
|
|
|
|
if vt is None:
|
|
|
|
return None
|
|
|
|
return vt.map(obj)
|
|
|
|
|
|
|
|
def mapRectToView(self, obj):
|
|
|
|
vt = self.viewTransform()
|
|
|
|
if vt is None:
|
|
|
|
return None
|
|
|
|
return vt.mapRect(obj)
|
|
|
|
|
|
|
|
def mapFromView(self, obj):
|
|
|
|
vt = self.viewTransform()
|
|
|
|
if vt is None:
|
|
|
|
return None
|
2012-07-12 19:35:58 +00:00
|
|
|
vt = fn.invertQTransform(vt)
|
2012-03-02 02:55:32 +00:00
|
|
|
return vt.map(obj)
|
|
|
|
|
|
|
|
def mapRectFromView(self, obj):
|
|
|
|
vt = self.viewTransform()
|
|
|
|
if vt is None:
|
|
|
|
return None
|
2012-07-12 19:35:58 +00:00
|
|
|
vt = fn.invertQTransform(vt)
|
2012-03-02 02:55:32 +00:00
|
|
|
return vt.mapRect(obj)
|
|
|
|
|
|
|
|
def pos(self):
|
2012-06-18 23:40:15 +00:00
|
|
|
return Point(self._qtBaseClass.pos(self))
|
2012-03-02 02:55:32 +00:00
|
|
|
|
|
|
|
def viewPos(self):
|
2012-03-20 03:02:29 +00:00
|
|
|
return self.mapToView(self.mapFromParent(self.pos()))
|
2012-03-02 02:55:32 +00:00
|
|
|
|
|
|
|
def parentItem(self):
|
|
|
|
## PyQt bug -- some items are returned incorrectly.
|
2012-06-18 23:40:15 +00:00
|
|
|
return GraphicsScene.translateGraphicsItem(self._qtBaseClass.parentItem(self))
|
2012-03-02 02:55:32 +00:00
|
|
|
|
2012-06-18 23:40:15 +00:00
|
|
|
def setParentItem(self, parent):
|
|
|
|
## Workaround for Qt bug: https://bugreports.qt-project.org/browse/QTBUG-18616
|
|
|
|
if parent is not None:
|
|
|
|
pscene = parent.scene()
|
|
|
|
if pscene is not None and self.scene() is not pscene:
|
|
|
|
pscene.addItem(self)
|
|
|
|
return self._qtBaseClass.setParentItem(self, parent)
|
2012-03-02 02:55:32 +00:00
|
|
|
|
|
|
|
def childItems(self):
|
|
|
|
## PyQt bug -- some child items are returned incorrectly.
|
2012-06-18 23:40:15 +00:00
|
|
|
return list(map(GraphicsScene.translateGraphicsItem, self._qtBaseClass.childItems(self)))
|
2012-03-02 02:55:32 +00:00
|
|
|
|
|
|
|
|
|
|
|
def sceneTransform(self):
|
|
|
|
## Qt bug: do no allow access to sceneTransform() until
|
|
|
|
## the item has a scene.
|
|
|
|
|
|
|
|
if self.scene() is None:
|
|
|
|
return self.transform()
|
|
|
|
else:
|
2012-06-18 23:40:15 +00:00
|
|
|
return self._qtBaseClass.sceneTransform(self)
|
2012-03-02 02:55:32 +00:00
|
|
|
|
|
|
|
|
|
|
|
def transformAngle(self, relativeItem=None):
|
|
|
|
"""Return the rotation produced by this item's transform (this assumes there is no shear in the transform)
|
|
|
|
If relativeItem is given, then the angle is determined relative to that item.
|
|
|
|
"""
|
|
|
|
if relativeItem is None:
|
|
|
|
relativeItem = self.parentItem()
|
|
|
|
|
2012-03-02 03:53:52 +00:00
|
|
|
|
|
|
|
tr = self.itemTransform(relativeItem)
|
|
|
|
if isinstance(tr, tuple): ## difference between pyside and pyqt
|
2013-01-10 03:21:32 +00:00
|
|
|
tr = tr[0]
|
|
|
|
#vec = tr.map(Point(1,0)) - tr.map(Point(0,0))
|
|
|
|
vec = tr.map(QtCore.QLineF(0,0,1,0))
|
|
|
|
#return Point(vec).angle(Point(1,0))
|
|
|
|
return vec.angleTo(QtCore.QLineF(vec.p1(), vec.p1()+QtCore.QPointF(1,0)))
|
2012-03-02 02:55:32 +00:00
|
|
|
|
2012-05-15 02:05:53 +00:00
|
|
|
#def itemChange(self, change, value):
|
2012-06-18 23:40:15 +00:00
|
|
|
#ret = self._qtBaseClass.itemChange(self, change, value)
|
2012-05-15 02:05:53 +00:00
|
|
|
#if change == self.ItemParentHasChanged or change == self.ItemSceneHasChanged:
|
|
|
|
#print "Item scene changed:", self
|
|
|
|
#self.setChildScene(self) ## This is bizarre.
|
|
|
|
#return ret
|
|
|
|
|
|
|
|
#def setChildScene(self, ch):
|
|
|
|
#scene = self.scene()
|
|
|
|
#for ch2 in ch.childItems():
|
|
|
|
#if ch2.scene() is not scene:
|
|
|
|
#print "item", ch2, "has different scene:", ch2.scene(), scene
|
|
|
|
#scene.addItem(ch2)
|
|
|
|
#QtGui.QApplication.processEvents()
|
|
|
|
#print " --> ", ch2.scene()
|
|
|
|
#self.setChildScene(ch2)
|
|
|
|
|
2013-03-26 17:46:26 +00:00
|
|
|
def parentChanged(self):
|
|
|
|
"""Called when the item's parent has changed.
|
|
|
|
This method handles connecting / disconnecting from ViewBox signals
|
|
|
|
to make sure viewRangeChanged works properly. It should generally be
|
|
|
|
extended, not overridden."""
|
|
|
|
self._updateView()
|
|
|
|
|
|
|
|
|
2012-05-15 02:05:53 +00:00
|
|
|
def _updateView(self):
|
|
|
|
## called to see whether this item has a new view to connect to
|
|
|
|
## NOTE: This is called from GraphicsObject.itemChange or GraphicsWidget.itemChange.
|
|
|
|
|
|
|
|
## It is possible this item has moved to a different ViewBox or widget;
|
|
|
|
## clear out previously determined references to these.
|
|
|
|
self.forgetViewBox()
|
|
|
|
self.forgetViewWidget()
|
2012-03-02 02:55:32 +00:00
|
|
|
|
2012-05-15 02:05:53 +00:00
|
|
|
## check for this item's current viewbox or view widget
|
|
|
|
view = self.getViewBox()
|
2012-06-18 18:00:19 +00:00
|
|
|
#if view is None:
|
|
|
|
##print " no view"
|
|
|
|
#return
|
2012-05-15 02:05:53 +00:00
|
|
|
|
2012-06-18 18:00:19 +00:00
|
|
|
oldView = None
|
|
|
|
if self._connectedView is not None:
|
|
|
|
oldView = self._connectedView()
|
|
|
|
|
|
|
|
if view is oldView:
|
2012-05-15 02:05:53 +00:00
|
|
|
#print " already have view", view
|
|
|
|
return
|
|
|
|
|
|
|
|
## disconnect from previous view
|
2012-06-18 18:00:19 +00:00
|
|
|
if oldView is not None:
|
2014-02-07 15:38:41 +00:00
|
|
|
for signal, slot in [('sigRangeChanged', self.viewRangeChanged),
|
|
|
|
('sigDeviceRangeChanged', self.viewRangeChanged),
|
|
|
|
('sigTransformChanged', self.viewTransformChanged),
|
|
|
|
('sigDeviceTransformChanged', self.viewTransformChanged)]:
|
|
|
|
try:
|
|
|
|
getattr(oldView, signal).disconnect(slot)
|
|
|
|
except (TypeError, AttributeError, RuntimeError):
|
|
|
|
# TypeError and RuntimeError are from pyqt and pyside, respectively
|
|
|
|
pass
|
2012-12-22 21:51:25 +00:00
|
|
|
|
2012-06-18 18:00:19 +00:00
|
|
|
self._connectedView = None
|
2012-05-15 02:05:53 +00:00
|
|
|
|
|
|
|
## connect to new view
|
2012-06-18 18:00:19 +00:00
|
|
|
if view is not None:
|
|
|
|
#print "connect:", self, view
|
2014-02-07 15:38:41 +00:00
|
|
|
if hasattr(view, 'sigDeviceRangeChanged'):
|
|
|
|
# connect signals from GraphicsView
|
|
|
|
view.sigDeviceRangeChanged.connect(self.viewRangeChanged)
|
|
|
|
view.sigDeviceTransformChanged.connect(self.viewTransformChanged)
|
|
|
|
else:
|
|
|
|
# connect signals from ViewBox
|
|
|
|
view.sigRangeChanged.connect(self.viewRangeChanged)
|
|
|
|
view.sigTransformChanged.connect(self.viewTransformChanged)
|
2012-06-18 18:00:19 +00:00
|
|
|
self._connectedView = weakref.ref(view)
|
|
|
|
self.viewRangeChanged()
|
2012-10-22 17:34:03 +00:00
|
|
|
self.viewTransformChanged()
|
2012-06-18 18:00:19 +00:00
|
|
|
|
|
|
|
## inform children that their view might have changed
|
|
|
|
self._replaceView(oldView)
|
|
|
|
|
2013-03-26 17:46:26 +00:00
|
|
|
self.viewChanged(view, oldView)
|
|
|
|
|
|
|
|
def viewChanged(self, view, oldView):
|
|
|
|
"""Called when this item's view has changed
|
|
|
|
(ie, the item has been added to or removed from a ViewBox)"""
|
|
|
|
pass
|
2012-06-18 18:00:19 +00:00
|
|
|
|
|
|
|
def _replaceView(self, oldView, item=None):
|
|
|
|
if item is None:
|
|
|
|
item = self
|
|
|
|
for child in item.childItems():
|
|
|
|
if isinstance(child, GraphicsItem):
|
|
|
|
if child.getViewBox() is oldView:
|
|
|
|
child._updateView()
|
|
|
|
#self._replaceView(oldView, child)
|
|
|
|
else:
|
|
|
|
self._replaceView(oldView, child)
|
|
|
|
|
|
|
|
|
2012-05-15 02:05:53 +00:00
|
|
|
|
|
|
|
def viewRangeChanged(self):
|
|
|
|
"""
|
|
|
|
Called whenever the view coordinates of the ViewBox containing this item have changed.
|
|
|
|
"""
|
|
|
|
pass
|
2012-10-22 17:34:03 +00:00
|
|
|
|
|
|
|
def viewTransformChanged(self):
|
|
|
|
"""
|
|
|
|
Called whenever the transformation matrix of the view has changed.
|
2013-07-03 15:20:49 +00:00
|
|
|
(eg, the view range has changed or the view was resized)
|
2012-10-22 17:34:03 +00:00
|
|
|
"""
|
2012-10-31 05:57:00 +00:00
|
|
|
pass
|
|
|
|
|
|
|
|
#def prepareGeometryChange(self):
|
|
|
|
#self._qtBaseClass.prepareGeometryChange(self)
|
|
|
|
#self.informViewBoundsChanged()
|
|
|
|
|
|
|
|
def informViewBoundsChanged(self):
|
|
|
|
"""
|
|
|
|
Inform this item's container ViewBox that the bounds of this item have changed.
|
|
|
|
This is used by ViewBox to react if auto-range is enabled.
|
|
|
|
"""
|
|
|
|
view = self.getViewBox()
|
|
|
|
if view is not None and hasattr(view, 'implements') and view.implements('ViewBox'):
|
|
|
|
view.itemBoundsChanged(self) ## inform view so it can update its range if it wants
|
2012-12-22 21:51:25 +00:00
|
|
|
|
|
|
|
def childrenShape(self):
|
|
|
|
"""Return the union of the shapes of all descendants of this item in local coordinates."""
|
|
|
|
childs = self.allChildItems()
|
|
|
|
shapes = [self.mapFromItem(c, c.shape()) for c in self.allChildItems()]
|
|
|
|
return reduce(operator.add, shapes)
|
|
|
|
|
|
|
|
def allChildItems(self, root=None):
|
|
|
|
"""Return list of the entire item tree descending from this item."""
|
|
|
|
if root is None:
|
|
|
|
root = self
|
|
|
|
tree = []
|
|
|
|
for ch in root.childItems():
|
|
|
|
tree.append(ch)
|
|
|
|
tree.extend(self.allChildItems(ch))
|
|
|
|
return tree
|
|
|
|
|
|
|
|
|
2012-12-25 05:43:31 +00:00
|
|
|
def setExportMode(self, export, opts=None):
|
2012-12-26 21:29:29 +00:00
|
|
|
"""
|
|
|
|
This method is called by exporters to inform items that they are being drawn for export
|
|
|
|
with a specific set of options. Items access these via self._exportOptions.
|
|
|
|
When exporting is complete, _exportOptions is set to False.
|
|
|
|
"""
|
2012-12-25 05:43:31 +00:00
|
|
|
if opts is None:
|
|
|
|
opts = {}
|
2012-12-26 21:29:29 +00:00
|
|
|
if export:
|
|
|
|
self._exportOpts = opts
|
|
|
|
#if 'antialias' not in opts:
|
|
|
|
#self._exportOpts['antialias'] = True
|
|
|
|
else:
|
|
|
|
self._exportOpts = False
|
2012-12-26 23:54:12 +00:00
|
|
|
|
2013-01-10 03:21:32 +00:00
|
|
|
#def update(self):
|
|
|
|
#self._qtBaseClass.update(self)
|
2013-01-12 19:31:49 +00:00
|
|
|
#print "Update:", self
|
2013-11-20 20:13:35 +00:00
|
|
|
|
|
|
|
def getContextMenus(self, event):
|
|
|
|
return [self.getMenu()] if hasattr(self, "getMenu") else []
|