Make behavior configurable when a reloaded dock is missing.
+ other bugfixes
This commit is contained in:
parent
dd672c41b6
commit
b6f9516678
@ -17,16 +17,20 @@ class Container(object):
|
||||
|
||||
def containerChanged(self, c):
|
||||
self._container = c
|
||||
if c is None:
|
||||
self.area = None
|
||||
else:
|
||||
self.area = c.area
|
||||
|
||||
def type(self):
|
||||
return None
|
||||
|
||||
def insert(self, new, pos=None, neighbor=None):
|
||||
# remove from existing parent first
|
||||
new.setParent(None)
|
||||
|
||||
if not isinstance(new, list):
|
||||
new = [new]
|
||||
for n in new:
|
||||
# remove from existing parent first
|
||||
n.setParent(None)
|
||||
if neighbor is None:
|
||||
if pos == 'before':
|
||||
index = 0
|
||||
@ -40,34 +44,37 @@ class Container(object):
|
||||
index += 1
|
||||
|
||||
for n in new:
|
||||
#print "change container", n, " -> ", self
|
||||
n.containerChanged(self)
|
||||
#print "insert", n, " -> ", self, index
|
||||
self._insertItem(n, index)
|
||||
#print "change container", n, " -> ", self
|
||||
n.containerChanged(self)
|
||||
index += 1
|
||||
n.sigStretchChanged.connect(self.childStretchChanged)
|
||||
#print "child added", self
|
||||
self.updateStretch()
|
||||
|
||||
def apoptose(self, propagate=True):
|
||||
##if there is only one (or zero) item in this container, disappear.
|
||||
# if there is only one (or zero) item in this container, disappear.
|
||||
# if propagate is True, then also attempt to apoptose parent containers.
|
||||
cont = self._container
|
||||
c = self.count()
|
||||
if c > 1:
|
||||
return
|
||||
if self.count() == 1: ## if there is one item, give it to the parent container (unless this is the top)
|
||||
if self is self.area.topContainer:
|
||||
if c == 1: ## if there is one item, give it to the parent container (unless this is the top)
|
||||
ch = self.widget(0)
|
||||
if (self.area is not None and self is self.area.topContainer and not isinstance(ch, Container)) or self.container() is None:
|
||||
return
|
||||
self.container().insert(self.widget(0), 'before', self)
|
||||
self.container().insert(ch, 'before', self)
|
||||
#print "apoptose:", self
|
||||
self.close()
|
||||
if propagate and cont is not None:
|
||||
cont.apoptose()
|
||||
|
||||
|
||||
def close(self):
|
||||
self.area = None
|
||||
self._container = None
|
||||
self.setParent(None)
|
||||
if self.area is not None and self.area.topContainer is self:
|
||||
self.area.topContainer = None
|
||||
self.containerChanged(None)
|
||||
|
||||
def childEvent(self, ev):
|
||||
ch = ev.child()
|
||||
@ -92,7 +99,6 @@ class Container(object):
|
||||
###Set the stretch values for this container to reflect its contents
|
||||
pass
|
||||
|
||||
|
||||
def stretch(self):
|
||||
"""Return the stretch factors for this container"""
|
||||
return self._stretch
|
||||
|
@ -36,6 +36,7 @@ class Dock(QtGui.QWidget, DockDrop):
|
||||
self.widgetArea.setLayout(self.layout)
|
||||
self.widgetArea.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)
|
||||
self.widgets = []
|
||||
self._container = None
|
||||
self.currentRow = 0
|
||||
#self.titlePos = 'top'
|
||||
self.raiseOverlay()
|
||||
@ -187,9 +188,6 @@ class Dock(QtGui.QWidget, DockDrop):
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
def container(self):
|
||||
return self._container
|
||||
|
||||
def addWidget(self, widget, row=None, col=0, rowspan=1, colspan=1):
|
||||
"""
|
||||
Add a new widget to the interior of this Dock.
|
||||
@ -202,7 +200,6 @@ class Dock(QtGui.QWidget, DockDrop):
|
||||
self.layout.addWidget(widget, row, col, rowspan, colspan)
|
||||
self.raiseOverlay()
|
||||
|
||||
|
||||
def startDrag(self):
|
||||
self.drag = QtGui.QDrag(self)
|
||||
mime = QtCore.QMimeData()
|
||||
@ -216,21 +213,30 @@ class Dock(QtGui.QWidget, DockDrop):
|
||||
def float(self):
|
||||
self.area.floatDock(self)
|
||||
|
||||
def container(self):
|
||||
return self._container
|
||||
|
||||
def containerChanged(self, c):
|
||||
if self._container is not None:
|
||||
# ask old container to close itself if it is no longer needed
|
||||
self._container.apoptose()
|
||||
#print self.name(), "container changed"
|
||||
self._container = c
|
||||
if c.type() != 'tab':
|
||||
self.moveLabel = True
|
||||
self.label.setDim(False)
|
||||
if c is None:
|
||||
self.area = None
|
||||
else:
|
||||
self.moveLabel = False
|
||||
|
||||
self.setOrientation(force=True)
|
||||
|
||||
self.area = c.area
|
||||
if c.type() != 'tab':
|
||||
self.moveLabel = True
|
||||
self.label.setDim(False)
|
||||
else:
|
||||
self.moveLabel = False
|
||||
|
||||
self.setOrientation(force=True)
|
||||
|
||||
def raiseDock(self):
|
||||
"""If this Dock is stacked underneath others, raise it to the top."""
|
||||
self.container().raiseDock(self)
|
||||
|
||||
|
||||
def close(self):
|
||||
"""Remove this dock from the DockArea it lives inside."""
|
||||
|
@ -61,6 +61,8 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
|
||||
if isinstance(relativeTo, basestring):
|
||||
relativeTo = self.docks[relativeTo]
|
||||
container = self.getContainer(relativeTo)
|
||||
if container is None:
|
||||
raise TypeError("Dock %s is not contained in a DockArea; cannot add another dock relative to it." % relativeTo)
|
||||
neighbor = relativeTo
|
||||
|
||||
## what container type do we need?
|
||||
@ -98,7 +100,6 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
|
||||
#print "request insert", dock, insertPos, neighbor
|
||||
old = dock.container()
|
||||
container.insert(dock, insertPos, neighbor)
|
||||
dock.area = self
|
||||
self.docks[dock.name()] = dock
|
||||
if old is not None:
|
||||
old.apoptose()
|
||||
@ -142,23 +143,19 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
|
||||
|
||||
def insert(self, new, pos=None, neighbor=None):
|
||||
if self.topContainer is not None:
|
||||
# Adding new top-level container; addContainer() should
|
||||
# take care of giving the old top container a new home.
|
||||
self.topContainer.containerChanged(None)
|
||||
self.layout.addWidget(new)
|
||||
new.containerChanged(self)
|
||||
self.topContainer = new
|
||||
#print self, "set top:", new
|
||||
new._container = self
|
||||
self.raiseOverlay()
|
||||
#print "Insert top:", new
|
||||
|
||||
def count(self):
|
||||
if self.topContainer is None:
|
||||
return 0
|
||||
return 1
|
||||
|
||||
|
||||
#def paintEvent(self, ev):
|
||||
#self.drawDockOverlay()
|
||||
|
||||
def resizeEvent(self, ev):
|
||||
self.resizeOverlay(self.size())
|
||||
|
||||
@ -180,7 +177,6 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
|
||||
area.win.resize(dock.size())
|
||||
area.moveDock(dock, 'top', None)
|
||||
|
||||
|
||||
def removeTempArea(self, area):
|
||||
self.tempAreas.remove(area)
|
||||
#print "close window", area.window()
|
||||
@ -212,14 +208,16 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
|
||||
childs.append(self.childState(obj.widget(i)))
|
||||
return (obj.type(), childs, obj.saveState())
|
||||
|
||||
|
||||
def restoreState(self, state):
|
||||
def restoreState(self, state, missing='error'):
|
||||
"""
|
||||
Restore Dock configuration as generated by saveState.
|
||||
|
||||
Note that this function does not create any Docks--it will only
|
||||
This function does not create any Docks--it will only
|
||||
restore the arrangement of an existing set of Docks.
|
||||
|
||||
By default, docks that are described in *state* but do not exist
|
||||
in the dock area will cause an exception to be raised. This behavior
|
||||
can be changed by setting *missing* to 'ignore' or 'create'.
|
||||
"""
|
||||
|
||||
## 1) make dict of all docks and list of existing containers
|
||||
@ -229,17 +227,20 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
|
||||
|
||||
## 2) create container structure, move docks into new containers
|
||||
if state['main'] is not None:
|
||||
self.buildFromState(state['main'], docks, self)
|
||||
self.buildFromState(state['main'], docks, self, missing=missing)
|
||||
|
||||
## 3) create floating areas, populate
|
||||
for s in state['float']:
|
||||
a = self.addTempArea()
|
||||
a.buildFromState(s[0]['main'], docks, a)
|
||||
a.buildFromState(s[0]['main'], docks, a, missing=missing)
|
||||
a.win.setGeometry(*s[1])
|
||||
a.apoptose() # ask temp area to close itself if it is empty
|
||||
|
||||
## 4) Add any remaining docks to the bottom
|
||||
## 4) Add any remaining docks to a float
|
||||
for d in docks.values():
|
||||
self.moveDock(d, 'below', None)
|
||||
a = self.addTempArea()
|
||||
a.addDock(d, 'below')
|
||||
# self.moveDock(d, 'below', None)
|
||||
|
||||
#print "\nKill old containers:"
|
||||
## 5) kill old containers
|
||||
@ -248,8 +249,7 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
|
||||
for a in oldTemps:
|
||||
a.apoptose()
|
||||
|
||||
|
||||
def buildFromState(self, state, docks, root, depth=0):
|
||||
def buildFromState(self, state, docks, root, depth=0, missing='error'):
|
||||
typ, contents, state = state
|
||||
pfx = " " * depth
|
||||
if typ == 'dock':
|
||||
@ -257,7 +257,15 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
|
||||
obj = docks[contents]
|
||||
del docks[contents]
|
||||
except KeyError:
|
||||
raise Exception('Cannot restore dock state; no dock with name "%s"' % contents)
|
||||
if missing == 'error':
|
||||
raise Exception('Cannot restore dock state; no dock with name "%s"' % contents)
|
||||
elif missing == 'create':
|
||||
obj = Dock(name=contents)
|
||||
elif missing == 'ignore':
|
||||
return
|
||||
else:
|
||||
raise ValueError('"missing" argument must be one of "error", "create", or "ignore".')
|
||||
|
||||
else:
|
||||
obj = self.makeContainer(typ)
|
||||
|
||||
@ -266,10 +274,11 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
|
||||
|
||||
if typ != 'dock':
|
||||
for o in contents:
|
||||
self.buildFromState(o, docks, obj, depth+1)
|
||||
self.buildFromState(o, docks, obj, depth+1, missing=missing)
|
||||
# remove this container if possible. (there are valid situations when a restore will
|
||||
# generate empty containers, such as when using missing='ignore')
|
||||
obj.apoptose(propagate=False)
|
||||
obj.restoreState(state) ## this has to be done later?
|
||||
|
||||
obj.restoreState(state) ## this has to be done later?
|
||||
|
||||
def findAll(self, obj=None, c=None, d=None):
|
||||
if obj is None:
|
||||
@ -295,14 +304,15 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
|
||||
d.update(d2)
|
||||
return (c, d)
|
||||
|
||||
def apoptose(self):
|
||||
def apoptose(self, propagate=True):
|
||||
# remove top container if possible, close this area if it is temporary.
|
||||
#print "apoptose area:", self.temporary, self.topContainer, self.topContainer.count()
|
||||
if self.topContainer.count() == 0:
|
||||
if self.topContainer is None or self.topContainer.count() == 0:
|
||||
self.topContainer = None
|
||||
if self.temporary:
|
||||
self.home.removeTempArea(self)
|
||||
#self.close()
|
||||
|
||||
|
||||
def clear(self):
|
||||
docks = self.findAll()[1]
|
||||
for dock in docks.values():
|
||||
@ -322,12 +332,38 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
|
||||
def dropEvent(self, *args):
|
||||
DockDrop.dropEvent(self, *args)
|
||||
|
||||
def printState(self, state=None, name='Main'):
|
||||
# for debugging
|
||||
if state is None:
|
||||
state = self.saveState()
|
||||
print("=== %s dock area ===" % name)
|
||||
if state['main'] is None:
|
||||
print(" (empty)")
|
||||
else:
|
||||
self._printAreaState(state['main'])
|
||||
for i, float in enumerate(state['float']):
|
||||
self.printState(float[0], name='float %d' % i)
|
||||
|
||||
class TempAreaWindow(QtGui.QMainWindow):
|
||||
def _printAreaState(self, area, indent=0):
|
||||
if area[0] == 'dock':
|
||||
print(" " * indent + area[0] + " " + str(area[1:]))
|
||||
return
|
||||
else:
|
||||
print(" " * indent + area[0])
|
||||
for ch in area[1]:
|
||||
self._printAreaState(ch, indent+1)
|
||||
|
||||
|
||||
|
||||
class TempAreaWindow(QtGui.QWidget):
|
||||
def __init__(self, area, **kwargs):
|
||||
QtGui.QMainWindow.__init__(self, **kwargs)
|
||||
self.setCentralWidget(area)
|
||||
QtGui.QWidget.__init__(self, **kwargs)
|
||||
self.layout = QtGui.QGridLayout()
|
||||
self.setLayout(self.layout)
|
||||
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.dockarea = area
|
||||
self.layout.addWidget(area)
|
||||
|
||||
def closeEvent(self, *args, **kwargs):
|
||||
self.centralWidget().clear()
|
||||
QtGui.QMainWindow.closeEvent(self, *args, **kwargs)
|
||||
def closeEvent(self, *args):
|
||||
self.dockarea.clear()
|
||||
QtGui.QWidget.closeEvent(self, *args)
|
||||
|
184
pyqtgraph/dockarea/tests/test_dockarea.py
Normal file
184
pyqtgraph/dockarea/tests/test_dockarea.py
Normal file
@ -0,0 +1,184 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import pytest
|
||||
import pyqtgraph as pg
|
||||
from collections import OrderedDict
|
||||
pg.mkQApp()
|
||||
|
||||
import pyqtgraph.dockarea as da
|
||||
|
||||
def test_dockarea():
|
||||
a = da.DockArea()
|
||||
d1 = da.Dock("dock 1")
|
||||
a.addDock(d1, 'left')
|
||||
|
||||
assert a.topContainer is d1.container()
|
||||
assert d1.container().container() is a
|
||||
assert d1.area is a
|
||||
assert a.topContainer.widget(0) is d1
|
||||
|
||||
d2 = da.Dock("dock 2")
|
||||
a.addDock(d2, 'right')
|
||||
|
||||
assert a.topContainer is d1.container()
|
||||
assert a.topContainer is d2.container()
|
||||
assert d1.container().container() is a
|
||||
assert d2.container().container() is a
|
||||
assert d2.area is a
|
||||
assert a.topContainer.widget(0) is d1
|
||||
assert a.topContainer.widget(1) is d2
|
||||
|
||||
d3 = da.Dock("dock 3")
|
||||
a.addDock(d3, 'bottom')
|
||||
|
||||
assert a.topContainer is d3.container()
|
||||
assert d2.container().container() is d3.container()
|
||||
assert d1.container().container() is d3.container()
|
||||
assert d1.container().container().container() is a
|
||||
assert d2.container().container().container() is a
|
||||
assert d3.container().container() is a
|
||||
assert d3.area is a
|
||||
assert d2.area is a
|
||||
assert a.topContainer.widget(0) is d1.container()
|
||||
assert a.topContainer.widget(1) is d3
|
||||
|
||||
d4 = da.Dock("dock 4")
|
||||
a.addDock(d4, 'below', d3)
|
||||
|
||||
assert d4.container().type() == 'tab'
|
||||
assert d4.container() is d3.container()
|
||||
assert d3.container().container() is d2.container().container()
|
||||
assert d4.area is a
|
||||
a.printState()
|
||||
|
||||
# layout now looks like:
|
||||
# vcontainer
|
||||
# hcontainer
|
||||
# dock 1
|
||||
# dock 2
|
||||
# tcontainer
|
||||
# dock 3
|
||||
# dock 4
|
||||
|
||||
# test save/restore state
|
||||
state = a.saveState()
|
||||
a2 = da.DockArea()
|
||||
# default behavior is to raise exception if docks are missing
|
||||
with pytest.raises(Exception):
|
||||
a2.restoreState(state)
|
||||
|
||||
# test restore with ignore missing
|
||||
a2.restoreState(state, missing='ignore')
|
||||
assert a2.topContainer is None
|
||||
|
||||
# test restore with auto-create
|
||||
a2.restoreState(state, missing='create')
|
||||
assert a2.saveState() == state
|
||||
a2.printState()
|
||||
|
||||
# double-check that state actually matches the output of saveState()
|
||||
c1 = a2.topContainer
|
||||
assert c1.type() == 'vertical'
|
||||
c2 = c1.widget(0)
|
||||
c3 = c1.widget(1)
|
||||
assert c2.type() == 'horizontal'
|
||||
assert c2.widget(0).name() == 'dock 1'
|
||||
assert c2.widget(1).name() == 'dock 2'
|
||||
assert c3.type() == 'tab'
|
||||
assert c3.widget(0).name() == 'dock 3'
|
||||
assert c3.widget(1).name() == 'dock 4'
|
||||
|
||||
# test restore with docks already present
|
||||
a3 = da.DockArea()
|
||||
a3docks = []
|
||||
for i in range(1, 5):
|
||||
dock = da.Dock('dock %d' % i)
|
||||
a3docks.append(dock)
|
||||
a3.addDock(dock, 'right')
|
||||
a3.restoreState(state)
|
||||
assert a3.saveState() == state
|
||||
|
||||
# test restore with extra docks present
|
||||
a3 = da.DockArea()
|
||||
a3docks = []
|
||||
for i in [1, 2, 5, 4, 3]:
|
||||
dock = da.Dock('dock %d' % i)
|
||||
a3docks.append(dock)
|
||||
a3.addDock(dock, 'left')
|
||||
a3.restoreState(state)
|
||||
a3.printState()
|
||||
|
||||
|
||||
# test a more complex restore
|
||||
a4 = da.DockArea()
|
||||
state1 = {'float': [], 'main':
|
||||
('horizontal', [
|
||||
('vertical', [
|
||||
('horizontal', [
|
||||
('tab', [
|
||||
('dock', 'dock1', {}),
|
||||
('dock', 'dock2', {}),
|
||||
('dock', 'dock3', {}),
|
||||
('dock', 'dock4', {})
|
||||
], {'index': 1}),
|
||||
('vertical', [
|
||||
('dock', 'dock5', {}),
|
||||
('horizontal', [
|
||||
('dock', 'dock6', {}),
|
||||
('dock', 'dock7', {})
|
||||
], {'sizes': [184, 363]})
|
||||
], {'sizes': [355, 120]})
|
||||
], {'sizes': [9, 552]})
|
||||
], {'sizes': [480]}),
|
||||
('dock', 'dock8', {})
|
||||
], {'sizes': [566, 69]})
|
||||
}
|
||||
|
||||
state2 = {'float': [], 'main':
|
||||
('horizontal', [
|
||||
('vertical', [
|
||||
('horizontal', [
|
||||
('dock', 'dock2', {}),
|
||||
('vertical', [
|
||||
('dock', 'dock5', {}),
|
||||
('horizontal', [
|
||||
('dock', 'dock6', {}),
|
||||
('dock', 'dock7', {})
|
||||
], {'sizes': [492, 485]})
|
||||
], {'sizes': [936, 0]})
|
||||
], {'sizes': [172, 982]})
|
||||
], {'sizes': [941]}),
|
||||
('vertical', [
|
||||
('dock', 'dock8', {}),
|
||||
('dock', 'dock4', {}),
|
||||
('dock', 'dock1', {})
|
||||
], {'sizes': [681, 225, 25]})
|
||||
], {'sizes': [1159, 116]})}
|
||||
|
||||
a4.restoreState(state1, missing='create')
|
||||
a4.restoreState(state2, missing='ignore')
|
||||
a4.printState()
|
||||
|
||||
c, d = a4.findAll()
|
||||
assert d['dock3'].area is not a4
|
||||
assert d['dock1'].container() is d['dock4'].container() is d['dock8'].container()
|
||||
assert d['dock6'].container() is d['dock7'].container()
|
||||
assert a4 is d['dock2'].area is d['dock2'].container().container().container()
|
||||
assert a4 is d['dock5'].area is d['dock5'].container().container().container().container()
|
||||
|
||||
# States should be the same with two exceptions:
|
||||
# dock3 is in a float because it does not appear in state2
|
||||
# a superfluous vertical splitter in state2 has been removed
|
||||
state4 = a4.saveState()
|
||||
state4['main'][1][0] = state4['main'][1][0][1][0]
|
||||
assert clean_state(state4['main']) == clean_state(state2['main'])
|
||||
|
||||
|
||||
def clean_state(state):
|
||||
# return state dict with sizes removed
|
||||
ch = [clean_state(x) for x in state[1]] if isinstance(state[1], list) else state[1]
|
||||
state = (state[0], ch, {})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_dockarea()
|
Loading…
x
Reference in New Issue
Block a user