Commit 6932c341 authored by Luke Campagnola's avatar Luke Campagnola
Browse files

- Added workaround for Qt bug:...

- Added workaround for Qt bug: https://bugreports.qt-project.org/browse/QTBUG-18616. (GraphicsItem.setParent needs to check for scene change first)
This _could_ cause other problems, but they will certainly be fewer than the existing problems.

- Fixed bugs with ViewBox linking to views which are subsequently deleted
parent cc94e15d
......@@ -26,19 +26,19 @@ win.addLabel("Linked Views", colspan=2)
win.nextRow()
p1 = win.addPlot(x=x, y=y, name="Plot1", title="Plot1")
p2 = win.addPlot(x=x, y=y, name="Plot2", title="Plot2 - Y linked with Plot1")
p2 = win.addPlot(x=x, y=y, name="Plot2", title="Plot2: Y linked with Plot1")
p2.setLabel('bottom', "Label to test offset")
p2.setYLink(p1)
p2.setYLink('Plot1') ## test linking by name
win.nextRow()
p3 = win.addPlot(x=x, y=y, name="Plot3", title="Plot3 - X linked with Plot1")
p4 = win.addPlot(x=x, y=y, name="Plot4", title="Plot4 - X and Y linked with Plot1")
## create plots 3 and 4 out of order
p4 = win.addPlot(x=x, y=y, name="Plot4", title="Plot4: X -> Plot3 (deferred), Y -> Plot1", row=2, col=1)
p4.setXLink('Plot3') ## Plot3 has not been created yet, but this should still work anyway.
p4.setYLink(p1)
p3 = win.addPlot(x=x, y=y, name="Plot3", title="Plot3: X linked with Plot1", row=2, col=0)
p3.setXLink(p1)
p3.setLabel('left', "Label to test offset")
#QtGui.QApplication.processEvents()
p3.setXLink(p1)
p4.setXLink(p1)
p4.setYLink(p1)
## Start Qt event loop unless running in interactive mode or using pyside.
......
......@@ -510,7 +510,7 @@ class NodeGraphicsItem(GraphicsObject):
if change == self.ItemPositionHasChanged:
for k, t in self.terminals.items():
t[1].nodeMoved()
return QtGui.QGraphicsItem.itemChange(self, change, val)
return GraphicsObject.itemChange(self, change, val)
#def contextMenuEvent(self, ev):
......
......@@ -342,26 +342,47 @@ class GraphicsItem(object):
## check for this item's current viewbox or view widget
view = self.getViewBox()
if view is None:
#print " no view"
return
#if view is None:
##print " no view"
#return
if self._connectedView is not None and view is self._connectedView():
oldView = None
if self._connectedView is not None:
oldView = self._connectedView()
if view is oldView:
#print " already have view", view
return
## disconnect from previous view
if self._connectedView is not None:
cv = self._connectedView()
if cv is not None:
#print "disconnect:", self
cv.sigRangeChanged.disconnect(self.viewRangeChanged)
if oldView is not None:
#print "disconnect:", self, oldView
oldView.sigRangeChanged.disconnect(self.viewRangeChanged)
self._connectedView = None
## connect to new view
#print "connect:", self
view.sigRangeChanged.connect(self.viewRangeChanged)
self._connectedView = weakref.ref(view)
self.viewRangeChanged()
if view is not None:
#print "connect:", self, view
view.sigRangeChanged.connect(self.viewRangeChanged)
self._connectedView = weakref.ref(view)
self.viewRangeChanged()
## inform children that their view might have changed
self._replaceView(oldView)
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)
def viewRangeChanged(self):
"""
......
......@@ -22,7 +22,8 @@ class GraphicsObject(GraphicsItem, QtGui.QGraphicsObject):
def setParentItem(self, parent):
## Workaround for Qt bug: https://bugreports.qt-project.org/browse/QTBUG-18616
pscene = parent.scene()
if pscene is not None and self.scene() is not pscene:
pscene.addItem(self)
if parent is not None:
pscene = parent.scene()
if pscene is not None and self.scene() is not pscene:
pscene.addItem(self)
return QtGui.QGraphicsObject.setParentItem(self, parent)
......@@ -55,7 +55,8 @@ class GraphicsWidget(GraphicsItem, QtGui.QGraphicsWidget):
def setParentItem(self, parent):
## Workaround for Qt bug: https://bugreports.qt-project.org/browse/QTBUG-18616
pscene = parent.scene()
if pscene is not None and self.scene() is not pscene:
pscene.addItem(self)
return QtGui.QGraphicsWidget.setParentItem(self, parent)
if parent is not None:
pscene = parent.scene()
if pscene is not None and self.scene() is not pscene:
pscene.addItem(self)
return QtGui.QGraphicsObject.setParentItem(self, parent)
......@@ -62,7 +62,6 @@ class ViewBox(GraphicsWidget):
NamedViews = weakref.WeakValueDictionary() # name: ViewBox
AllViews = weakref.WeakKeyDictionary() # ViewBox: None
def __init__(self, parent=None, border=None, lockAspect=False, enableMouse=True, invertY=False, name=None):
"""
============= =============================================================
......@@ -99,7 +98,8 @@ class ViewBox(GraphicsWidget):
## otherwise float gives the fraction of data that is visible
'autoPan': [False, False], ## whether to only pan (do not change scaling) when auto-range is enabled
'autoVisibleOnly': [False, False], ## whether to auto-range only to the visible portion of a plot
'linkedViews': [None, None],
'linkedViews': [None, None], ## may be None, "viewName", or weakref.ref(view)
## a name string indicates that the view *should* link to another, but no view with that name exists yet.
'mouseEnabled': [enableMouse, enableMouse],
'mouseMode': ViewBox.PanMode if pyqtgraph.getConfigOption('leftButtonPan') else ViewBox.RectMode,
......@@ -160,6 +160,8 @@ class ViewBox(GraphicsWidget):
if name is not None:
ViewBox.NamedViews[name] = self
ViewBox.updateAllViewLists()
self.destroyed.connect(lambda: ViewBox.forgetView(id(self), self.name))
#self.destroyed.connect(self.unregister)
def unregister(self):
"""
......@@ -177,14 +179,26 @@ class ViewBox(GraphicsWidget):
def getState(self, copy=True):
"""Return the current state of the ViewBox.
Linked views are always converted to view names in the returned state."""
state = self.state.copy()
state['linkedViews'] = [(None if v is None else v.name) for v in state['linkedViews']]
views = []
for v in state['linkedViews']:
if isinstance(v, weakref.ref):
v = v()
if v is None or isinstance(v, basestring):
views.append(v)
else:
views.append(v.name)
state['linkedViews'] = views
if copy:
return deepcopy(self.state)
return deepcopy(state)
else:
return self.state
return state
def setState(self, state):
"""Restore the state of this ViewBox.
(see also getState)"""
state = state.copy()
self.setXLink(state['linkedViews'][0])
self.setYLink(state['linkedViews'][1])
......@@ -368,7 +382,7 @@ class ViewBox(GraphicsWidget):
self.updateMatrix(changed)
for ax, range in changes.items():
link = self.state['linkedViews'][ax]
link = self.linkedView(ax)
if link is not None:
link.linkedViewChanged(self, ax)
......@@ -572,7 +586,7 @@ class ViewBox(GraphicsWidget):
if view == '':
view = None
else:
view = ViewBox.NamedViews[view]
view = ViewBox.NamedViews.get(view, view) ## convert view name to ViewBox if possible
if hasattr(view, 'implements') and view.implements('ViewBoxWrapper'):
view = view.getViewBox()
......@@ -586,13 +600,19 @@ class ViewBox(GraphicsWidget):
slot = self.linkedYChanged
oldLink = self.state['linkedViews'][axis]
oldLink = self.linkedView(axis)
if oldLink is not None:
getattr(oldLink, signal).disconnect(slot)
try:
getattr(oldLink, signal).disconnect(slot)
except TypeError:
## This can occur if the view has been deleted already
pass
self.state['linkedViews'][axis] = view
if view is not None:
if view is None or isinstance(view, basestring):
self.state['linkedViews'][axis] = view
else:
self.state['linkedViews'][axis] = weakref.ref(view)
getattr(view, signal).connect(slot)
if view.autoRangeEnabled()[axis] is not False:
self.enableAutoRange(axis, False)
......@@ -608,14 +628,22 @@ class ViewBox(GraphicsWidget):
def linkedXChanged(self):
## called when x range of linked view has changed
view = self.state['linkedViews'][0]
view = self.linkedView(0)
self.linkedViewChanged(view, ViewBox.XAxis)
def linkedYChanged(self):
## called when y range of linked view has changed
view = self.state['linkedViews'][1]
view = self.linkedView(1)
self.linkedViewChanged(view, ViewBox.YAxis)
def linkedView(self, ax):
## Return the linked view for axis *ax*.
## this method _always_ returns either a ViewBox or None.
v = self.state['linkedViews'][ax]
if v is None or isinstance(v, basestring):
return None
else:
return v() ## dereference weakref pointer. If the reference is dead, this returns None
def linkedViewChanged(self, view, axis):
if self.linksBlocked or view is None:
......@@ -623,10 +651,9 @@ class ViewBox(GraphicsWidget):
vr = view.viewRect()
vg = view.screenGeometry()
if vg is None:
return
sg = self.screenGeometry()
if vg is None or sg is None:
return
view.blockLink(True)
try:
......@@ -683,8 +710,11 @@ class ViewBox(GraphicsWidget):
By default, the positive y-axis points upward on the screen. Use invertY(True) to reverse the y-axis.
"""
self.state['yInverted'] = b
self.updateMatrix()
self.updateMatrix(changed=(False, True))
self.sigStateChanged.emit(self)
def yInverted(self):
return self.state['yInverted']
def setAspectLocked(self, lock=True, ratio=1):
"""
......@@ -1030,6 +1060,7 @@ class ViewBox(GraphicsWidget):
def updateMatrix(self, changed=None):
if changed is None:
changed = [False, False]
changed = list(changed)
#print "udpateMatrix:"
#print " range:", self.range
tr = self.targetRect()
......@@ -1124,20 +1155,40 @@ class ViewBox(GraphicsWidget):
## make a sorted list of all named views
nv = list(ViewBox.NamedViews.values())
#print "new view list:", nv
sortList(nv, cmpViews) ## see pyqtgraph.python2_3.sortList
if self in nv:
nv.remove(self)
names = [v.name for v in nv]
self.menu.setViewList(names)
self.menu.setViewList(nv)
for ax in [0,1]:
link = self.state['linkedViews'][ax]
if isinstance(link, basestring): ## axis has not been linked yet; see if it's possible now
for v in nv:
if link == v.name:
self.linkView(ax, v)
#print "New view list:", nv
#print "linked views:", self.state['linkedViews']
@staticmethod
def updateAllViewLists():
#print "Update:", ViewBox.AllViews.keys()
#print "Update:", ViewBox.NamedViews.keys()
for v in ViewBox.AllViews:
v.updateViewLists()
@staticmethod
def forgetView(vid, name):
## Called with ID and name of view (the view itself is no longer available)
for v in ViewBox.AllViews.iterkeys():
if id(v) == vid:
ViewBox.AllViews.pop(v)
break
ViewBox.NamedViews.pop(name, None)
ViewBox.updateAllViewLists()
from .ViewBoxMenu import ViewBoxMenu
from pyqtgraph.Qt import QtCore, QtGui
from pyqtgraph.WidgetGroup import WidgetGroup
from .axisCtrlTemplate import Ui_Form as AxisCtrlTemplate
import weakref
class ViewBoxMenu(QtGui.QMenu):
def __init__(self, view):
QtGui.QMenu.__init__(self)
self.view = view
self.view = weakref.ref(view) ## keep weakref to view to avoid circular reference (don't know why, but this prevents the ViewBox from being collected)
self.valid = False ## tells us whether the ui needs to be updated
self.viewMap = weakref.WeakValueDictionary() ## weakrefs to all views listed in the link combos
self.setTitle("ViewBox options")
self.viewAll = QtGui.QAction("View All", self)
......@@ -47,7 +49,9 @@ class ViewBoxMenu(QtGui.QMenu):
for sig, fn in connects:
sig.connect(getattr(self, axis.lower()+fn))
self.ctrl[0].invertCheck.hide() ## no invert for x-axis
self.ctrl[1].invertCheck.toggled.connect(self.yInvertToggled)
## exporting is handled by GraphicsScene now
#self.export = QtGui.QMenu("Export")
#self.setExportMethods(view.exportMethods)
......@@ -64,7 +68,7 @@ class ViewBoxMenu(QtGui.QMenu):
self.mouseModes = [pan, zoom]
self.addMenu(self.leftMenu)
self.view.sigStateChanged.connect(self.viewStateChanged)
self.view().sigStateChanged.connect(self.viewStateChanged)
self.updateState()
......@@ -97,14 +101,15 @@ class ViewBoxMenu(QtGui.QMenu):
self.updateState()
def updateState(self):
state = self.view.getState(copy=False)
## Something about the viewbox has changed; update the menu GUI
state = self.view().getState(copy=False)
if state['mouseMode'] == ViewBox.PanMode:
self.mouseModes[0].setChecked(True)
else:
self.mouseModes[1].setChecked(True)
for i in [0,1]:
for i in [0,1]: # x, y
tr = state['targetRange'][i]
self.ctrl[i].minText.setText("%0.5g" % tr[0])
self.ctrl[i].maxText.setText("%0.5g" % tr[1])
......@@ -116,17 +121,15 @@ class ViewBoxMenu(QtGui.QMenu):
self.ctrl[i].manualRadio.setChecked(True)
self.ctrl[i].mouseCheck.setChecked(state['mouseEnabled'][i])
## Update combo to show currently linked view
c = self.ctrl[i].linkCombo
c.blockSignals(True)
try:
view = state['linkedViews'][i]
view = state['linkedViews'][i] ## will always be string or None
if view is None:
view = ''
if isinstance(view, basestring):
ind = c.findText(view)
else:
ind = c.findText(view.name)
ind = c.findText(view)
if ind == -1:
ind = 0
......@@ -136,76 +139,79 @@ class ViewBoxMenu(QtGui.QMenu):
self.ctrl[i].autoPanCheck.setChecked(state['autoPan'][i])
self.ctrl[i].visibleOnlyCheck.setChecked(state['autoVisibleOnly'][i])
self.ctrl[1].invertCheck.setChecked(state['yInverted'])
self.valid = True
def autoRange(self):
self.view.autoRange() ## don't let signal call this directly--it'll add an unwanted argument
self.view().autoRange() ## don't let signal call this directly--it'll add an unwanted argument
def xMouseToggled(self, b):
self.view.setMouseEnabled(x=b)
self.view().setMouseEnabled(x=b)
def xManualClicked(self):
self.view.enableAutoRange(ViewBox.XAxis, False)
self.view().enableAutoRange(ViewBox.XAxis, False)
def xMinTextChanged(self):
self.ctrl[0].manualRadio.setChecked(True)
self.view.setXRange(float(self.ctrl[0].minText.text()), float(self.ctrl[0].maxText.text()), padding=0)
self.view().setXRange(float(self.ctrl[0].minText.text()), float(self.ctrl[0].maxText.text()), padding=0)
def xMaxTextChanged(self):
self.ctrl[0].manualRadio.setChecked(True)
self.view.setXRange(float(self.ctrl[0].minText.text()), float(self.ctrl[0].maxText.text()), padding=0)
self.view().setXRange(float(self.ctrl[0].minText.text()), float(self.ctrl[0].maxText.text()), padding=0)
def xAutoClicked(self):
val = self.ctrl[0].autoPercentSpin.value() * 0.01
self.view.enableAutoRange(ViewBox.XAxis, val)
self.view().enableAutoRange(ViewBox.XAxis, val)
def xAutoSpinChanged(self, val):
self.ctrl[0].autoRadio.setChecked(True)
self.view.enableAutoRange(ViewBox.XAxis, val*0.01)
self.view().enableAutoRange(ViewBox.XAxis, val*0.01)
def xLinkComboChanged(self, ind):
self.view.setXLink(str(self.ctrl[0].linkCombo.currentText()))
self.view().setXLink(str(self.ctrl[0].linkCombo.currentText()))
def xAutoPanToggled(self, b):
self.view.setAutoPan(x=b)
self.view().setAutoPan(x=b)
def xVisibleOnlyToggled(self, b):
self.view.setAutoVisible(x=b)
self.view().setAutoVisible(x=b)
def yMouseToggled(self, b):
self.view.setMouseEnabled(y=b)
self.view().setMouseEnabled(y=b)
def yManualClicked(self):
self.view.enableAutoRange(ViewBox.YAxis, False)
self.view().enableAutoRange(ViewBox.YAxis, False)
def yMinTextChanged(self):
self.ctrl[1].manualRadio.setChecked(True)
self.view.setYRange(float(self.ctrl[1].minText.text()), float(self.ctrl[1].maxText.text()), padding=0)
self.view().setYRange(float(self.ctrl[1].minText.text()), float(self.ctrl[1].maxText.text()), padding=0)
def yMaxTextChanged(self):
self.ctrl[1].manualRadio.setChecked(True)
self.view.setYRange(float(self.ctrl[1].minText.text()), float(self.ctrl[1].maxText.text()), padding=0)
self.view().setYRange(float(self.ctrl[1].minText.text()), float(self.ctrl[1].maxText.text()), padding=0)
def yAutoClicked(self):
val = self.ctrl[1].autoPercentSpin.value() * 0.01
self.view.enableAutoRange(ViewBox.YAxis, val)
self.view().enableAutoRange(ViewBox.YAxis, val)
def yAutoSpinChanged(self, val):
self.ctrl[1].autoRadio.setChecked(True)
self.view.enableAutoRange(ViewBox.YAxis, val*0.01)
self.view().enableAutoRange(ViewBox.YAxis, val*0.01)
def yLinkComboChanged(self, ind):
self.view.setYLink(str(self.ctrl[1].linkCombo.currentText()))
self.view().setYLink(str(self.ctrl[1].linkCombo.currentText()))
def yAutoPanToggled(self, b):
self.view.setAutoPan(y=b)
self.view().setAutoPan(y=b)
def yVisibleOnlyToggled(self, b):
self.view.setAutoVisible(y=b)
self.view().setAutoVisible(y=b)
def yInvertToggled(self, b):
self.view().invertY(b)
def exportMethod(self):
......@@ -214,14 +220,24 @@ class ViewBoxMenu(QtGui.QMenu):
def set3ButtonMode(self):
self.view.setLeftButtonAction('pan')
self.view().setLeftButtonAction('pan')
def set1ButtonMode(self):
self.view.setLeftButtonAction('rect')
self.view().setLeftButtonAction('rect')
def setViewList(self, views):
views = [''] + views
names = ['']
self.viewMap.clear()
## generate list of views to show in the link combo
for v in views:
name = v.name
if name is None: ## unnamed views do not show up in the view list (although they are linkable)
continue
names.append(name)
self.viewMap[name] = v
for i in [0,1]:
c = self.ctrl[i].linkCombo
current = asUnicode(c.currentText())
......@@ -229,9 +245,9 @@ class ViewBoxMenu(QtGui.QMenu):
changed = True
try:
c.clear()
for v in views:
c.addItem(v)
if v == current:
for name in names:
c.addItem(name)
if name == current:
changed = False
c.setCurrentIndex(c.count()-1)
finally:
......
......@@ -2,8 +2,8 @@
# Form implementation generated from reading ui file 'axisCtrlTemplate.ui'
#
# Created: Wed Mar 28 23:29:45 2012
# by: PyQt4 UI code generator 4.8.3
# Created: Fri Jun 1 17:38:02 2012
# by: PyQt4 UI code generator 4.9.1
#
# WARNING! All changes made in this file will be lost!
......@@ -17,12 +17,31 @@ except AttributeError:
class Ui_Form(object):
def setupUi(self, Form):
Form.setObjectName(_fromUtf8("Form"))
Form.resize(186, 137)
Form.resize(186, 154)
Form.setMaximumSize(QtCore.QSize(200, 16777215))
self.gridLayout = QtGui.QGridLayout(Form)
self.gridLayout.setMargin(0)
self.gridLayout.setSpacing(0)
self.gridLayout.setObjectName(_fromUtf8("gridLayout"))
self.label = QtGui.QLabel(Form)
self.label.setObjectName(_fromUtf8("label"))
self.gridLayout.addWidget(self.label, 7, 0, 1, 2)
self.linkCombo = QtGui.QComboBox(Form)
self.linkCombo.setSizeAdjustPolicy(QtGui.QComboBox.AdjustToContents)
self.linkCombo.setObjectName(_fromUtf8("linkCombo"))
self.gridLayout.addWidget(self.linkCombo, 7, 2, 1, 2)
self.autoPercentSpin = QtGui.QSpinBox(Form)
self.autoPercentSpin.setEnabled(True)
self.autoPercentSpin.setMinimum(1)
self.autoPercentSpin.setMaximum(100)
self.autoPercentSpin.setSingleStep(1)
self.autoPercentSpin.setProperty("value", 100)
self.autoPercentSpin.setObjectName(_fromUtf8("autoPercentSpin"))
self.gridLayout.addWidget(self.autoPercentSpin, 2, 2, 1, 2)
self.autoRadio = QtGui.QRadioButton(Form)
self.autoRadio.setChecked(True)
self.autoRadio.setObjectName(_fromUtf8("autoRadio"))
self.gridLayout.addWidget(self.autoRadio, 2, 0, 1, 2)
self.manualRadio = QtGui.QRadioButton(Form)
self.manualRadio.setObjectName(_fromUtf8("manualRadio"))
self.gridLayout.addWidget(self.manualRadio, 1, 0, 1, 2)
......@@ -32,48 +51,43 @@ class Ui_Form(object):
self.maxText = QtGui.QLineEdit(Form)
self.maxText.setObjectName(_fromUtf8("maxText"))
self.gridLayout.addWidget(self.maxText, 1, 3, 1, 1)
self.autoRadio = QtGui.QRadioButton(Form)
self.autoRadio.setChecked(True)
self.autoRadio.setObjectName(_fromUtf8("autoRadio"))
self.gridLayout.addWidget(self.autoRadio, 2, 0, 1, 2)
self.autoPercentSpin = QtGui.QSpinBox(Form)
self.autoPercentSpin