From 5ce8d09aa08db16ab18f8e063d66aa55a1926f45 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Tue, 16 Oct 2012 17:07:23 -0400 Subject: [PATCH 1/4] 10-100x speedup for ScatterPlotItem --- examples/ScatterPlotSpeedTest.py | 2 +- functions.py | 112 ++++++- graphicsItems/PlotDataItem.py | 33 +- graphicsItems/ScatterPlotItem.py | 544 +++++++++++++++++++++---------- 4 files changed, 496 insertions(+), 195 deletions(-) diff --git a/examples/ScatterPlotSpeedTest.py b/examples/ScatterPlotSpeedTest.py index 386522d1..17711d23 100644 --- a/examples/ScatterPlotSpeedTest.py +++ b/examples/ScatterPlotSpeedTest.py @@ -26,7 +26,7 @@ win.show() p = ui.plot data = np.random.normal(size=(50,500), scale=100) -sizeArray = np.random.random(500) * 20. +sizeArray = (np.random.random(500) * 20.).astype(int) ptr = 0 lastTime = time() fps = None diff --git a/functions.py b/functions.py index e70e72fc..5e2e4865 100644 --- a/functions.py +++ b/functions.py @@ -25,6 +25,7 @@ SI_PREFIXES_ASCII = 'yzafpnum kMGTPEZY' from .Qt import QtGui, QtCore import numpy as np import decimal, re +import ctypes try: import scipy.ndimage @@ -223,13 +224,15 @@ def mkColor(*args): return QtGui.QColor(*args) -def mkBrush(*args): +def mkBrush(*args, **kwds): """ | Convenience function for constructing Brush. | This function always constructs a solid brush and accepts the same arguments as :func:`mkColor() ` | Calling mkBrush(None) returns an invisible brush. """ - if len(args) == 1: + if 'color' in kwds: + color = kwds['color'] + elif len(args) == 1: arg = args[0] if arg is None: return QtGui.QBrush(QtCore.Qt.NoBrush) @@ -237,7 +240,7 @@ def mkBrush(*args): return QtGui.QBrush(arg) else: color = arg - if len(args) > 1: + elif len(args) > 1: color = args return QtGui.QBrush(mkColor(color)) @@ -779,30 +782,107 @@ def makeARGB(data, lut=None, levels=None, useRGBA=False): return imgData, alpha -def makeQImage(imgData, alpha): - """Turn an ARGB array into QImage""" +def makeQImage(imgData, alpha=None, copy=True, transpose=True): + """ + Turn an ARGB array into QImage. + By default, the data is copied; changes to the array will not + be reflected in the image. The image will be given a 'data' attribute + pointing to the array which shares its data to prevent python + freeing that memory while the image is in use. + + =========== =================================================================== + Arguments: + imgData Array of data to convert. Must have shape (width, height, 3 or 4) + and dtype=ubyte. The order of values in the 3rd axis must be + (b, g, r, a). + alpha If True, the QImage returned will have format ARGB32. If False, + the format will be RGB32. By default, _alpha_ is True if + array.shape[2] == 4. + copy If True, the data is copied before converting to QImage. + If False, the new QImage points directly to the data in the array. + Note that the array must be contiguous for this to work. + transpose If True (the default), the array x/y axes are transposed before + creating the image. Note that Qt expects the axes to be in + (height, width) order whereas pyqtgraph usually prefers the + opposite. + =========== =================================================================== + """ ## create QImage from buffer prof = debug.Profiler('functions.makeQImage', disabled=True) + ## If we didn't explicitly specify alpha, check the array shape. + if alpha is None: + alpha = (imgData.shape[2] == 4) + + copied = False + if imgData.shape[2] == 3: ## need to make alpha channel (even if alpha==False; QImage requires 32 bpp) + if copy is True: + d2 = np.empty(imgData.shape[:2] + (4,), dtype=imgData.dtype) + d2[:,:,:3] = imgData + d2[:,:,3] = 255 + imgData = d2 + copied = True + else: + raise Exception('Array has only 3 channels; cannot make QImage without copying.') + if alpha: imgFormat = QtGui.QImage.Format_ARGB32 else: imgFormat = QtGui.QImage.Format_RGB32 - imgData = imgData.transpose((1, 0, 2)) ## QImage expects the row/column order to be opposite - try: - buf = imgData.data - except AttributeError: ## happens when image data is non-contiguous + if transpose: + imgData = imgData.transpose((1, 0, 2)) ## QImage expects the row/column order to be opposite + + if not imgData.flags['C_CONTIGUOUS']: + if copy is False: + extra = ' (try setting transpose=False)' if transpose else '' + raise Exception('Array is not contiguous; cannot make QImage without copying.'+extra) imgData = np.ascontiguousarray(imgData) - buf = imgData.data + copied = True - prof.mark('1') - qimage = QtGui.QImage(buf, imgData.shape[1], imgData.shape[0], imgFormat) - prof.mark('2') - qimage.data = imgData - prof.finish() - return qimage + if copy is True and copied is False: + imgData = imgData.copy() + + addr = ctypes.addressof(ctypes.c_char.from_buffer(imgData, 0)) + img = QtGui.QImage(addr, imgData.shape[1], imgData.shape[0], imgFormat) + img.data = imgData + return img + #try: + #buf = imgData.data + #except AttributeError: ## happens when image data is non-contiguous + #buf = imgData.data + + #prof.mark('1') + #qimage = QtGui.QImage(buf, imgData.shape[1], imgData.shape[0], imgFormat) + #prof.mark('2') + #qimage.data = imgData + #prof.finish() + #return qimage +def imageToArray(img, copy=False, transpose=True): + """ + Convert a QImage into numpy array. The image must have format RGB32, ARGB32, or ARGB32_Premultiplied. + By default, the image is not copied; changes made to the array will appear in the QImage as well (beware: if + the QImage is collected before the array, there may be trouble). + The array will have shape (width, height, (b,g,r,a)). + """ + ptr = img.bits() + ptr.setsize(img.byteCount()) + fmt = img.format() + if fmt == img.Format_RGB32: + arr = np.asarray(ptr).reshape(img.height(), img.width(), 3) + elif fmt == img.Format_ARGB32 or fmt == img.Format_ARGB32_Premultiplied: + arr = np.asarray(ptr).reshape(img.height(), img.width(), 4) + if copy: + arr = arr.copy() + + if transpose: + return arr.transpose((1,0,2)) + else: + return arr + + + def rescaleData(data, scale, offset): newData = np.empty((data.size,), dtype=np.int) diff --git a/graphicsItems/PlotDataItem.py b/graphicsItems/PlotDataItem.py index d30d737b..f1a01044 100644 --- a/graphicsItems/PlotDataItem.py +++ b/graphicsItems/PlotDataItem.py @@ -130,6 +130,8 @@ class PlotDataItem(GraphicsObject): 'symbolBrush': (50, 50, 150), 'pxMode': True, + 'pointMode': None, + 'data': None, } self.setData(*args, **kargs) @@ -144,22 +146,30 @@ class PlotDataItem(GraphicsObject): return QtCore.QRectF() ## let child items handle this def setAlpha(self, alpha, auto): + if self.opts['alphaHint'] == alpha and self.opts['alphaMode'] == auto: + return self.opts['alphaHint'] = alpha self.opts['alphaMode'] = auto self.setOpacity(alpha) #self.update() def setFftMode(self, mode): + if self.opts['fftMode'] == mode: + return self.opts['fftMode'] = mode self.xDisp = self.yDisp = None self.updateItems() def setLogMode(self, xMode, yMode): - self.opts['logMode'] = (xMode, yMode) + if self.opts['logMode'] == [xMode, yMode]: + return + self.opts['logMode'] = [xMode, yMode] self.xDisp = self.yDisp = None self.updateItems() def setPointMode(self, mode): + if self.opts['pointMode'] == mode: + return self.opts['pointMode'] = mode self.update() @@ -193,6 +203,8 @@ class PlotDataItem(GraphicsObject): def setFillBrush(self, *args, **kargs): brush = fn.mkBrush(*args, **kargs) + if self.opts['fillBrush'] == brush: + return self.opts['fillBrush'] = brush self.updateItems() @@ -200,16 +212,22 @@ class PlotDataItem(GraphicsObject): return self.setFillBrush(*args, **kargs) def setFillLevel(self, level): + if self.opts['fillLevel'] == level: + return self.opts['fillLevel'] = level self.updateItems() def setSymbol(self, symbol): + if self.opts['symbol'] == symbol: + return self.opts['symbol'] = symbol #self.scatter.setSymbol(symbol) self.updateItems() def setSymbolPen(self, *args, **kargs): pen = fn.mkPen(*args, **kargs) + if self.opts['symbolPen'] == pen: + return self.opts['symbolPen'] = pen #self.scatter.setSymbolPen(pen) self.updateItems() @@ -218,21 +236,26 @@ class PlotDataItem(GraphicsObject): def setSymbolBrush(self, *args, **kargs): brush = fn.mkBrush(*args, **kargs) + if self.opts['symbolBrush'] == brush: + return self.opts['symbolBrush'] = brush #self.scatter.setSymbolBrush(brush) self.updateItems() def setSymbolSize(self, size): + if self.opts['symbolSize'] == size: + return self.opts['symbolSize'] = size #self.scatter.setSymbolSize(symbolSize) self.updateItems() def setDownsampling(self, ds): - if self.opts['downsample'] != ds: - self.opts['downsample'] = ds - self.xDisp = self.yDisp = None - self.updateItems() + if self.opts['downsample'] == ds: + return + self.opts['downsample'] = ds + self.xDisp = self.yDisp = None + self.updateItems() def setData(self, *args, **kargs): """ diff --git a/graphicsItems/ScatterPlotItem.py b/graphicsItems/ScatterPlotItem.py index 625eb0b6..dfbfa155 100644 --- a/graphicsItems/ScatterPlotItem.py +++ b/graphicsItems/ScatterPlotItem.py @@ -32,26 +32,165 @@ for k, c in coords.items(): Symbols[k].lineTo(x, y) Symbols[k].closeSubpath() + +def drawSymbol(painter, symbol, size, pen, brush): + painter.scale(size, size) + painter.setPen(pen) + painter.setBrush(brush) + if isinstance(symbol, basestring): + symbol = Symbols[symbol] + if np.isscalar(symbol): + symbol = Symbols.values()[symbol % len(Symbols)] + painter.drawPath(symbol) -def makeSymbolPixmap(size, pen, brush, symbol): + +def renderSymbol(symbol, size, pen, brush, device=None): + """ + Render a symbol specification to QImage. + Symbol may be either a QPainterPath or one of the keys in the Symbols dict. + If *device* is None, a new QPixmap will be returned. Otherwise, + the symbol will be rendered into the device specified (See QPainter documentation + for more information). + """ + ## see if this pixmap is already cached + #global SymbolPixmapCache + #key = (symbol, size, fn.colorTuple(pen.color()), pen.width(), pen.style(), fn.colorTuple(brush.color())) + #if key in SymbolPixmapCache: + #return SymbolPixmapCache[key] + ## Render a spot with the given parameters to a pixmap penPxWidth = max(np.ceil(pen.width()), 1) - image = QtGui.QImage(int(size+penPxWidth), int(size+penPxWidth), QtGui.QImage.Format_ARGB32_Premultiplied) + image = QtGui.QImage(int(size+penPxWidth), int(size+penPxWidth), QtGui.QImage.Format_ARGB32) image.fill(0) p = QtGui.QPainter(image) p.setRenderHint(p.Antialiasing) p.translate(image.width()*0.5, image.height()*0.5) - p.scale(size, size) - p.setPen(pen) - p.setBrush(brush) - if isinstance(symbol, basestring): - symbol = Symbols[symbol] - p.drawPath(symbol) + drawSymbol(p, symbol, size, pen, brush) p.end() - return QtGui.QPixmap(image) + return image + #pixmap = QtGui.QPixmap(image) + #SymbolPixmapCache[key] = pixmap + #return pixmap +def makeSymbolPixmap(size, pen, brush, symbol): + ## deprecated + img = renderSymbol(symbol, size, pen, brush) + return QtGui.QPixmap(img) + +class SymbolAtlas: + """ + Used to efficiently construct a single QPixmap containing all rendered symbols + for a ScatterPlotItem. This is required for fragment rendering. + + Use example: + atlas = SymbolAtlas() + sc1 = atlas.getSymbolCoords('o', 5, QPen(..), QBrush(..)) + sc2 = atlas.getSymbolCoords('t', 10, QPen(..), QBrush(..)) + pm = atlas.getAtlas() + + """ + class SymbolCoords(list): ## needed because lists are not allowed in weak references. + pass + + def __init__(self): + # symbol key : [x, y, w, h] atlas coordinates + # note that the coordinate list will always be the same list object as + # long as the symbol is in the atlas, but the coordinates may + # change if the atlas is rebuilt. + # weak value; if all external refs to this list disappear, + # the symbol will be forgotten. + self.symbolMap = weakref.WeakValueDictionary() + + self.atlasData = None # numpy array of atlas image + self.atlas = None # atlas as QPixmap + self.atlasValid = False + + def getSymbolCoords(self, symbol, size, pen, brush): + key = (symbol, size, fn.colorTuple(pen.color()), pen.width(), pen.style(), fn.colorTuple(brush.color())) + if key not in self.symbolMap: + newCoords = SymbolAtlas.SymbolCoords() + self.symbolMap[key] = newCoords + self.invalidateAtlas() + #try: + #self.addToAtlas(key) ## squeeze this into the atlas if there is room + #except: + #self.buildAtlas() ## otherwise, we need to rebuild + + return self.symbolMap[key] + + def invalidateAtlas(self): + self.atlasValid = False + + def buildAtlas(self): + # get rendered array for all symbols, keep track of avg/max width + rendered = {} + avgWidth = 0.0 + maxWidth = 0 + images = [] + for key, coords in self.symbolMap.items(): + if len(coords) == 0: + pen = fn.mkPen(color=key[2], width=key[3], style=key[4]) + brush = fn.mkBrush(color=key[5]) + img = renderSymbol(key[0], key[1], pen, brush) + images.append(img) ## we only need this to prevent the images being garbage collected immediately + arr = fn.imageToArray(img, copy=False, transpose=False) + else: + (x,y,w,h) = self.symbolMap[key] + arr = self.atlasData[x:x+w, y:y+w] + rendered[key] = arr + w = arr.shape[0] + avgWidth += w + maxWidth = max(maxWidth, w) + + nSymbols = len(rendered) + if nSymbols > 0: + avgWidth /= nSymbols + width = max(maxWidth, avgWidth * (nSymbols**0.5)) + else: + avgWidth = 0 + width = 0 + + # sort symbols by height + symbols = sorted(rendered.keys(), key=lambda x: rendered[x].shape[1], reverse=True) + + self.atlasRows = [] + x = width + y = 0 + rowheight = 0 + for key in symbols: + arr = rendered[key] + w,h = arr.shape[:2] + if x+w > width: + y += rowheight + x = 0 + rowheight = h + self.atlasRows.append([y, rowheight, 0]) + self.symbolMap[key][:] = x, y, w, h + x += w + self.atlasRows[-1][2] = x + height = y + rowheight + self.atlasData = np.zeros((width, height, 4), dtype=np.ubyte) + for key in symbols: + x, y, w, h = self.symbolMap[key] + self.atlasData[x:x+w, y:y+h] = rendered[key] + self.atlas = None + self.atlasValid = True + + def getAtlas(self): + if not self.atlasValid: + self.buildAtlas() + if self.atlas is None: + if len(self.atlasData) == 0: + return QtGui.QPixmap(0,0) + img = fn.makeQImage(self.atlasData, copy=False, transpose=False) + self.atlas = QtGui.QPixmap(img) + return self.atlas + + + + class ScatterPlotItem(GraphicsObject): """ Displays a set of x/y points. Instances of this class are created @@ -79,13 +218,16 @@ class ScatterPlotItem(GraphicsObject): """ prof = debug.Profiler('ScatterPlotItem.__init__', disabled=True) GraphicsObject.__init__(self) - self.setFlag(self.ItemHasNoContents, True) - self.data = np.empty(0, dtype=[('x', float), ('y', float), ('size', float), ('symbol', object), ('pen', object), ('brush', object), ('item', object), ('data', object)]) + + self.picture = None # QPicture used for rendering when pxmode==False + self.fragments = None # fragment specification for pxmode; updated every time the view changes. + self.fragmentAtlas = SymbolAtlas() + + self.data = np.empty(0, dtype=[('x', float), ('y', float), ('size', float), ('symbol', object), ('pen', object), ('brush', object), ('data', object), ('fragCoords', object)]) self.bounds = [None, None] ## caches data bounds self._maxSpotWidth = 0 ## maximum size of the scale-variant portion of all spots self._maxSpotPxWidth = 0 ## maximum size of the scale-invariant portion of all spots - self._spotPixmap = None - self.opts = {'pxMode': True} + self.opts = {'pxMode': True, 'useCache': True} ## If useCache is False, symbols are re-drawn on every paint. self.setPen(200,200,200, update=False) self.setBrush(100,100,150, update=False) @@ -96,6 +238,8 @@ class ScatterPlotItem(GraphicsObject): prof.mark('setData') prof.finish() + #self.setCacheMode(self.DeviceCoordinateCache) + def setData(self, *args, **kargs): """ **Ordered Arguments:** @@ -130,6 +274,7 @@ class ScatterPlotItem(GraphicsObject): *identical* *Deprecated*. This functionality is handled automatically now. ====================== =============================================================================================== """ + oldData = self.data ## this causes cached pixmaps to be preserved while new data is registered. self.clear() ## clear out all old data self.addPoints(*args, **kargs) @@ -183,8 +328,8 @@ class ScatterPlotItem(GraphicsObject): ## note that np.empty initializes object fields to None and string fields to '' self.data[:len(oldData)] = oldData - for i in range(len(oldData)): - oldData[i]['item']._data = self.data[i] ## Make sure items have proper reference to new array + #for i in range(len(oldData)): + #oldData[i]['item']._data = self.data[i] ## Make sure items have proper reference to new array newData = self.data[len(oldData):] newData['size'] = -1 ## indicates to use default size @@ -217,7 +362,7 @@ class ScatterPlotItem(GraphicsObject): newData['y'] = kargs['y'] if 'pxMode' in kargs: - self.setPxMode(kargs['pxMode'], update=False) + self.setPxMode(kargs['pxMode']) ## Set any extra parameters provided in keyword arguments for k in ['pen', 'brush', 'symbol', 'size']: @@ -228,12 +373,18 @@ class ScatterPlotItem(GraphicsObject): if 'data' in kargs: self.setPointData(kargs['data'], dataSet=newData) - #self.updateSpots() self.prepareGeometryChange() self.bounds = [None, None] - self.generateSpotItems() + self.invalidate() + self.updateSpots(newData) self.sigPlotChanged.emit(self) + def invalidate(self): + ## clear any cached drawing state + self.picture = None + self.fragments = None + self.update() + def getData(self): return self.data['x'], self.data['y'] @@ -263,8 +414,8 @@ class ScatterPlotItem(GraphicsObject): dataSet['pen'] = pens else: self.opts['pen'] = fn.mkPen(*args, **kargs) - self._spotPixmap = None + dataSet['fragCoords'] = None if update: self.updateSpots(dataSet) @@ -285,8 +436,9 @@ class ScatterPlotItem(GraphicsObject): dataSet['brush'] = brushes else: self.opts['brush'] = fn.mkBrush(*args, **kargs) - self._spotPixmap = None + #self._spotPixmap = None + dataSet['fragCoords'] = None if update: self.updateSpots(dataSet) @@ -307,6 +459,7 @@ class ScatterPlotItem(GraphicsObject): self.opts['symbol'] = symbol self._spotPixmap = None + dataSet['fragCoords'] = None if update: self.updateSpots(dataSet) @@ -327,6 +480,7 @@ class ScatterPlotItem(GraphicsObject): self.opts['size'] = size self._spotPixmap = None + dataSet['fragCoords'] = None if update: self.updateSpots(dataSet) @@ -346,34 +500,52 @@ class ScatterPlotItem(GraphicsObject): else: dataSet['data'] = data - def setPxMode(self, mode, update=True): + def setPxMode(self, mode): if self.opts['pxMode'] == mode: return self.opts['pxMode'] = mode - self.clearItems() - if update: - self.generateSpotItems() + self.invalidate() def updateSpots(self, dataSet=None): if dataSet is None: dataSet = self.data self._maxSpotWidth = 0 self._maxSpotPxWidth = 0 - for spot in dataSet['item']: - spot.updateItem() + if self.opts['pxMode']: + for rec in dataSet: + if rec['fragCoords'] is None: + + + rec['fragCoords'] = self.fragmentAtlas.getSymbolCoords(*self.getSpotOpts(rec)) self.measureSpotSizes(dataSet) + def getSpotOpts(self, rec): + symbol = rec['symbol'] + if symbol is None: + symbol = self.opts['symbol'] + size = rec['size'] + if size < 0: + size = self.opts['size'] + pen = rec['pen'] + if pen is None: + pen = self.opts['pen'] + brush = rec['brush'] + if brush is None: + brush = self.opts['brush'] + return (symbol, size, fn.mkPen(pen), fn.mkBrush(brush)) + + def measureSpotSizes(self, dataSet): - for spot in dataSet['item']: + for rec in dataSet: ## keep track of the maximum spot size and pixel size + symbol, size, pen, brush = self.getSpotOpts(rec) width = 0 pxWidth = 0 - pen = spot.pen() if self.opts['pxMode']: - pxWidth = spot.size() + pen.width() + pxWidth = size + pen.width() else: - width = spot.size() + width = size if pen.isCosmetic(): pxWidth += pen.width() else: @@ -385,20 +557,11 @@ class ScatterPlotItem(GraphicsObject): def clear(self): """Remove all spots from the scatter plot""" - self.clearItems() + #self.clearItems() self.data = np.empty(0, dtype=self.data.dtype) self.bounds = [None, None] + self.invalidate() - def clearItems(self): - for i in self.data['item']: - if i is None: - continue - i.setParentItem(None) - s = i.scene() - if s is not None: - s.removeItem(i) - self.data['item'] = None - def dataBounds(self, ax, frac=1.0, orthoRange=None): if frac >= 1.0 and self.bounds[ax] is not None: return self.bounds[ax] @@ -436,28 +599,12 @@ class ScatterPlotItem(GraphicsObject): else: return (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50))) - - - - - def generateSpotItems(self): - if self.opts['pxMode']: - for rec in self.data: - if rec['item'] is None: - rec['item'] = PixmapSpotItem(rec, self) - else: - for rec in self.data: - if rec['item'] is None: - rec['item'] = PathSpotItem(rec, self) - self.measureSpotSizes(self.data) - self.sigPlotChanged.emit(self) - - def defaultSpotPixmap(self): - ## Return the default spot pixmap - if self._spotPixmap is None: - self._spotPixmap = makeSymbolPixmap(size=self.opts['size'], brush=self.opts['brush'], pen=self.opts['pen'], symbol=self.opts['symbol']) - return self._spotPixmap + #def defaultSpotPixmap(self): + ### Return the default spot pixmap + #if self._spotPixmap is None: + #self._spotPixmap = makeSymbolPixmap(size=self.opts['size'], brush=self.opts['brush'], pen=self.opts['pen'], symbol=self.opts['symbol']) + #return self._spotPixmap def boundingRect(self): (xmn, xmx) = self.dataBounds(ax=0) @@ -474,12 +621,63 @@ class ScatterPlotItem(GraphicsObject): self.prepareGeometryChange() GraphicsObject.viewRangeChanged(self) self.bounds = [None, None] + self.fragments = None + def generateFragments(self): + tr = self.deviceTransform() + if tr is None: + return + pts = np.empty((2,len(self.data['x']))) + pts[0] = self.data['x'] + pts[1] = self.data['y'] + pts = fn.transformCoordinates(tr, pts) + self.fragments = [] + for i in xrange(len(self.data)): + rec = self.data[i] + pos = QtCore.QPointF(pts[0,i], pts[1,i]) + x,y,w,h = rec['fragCoords'] + rect = QtCore.QRectF(y, x, h, w) + self.fragments.append(QtGui.QPainter.PixmapFragment.create(pos, rect)) + def paint(self, p, *args): - ## NOTE: self.paint is disabled by this line in __init__: - ## self.setFlag(self.ItemHasNoContents, True) - p.setPen(fn.mkPen('r')) - p.drawRect(self.boundingRect()) + #p.setPen(fn.mkPen('r')) + #p.drawRect(self.boundingRect()) + + if self.opts['pxMode']: + atlas = self.fragmentAtlas.getAtlas() + #arr = fn.imageToArray(atlas.toImage(), copy=True) + #if hasattr(self, 'lastAtlas'): + #if np.any(self.lastAtlas != arr): + #print "Atlas changed:", arr + #self.lastAtlas = arr + + if self.fragments is None: + self.updateSpots() + self.generateFragments() + + p.resetTransform() + + if self.opts['useCache']: + p.drawPixmapFragments(self.fragments, atlas) + else: + for i in range(len(self.data)): + rec = self.data[i] + frag = self.fragments[i] + p.resetTransform() + p.translate(frag.x, frag.y) + drawSymbol(p, *self.getSpotOpts(rec)) + else: + if self.picture is None: + self.picture = QtGui.QPicture() + p2 = QtGui.QPainter(self.picture) + for rec in self.data: + + p2.resetTransform() + p2.translate(rec['x'], rec['y']) + drawSymbol(p2, *self.getSpotOpts(rec)) + p2.end() + + self.picture.play(p) def points(self): @@ -524,131 +722,131 @@ class ScatterPlotItem(GraphicsObject): ev.ignore() -class SpotItem(GraphicsItem): - """ - Class referring to individual spots in a scatter plot. - These can be retrieved by calling ScatterPlotItem.points() or - by connecting to the ScatterPlotItem's click signals. - """ +#class SpotItem(GraphicsItem): + #""" + #Class referring to individual spots in a scatter plot. + #These can be retrieved by calling ScatterPlotItem.points() or + #by connecting to the ScatterPlotItem's click signals. + #""" - def __init__(self, data, plot): - GraphicsItem.__init__(self, register=False) - self._data = data - self._plot = plot - #self._viewBox = None - #self._viewWidget = None - self.setParentItem(plot) - self.setPos(QtCore.QPointF(data['x'], data['y'])) - self.updateItem() + #def __init__(self, data, plot): + #GraphicsItem.__init__(self, register=False) + #self._data = data + #self._plot = plot + ##self._viewBox = None + ##self._viewWidget = None + #self.setParentItem(plot) + #self.setPos(QtCore.QPointF(data['x'], data['y'])) + #self.updateItem() - def data(self): - """Return the user data associated with this spot.""" - return self._data['data'] + #def data(self): + #"""Return the user data associated with this spot.""" + #return self._data['data'] - def size(self): - """Return the size of this spot. - If the spot has no explicit size set, then return the ScatterPlotItem's default size instead.""" - if self._data['size'] == -1: - return self._plot.opts['size'] - else: - return self._data['size'] + #def size(self): + #"""Return the size of this spot. + #If the spot has no explicit size set, then return the ScatterPlotItem's default size instead.""" + #if self._data['size'] == -1: + #return self._plot.opts['size'] + #else: + #return self._data['size'] - def setSize(self, size): - """Set the size of this spot. - If the size is set to -1, then the ScatterPlotItem's default size - will be used instead.""" - self._data['size'] = size - self.updateItem() + #def setSize(self, size): + #"""Set the size of this spot. + #If the size is set to -1, then the ScatterPlotItem's default size + #will be used instead.""" + #self._data['size'] = size + #self.updateItem() - def symbol(self): - """Return the symbol of this spot. - If the spot has no explicit symbol set, then return the ScatterPlotItem's default symbol instead. - """ - symbol = self._data['symbol'] - if symbol is None: - symbol = self._plot.opts['symbol'] - try: - n = int(symbol) - symbol = list(Symbols.keys())[n % len(Symbols)] - except: - pass - return symbol + #def symbol(self): + #"""Return the symbol of this spot. + #If the spot has no explicit symbol set, then return the ScatterPlotItem's default symbol instead. + #""" + #symbol = self._data['symbol'] + #if symbol is None: + #symbol = self._plot.opts['symbol'] + #try: + #n = int(symbol) + #symbol = list(Symbols.keys())[n % len(Symbols)] + #except: + #pass + #return symbol - def setSymbol(self, symbol): - """Set the symbol for this spot. - If the symbol is set to '', then the ScatterPlotItem's default symbol will be used instead.""" - self._data['symbol'] = symbol - self.updateItem() + #def setSymbol(self, symbol): + #"""Set the symbol for this spot. + #If the symbol is set to '', then the ScatterPlotItem's default symbol will be used instead.""" + #self._data['symbol'] = symbol + #self.updateItem() - def pen(self): - pen = self._data['pen'] - if pen is None: - pen = self._plot.opts['pen'] - return fn.mkPen(pen) + #def pen(self): + #pen = self._data['pen'] + #if pen is None: + #pen = self._plot.opts['pen'] + #return fn.mkPen(pen) - def setPen(self, *args, **kargs): - """Set the outline pen for this spot""" - pen = fn.mkPen(*args, **kargs) - self._data['pen'] = pen - self.updateItem() + #def setPen(self, *args, **kargs): + #"""Set the outline pen for this spot""" + #pen = fn.mkPen(*args, **kargs) + #self._data['pen'] = pen + #self.updateItem() - def resetPen(self): - """Remove the pen set for this spot; the scatter plot's default pen will be used instead.""" - self._data['pen'] = None ## Note this is NOT the same as calling setPen(None) - self.updateItem() + #def resetPen(self): + #"""Remove the pen set for this spot; the scatter plot's default pen will be used instead.""" + #self._data['pen'] = None ## Note this is NOT the same as calling setPen(None) + #self.updateItem() - def brush(self): - brush = self._data['brush'] - if brush is None: - brush = self._plot.opts['brush'] - return fn.mkBrush(brush) + #def brush(self): + #brush = self._data['brush'] + #if brush is None: + #brush = self._plot.opts['brush'] + #return fn.mkBrush(brush) - def setBrush(self, *args, **kargs): - """Set the fill brush for this spot""" - brush = fn.mkBrush(*args, **kargs) - self._data['brush'] = brush - self.updateItem() + #def setBrush(self, *args, **kargs): + #"""Set the fill brush for this spot""" + #brush = fn.mkBrush(*args, **kargs) + #self._data['brush'] = brush + #self.updateItem() - def resetBrush(self): - """Remove the brush set for this spot; the scatter plot's default brush will be used instead.""" - self._data['brush'] = None ## Note this is NOT the same as calling setBrush(None) - self.updateItem() + #def resetBrush(self): + #"""Remove the brush set for this spot; the scatter plot's default brush will be used instead.""" + #self._data['brush'] = None ## Note this is NOT the same as calling setBrush(None) + #self.updateItem() - def setData(self, data): - """Set the user-data associated with this spot""" - self._data['data'] = data + #def setData(self, data): + #"""Set the user-data associated with this spot""" + #self._data['data'] = data -class PixmapSpotItem(SpotItem, QtGui.QGraphicsPixmapItem): - def __init__(self, data, plot): - QtGui.QGraphicsPixmapItem.__init__(self) - self.setFlags(self.flags() | self.ItemIgnoresTransformations) - SpotItem.__init__(self, data, plot) +#class PixmapSpotItem(SpotItem, QtGui.QGraphicsPixmapItem): + #def __init__(self, data, plot): + #QtGui.QGraphicsPixmapItem.__init__(self) + #self.setFlags(self.flags() | self.ItemIgnoresTransformations) + #SpotItem.__init__(self, data, plot) - def setPixmap(self, pixmap): - QtGui.QGraphicsPixmapItem.setPixmap(self, pixmap) - self.setOffset(-pixmap.width()/2.+0.5, -pixmap.height()/2.) + #def setPixmap(self, pixmap): + #QtGui.QGraphicsPixmapItem.setPixmap(self, pixmap) + #self.setOffset(-pixmap.width()/2.+0.5, -pixmap.height()/2.) - def updateItem(self): - symbolOpts = (self._data['pen'], self._data['brush'], self._data['size'], self._data['symbol']) + #def updateItem(self): + #symbolOpts = (self._data['pen'], self._data['brush'], self._data['size'], self._data['symbol']) - ## If all symbol options are default, use default pixmap - if symbolOpts == (None, None, -1, ''): - pixmap = self._plot.defaultSpotPixmap() - else: - pixmap = makeSymbolPixmap(size=self.size(), pen=self.pen(), brush=self.brush(), symbol=self.symbol()) - self.setPixmap(pixmap) + ### If all symbol options are default, use default pixmap + #if symbolOpts == (None, None, -1, ''): + #pixmap = self._plot.defaultSpotPixmap() + #else: + #pixmap = makeSymbolPixmap(size=self.size(), pen=self.pen(), brush=self.brush(), symbol=self.symbol()) + #self.setPixmap(pixmap) -class PathSpotItem(SpotItem, QtGui.QGraphicsPathItem): - def __init__(self, data, plot): - QtGui.QGraphicsPathItem.__init__(self) - SpotItem.__init__(self, data, plot) +#class PathSpotItem(SpotItem, QtGui.QGraphicsPathItem): + #def __init__(self, data, plot): + #QtGui.QGraphicsPathItem.__init__(self) + #SpotItem.__init__(self, data, plot) - def updateItem(self): - QtGui.QGraphicsPathItem.setPath(self, Symbols[self.symbol()]) - QtGui.QGraphicsPathItem.setPen(self, self.pen()) - QtGui.QGraphicsPathItem.setBrush(self, self.brush()) - size = self.size() - self.resetTransform() - self.scale(size, size) + #def updateItem(self): + #QtGui.QGraphicsPathItem.setPath(self, Symbols[self.symbol()]) + #QtGui.QGraphicsPathItem.setPen(self, self.pen()) + #QtGui.QGraphicsPathItem.setBrush(self, self.brush()) + #size = self.size() + #self.resetTransform() + #self.scale(size, size) From 3a0d599d70ae0d69d293ad9daef4181d489c5e08 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Tue, 16 Oct 2012 20:54:42 -0400 Subject: [PATCH 2/4] scatterplot spots are clickable again --- graphicsItems/ScatterPlotItem.py | 166 ++++++++++++++++--------------- 1 file changed, 87 insertions(+), 79 deletions(-) diff --git a/graphicsItems/ScatterPlotItem.py b/graphicsItems/ScatterPlotItem.py index dfbfa155..2cfb5585 100644 --- a/graphicsItems/ScatterPlotItem.py +++ b/graphicsItems/ScatterPlotItem.py @@ -223,7 +223,7 @@ class ScatterPlotItem(GraphicsObject): self.fragments = None # fragment specification for pxmode; updated every time the view changes. self.fragmentAtlas = SymbolAtlas() - self.data = np.empty(0, dtype=[('x', float), ('y', float), ('size', float), ('symbol', object), ('pen', object), ('brush', object), ('data', object), ('fragCoords', object)]) + self.data = np.empty(0, dtype=[('x', float), ('y', float), ('size', float), ('symbol', object), ('pen', object), ('brush', object), ('data', object), ('fragCoords', object), ('item', object)]) self.bounds = [None, None] ## caches data bounds self._maxSpotWidth = 0 ## maximum size of the scale-variant portion of all spots self._maxSpotPxWidth = 0 ## maximum size of the scale-invariant portion of all spots @@ -681,6 +681,9 @@ class ScatterPlotItem(GraphicsObject): def points(self): + for rec in self.data: + if rec['item'] is None: + rec['item'] = SpotItem(rec, self) return self.data['item'] def pointsAt(self, pos): @@ -704,8 +707,8 @@ class ScatterPlotItem(GraphicsObject): #else: #print "No hit:", (x, y), (sx, sy) #print " ", (sx-s2x, sy-s2y), (sx+s2x, sy+s2y) - pts.sort(lambda a,b: cmp(b.zValue(), a.zValue())) - return pts + #pts.sort(lambda a,b: cmp(b.zValue(), a.zValue())) + return pts[::-1] def mouseClickEvent(self, ev): @@ -722,100 +725,105 @@ class ScatterPlotItem(GraphicsObject): ev.ignore() -#class SpotItem(GraphicsItem): - #""" - #Class referring to individual spots in a scatter plot. - #These can be retrieved by calling ScatterPlotItem.points() or - #by connecting to the ScatterPlotItem's click signals. - #""" +class SpotItem(object): + """ + Class referring to individual spots in a scatter plot. + These can be retrieved by calling ScatterPlotItem.points() or + by connecting to the ScatterPlotItem's click signals. + """ - #def __init__(self, data, plot): + def __init__(self, data, plot): #GraphicsItem.__init__(self, register=False) - #self._data = data - #self._plot = plot - ##self._viewBox = None - ##self._viewWidget = None + self._data = data + self._plot = plot #self.setParentItem(plot) #self.setPos(QtCore.QPointF(data['x'], data['y'])) #self.updateItem() - #def data(self): - #"""Return the user data associated with this spot.""" - #return self._data['data'] + def data(self): + """Return the user data associated with this spot.""" + return self._data['data'] - #def size(self): - #"""Return the size of this spot. - #If the spot has no explicit size set, then return the ScatterPlotItem's default size instead.""" - #if self._data['size'] == -1: - #return self._plot.opts['size'] - #else: - #return self._data['size'] + def size(self): + """Return the size of this spot. + If the spot has no explicit size set, then return the ScatterPlotItem's default size instead.""" + if self._data['size'] == -1: + return self._plot.opts['size'] + else: + return self._data['size'] - #def setSize(self, size): - #"""Set the size of this spot. - #If the size is set to -1, then the ScatterPlotItem's default size - #will be used instead.""" - #self._data['size'] = size - #self.updateItem() + def pos(self): + return Point(self._data['x'], self._data['y']) - #def symbol(self): - #"""Return the symbol of this spot. - #If the spot has no explicit symbol set, then return the ScatterPlotItem's default symbol instead. - #""" - #symbol = self._data['symbol'] - #if symbol is None: - #symbol = self._plot.opts['symbol'] - #try: - #n = int(symbol) - #symbol = list(Symbols.keys())[n % len(Symbols)] - #except: - #pass - #return symbol + def setSize(self, size): + """Set the size of this spot. + If the size is set to -1, then the ScatterPlotItem's default size + will be used instead.""" + self._data['size'] = size + self.updateItem() - #def setSymbol(self, symbol): - #"""Set the symbol for this spot. - #If the symbol is set to '', then the ScatterPlotItem's default symbol will be used instead.""" - #self._data['symbol'] = symbol - #self.updateItem() + def symbol(self): + """Return the symbol of this spot. + If the spot has no explicit symbol set, then return the ScatterPlotItem's default symbol instead. + """ + symbol = self._data['symbol'] + if symbol is None: + symbol = self._plot.opts['symbol'] + try: + n = int(symbol) + symbol = list(Symbols.keys())[n % len(Symbols)] + except: + pass + return symbol + + def setSymbol(self, symbol): + """Set the symbol for this spot. + If the symbol is set to '', then the ScatterPlotItem's default symbol will be used instead.""" + self._data['symbol'] = symbol + self.updateItem() - #def pen(self): - #pen = self._data['pen'] - #if pen is None: - #pen = self._plot.opts['pen'] - #return fn.mkPen(pen) + def pen(self): + pen = self._data['pen'] + if pen is None: + pen = self._plot.opts['pen'] + return fn.mkPen(pen) - #def setPen(self, *args, **kargs): - #"""Set the outline pen for this spot""" - #pen = fn.mkPen(*args, **kargs) - #self._data['pen'] = pen - #self.updateItem() + def setPen(self, *args, **kargs): + """Set the outline pen for this spot""" + pen = fn.mkPen(*args, **kargs) + self._data['pen'] = pen + self.updateItem() - #def resetPen(self): - #"""Remove the pen set for this spot; the scatter plot's default pen will be used instead.""" - #self._data['pen'] = None ## Note this is NOT the same as calling setPen(None) - #self.updateItem() + def resetPen(self): + """Remove the pen set for this spot; the scatter plot's default pen will be used instead.""" + self._data['pen'] = None ## Note this is NOT the same as calling setPen(None) + self.updateItem() - #def brush(self): - #brush = self._data['brush'] - #if brush is None: - #brush = self._plot.opts['brush'] - #return fn.mkBrush(brush) + def brush(self): + brush = self._data['brush'] + if brush is None: + brush = self._plot.opts['brush'] + return fn.mkBrush(brush) - #def setBrush(self, *args, **kargs): - #"""Set the fill brush for this spot""" - #brush = fn.mkBrush(*args, **kargs) - #self._data['brush'] = brush - #self.updateItem() + def setBrush(self, *args, **kargs): + """Set the fill brush for this spot""" + brush = fn.mkBrush(*args, **kargs) + self._data['brush'] = brush + self.updateItem() - #def resetBrush(self): - #"""Remove the brush set for this spot; the scatter plot's default brush will be used instead.""" - #self._data['brush'] = None ## Note this is NOT the same as calling setBrush(None) - #self.updateItem() + def resetBrush(self): + """Remove the brush set for this spot; the scatter plot's default brush will be used instead.""" + self._data['brush'] = None ## Note this is NOT the same as calling setBrush(None) + self.updateItem() - #def setData(self, data): - #"""Set the user-data associated with this spot""" - #self._data['data'] = data + def setData(self, data): + """Set the user-data associated with this spot""" + self._data['data'] = data + def updateItem(self): + self._data['fragCoords'] = None + self._plot.updateSpots([self._data]) + self._plot.invalidate() #class PixmapSpotItem(SpotItem, QtGui.QGraphicsPixmapItem): #def __init__(self, data, plot): From 3c5503039f17ad32a7b73f9d4ae99101915fba6f Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Tue, 16 Oct 2012 22:35:53 -0400 Subject: [PATCH 3/4] speedup --- examples/ScatterPlotSpeedTest.py | 3 +- graphicsItems/ScatterPlotItem.py | 98 ++++++++++++++++++++------------ 2 files changed, 63 insertions(+), 38 deletions(-) diff --git a/examples/ScatterPlotSpeedTest.py b/examples/ScatterPlotSpeedTest.py index 17711d23..a44e58e3 100644 --- a/examples/ScatterPlotSpeedTest.py +++ b/examples/ScatterPlotSpeedTest.py @@ -49,7 +49,8 @@ def update(): s = np.clip(dt*3., 0, 1) fps = fps * (1-s) + (1.0/dt) * s p.setTitle('%0.2f fps' % fps) - app.processEvents() ## force complete redraw for every plot + p.repaint() + #app.processEvents() ## force complete redraw for every plot timer = QtCore.QTimer() timer.timeout.connect(update) timer.start(0) diff --git a/graphicsItems/ScatterPlotItem.py b/graphicsItems/ScatterPlotItem.py index 2cfb5585..c1b150e2 100644 --- a/graphicsItems/ScatterPlotItem.py +++ b/graphicsItems/ScatterPlotItem.py @@ -105,21 +105,27 @@ class SymbolAtlas: self.atlas = None # atlas as QPixmap self.atlasValid = False - def getSymbolCoords(self, symbol, size, pen, brush): - key = (symbol, size, fn.colorTuple(pen.color()), pen.width(), pen.style(), fn.colorTuple(brush.color())) - if key not in self.symbolMap: - newCoords = SymbolAtlas.SymbolCoords() - self.symbolMap[key] = newCoords - self.invalidateAtlas() - #try: - #self.addToAtlas(key) ## squeeze this into the atlas if there is room - #except: - #self.buildAtlas() ## otherwise, we need to rebuild - - return self.symbolMap[key] - - def invalidateAtlas(self): - self.atlasValid = False + def getSymbolCoords(self, opts): + """ + Given a list of spot records, return an object representing the coordinates of that symbol within the atlas + """ + coords = np.empty(len(opts), dtype=object) + for i, rec in enumerate(opts): + symbol, size, pen, brush = rec['symbol'], rec['size'], rec['pen'], rec['brush'] + pen = fn.mkPen(pen) if not isinstance(pen, QtGui.QPen) else pen + brush = fn.mkBrush(brush) if not isinstance(pen, QtGui.QBrush) else brush + key = (symbol, size, fn.colorTuple(pen.color()), pen.width(), pen.style(), fn.colorTuple(brush.color())) + if key not in self.symbolMap: + newCoords = SymbolAtlas.SymbolCoords() + self.symbolMap[key] = newCoords + self.atlasValid = False + #try: + #self.addToAtlas(key) ## squeeze this into the atlas if there is room + #except: + #self.buildAtlas() ## otherwise, we need to rebuild + + coords[i] = self.symbolMap[key] + return coords def buildAtlas(self): # get rendered array for all symbols, keep track of avg/max width @@ -512,29 +518,48 @@ class ScatterPlotItem(GraphicsObject): dataSet = self.data self._maxSpotWidth = 0 self._maxSpotPxWidth = 0 - if self.opts['pxMode']: - for rec in dataSet: - if rec['fragCoords'] is None: - - - rec['fragCoords'] = self.fragmentAtlas.getSymbolCoords(*self.getSpotOpts(rec)) + invalidate = False self.measureSpotSizes(dataSet) + if self.opts['pxMode']: + mask = np.equal(dataSet['fragCoords'], None) + if np.any(mask): + invalidate = True + opts = self.getSpotOpts(dataSet[mask]) + coords = self.fragmentAtlas.getSymbolCoords(opts) + dataSet['fragCoords'][mask] = coords + + #for rec in dataSet: + #if rec['fragCoords'] is None: + #invalidate = True + #rec['fragCoords'] = self.fragmentAtlas.getSymbolCoords(*self.getSpotOpts(rec)) + if invalidate: + self.invalidate() - def getSpotOpts(self, rec): - symbol = rec['symbol'] - if symbol is None: - symbol = self.opts['symbol'] - size = rec['size'] - if size < 0: - size = self.opts['size'] - pen = rec['pen'] - if pen is None: - pen = self.opts['pen'] - brush = rec['brush'] - if brush is None: - brush = self.opts['brush'] - return (symbol, size, fn.mkPen(pen), fn.mkBrush(brush)) - + def getSpotOpts(self, recs): + if recs.ndim == 0: + rec = recs + symbol = rec['symbol'] + if symbol is None: + symbol = self.opts['symbol'] + size = rec['size'] + if size < 0: + size = self.opts['size'] + pen = rec['pen'] + if pen is None: + pen = self.opts['pen'] + brush = rec['brush'] + if brush is None: + brush = self.opts['brush'] + return (symbol, size, fn.mkPen(pen), fn.mkBrush(brush)) + else: + recs = recs.copy() + recs['symbol'][np.equal(recs['symbol'], None)] = self.opts['symbol'] + recs['size'][np.equal(recs['size'], -1)] = self.opts['size'] + recs['pen'][np.equal(recs['pen'], None)] = fn.mkPen(self.opts['pen']) + recs['brush'][np.equal(recs['brush'], None)] = fn.mkBrush(self.opts['brush']) + return recs + + def measureSpotSizes(self, dataSet): for rec in dataSet: @@ -642,7 +667,6 @@ class ScatterPlotItem(GraphicsObject): def paint(self, p, *args): #p.setPen(fn.mkPen('r')) #p.drawRect(self.boundingRect()) - if self.opts['pxMode']: atlas = self.fragmentAtlas.getAtlas() #arr = fn.imageToArray(atlas.toImage(), copy=True) From 916241e6aa2058363c5f67c6ab781530f5d3ec9f Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Thu, 25 Oct 2012 00:15:11 -0400 Subject: [PATCH 4/4] documentation updates for DockArea --- dockarea/Dock.py | 99 +++++++++++--------------------------------- dockarea/DockArea.py | 40 ++++++++++++++---- 2 files changed, 56 insertions(+), 83 deletions(-) diff --git a/dockarea/Dock.py b/dockarea/Dock.py index 63fafca8..35781535 100644 --- a/dockarea/Dock.py +++ b/dockarea/Dock.py @@ -77,6 +77,11 @@ class Dock(QtGui.QWidget, DockDrop): return name == 'dock' def setStretch(self, x=None, y=None): + """ + Set the 'target' size for this Dock. + The actual size will be determined by comparing this Dock's + stretch value to the rest of the docks it shares space with. + """ #print "setStretch", self, x, y #self._stretch = (x, y) if x is None: @@ -100,6 +105,10 @@ class Dock(QtGui.QWidget, DockDrop): #return self._stretch def hideTitleBar(self): + """ + Hide the title bar for this Dock. + This will prevent the Dock being moved by the user. + """ self.label.hide() self.labelHidden = True if 'center' in self.allowedAreas: @@ -107,12 +116,21 @@ class Dock(QtGui.QWidget, DockDrop): self.updateStyle() def showTitleBar(self): + """ + Show the title bar for this Dock. + """ self.label.show() self.labelHidden = False self.allowedAreas.add('center') self.updateStyle() def setOrientation(self, o='auto', force=False): + """ + Sets the orientation of the title bar for this Dock. + Must be one of 'auto', 'horizontal', or 'vertical'. + By default ('auto'), the orientation is determined + based on the aspect ratio of the Dock. + """ #print self.name(), "setOrientation", o, force if o == 'auto' and self.autoOrient: if self.container().type() == 'tab': @@ -127,6 +145,7 @@ class Dock(QtGui.QWidget, DockDrop): self.updateStyle() def updateStyle(self): + ## updates orientation and appearance of title bar #print self.name(), "update style:", self.orientation, self.moveLabel, self.label.isVisible() if self.labelHidden: self.widgetArea.setStyleSheet(self.nStyle) @@ -154,6 +173,10 @@ class Dock(QtGui.QWidget, DockDrop): 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. + Each Dock uses a QGridLayout to arrange widgets within. + """ if row is None: row = self.currentRow self.currentRow = max(row+1, self.currentRow) @@ -188,7 +211,8 @@ class Dock(QtGui.QWidget, DockDrop): def __repr__(self): return "" % (self.name(), self.stretch()) - + + class DockLabel(VerticalLabel): sigClicked = QtCore.Signal(object, object) @@ -287,76 +311,3 @@ class DockLabel(VerticalLabel): -#class DockLabel(QtGui.QWidget): - #def __init__(self, text, dock): - #QtGui.QWidget.__init__(self) - #self._text = text - #self.dock = dock - #self.orientation = None - #self.setOrientation('horizontal') - - #def text(self): - #return self._text - - #def mousePressEvent(self, ev): - #if ev.button() == QtCore.Qt.LeftButton: - #self.pressPos = ev.pos() - #self.startedDrag = False - #ev.accept() - - #def mouseMoveEvent(self, ev): - #if not self.startedDrag and (ev.pos() - self.pressPos).manhattanLength() > QtGui.QApplication.startDragDistance(): - #self.dock.startDrag() - #ev.accept() - ##print ev.pos() - - #def mouseReleaseEvent(self, ev): - #ev.accept() - - #def mouseDoubleClickEvent(self, ev): - #if ev.button() == QtCore.Qt.LeftButton: - #self.dock.float() - - #def setOrientation(self, o): - #if self.orientation == o: - #return - #self.orientation = o - #self.update() - #self.updateGeometry() - - #def paintEvent(self, ev): - #p = QtGui.QPainter(self) - #p.setBrush(QtGui.QBrush(QtGui.QColor(100, 100, 200))) - #p.setPen(QtGui.QPen(QtGui.QColor(50, 50, 100))) - #p.drawRect(self.rect().adjusted(0, 0, -1, -1)) - - #p.setPen(QtGui.QPen(QtGui.QColor(255, 255, 255))) - - #if self.orientation == 'vertical': - #p.rotate(-90) - #rgn = QtCore.QRect(-self.height(), 0, self.height(), self.width()) - #else: - #rgn = self.rect() - #align = QtCore.Qt.AlignTop|QtCore.Qt.AlignHCenter - - #self.hint = p.drawText(rgn, align, self.text()) - #p.end() - - #if self.orientation == 'vertical': - #self.setMaximumWidth(self.hint.height()) - #self.setMaximumHeight(16777215) - #else: - #self.setMaximumHeight(self.hint.height()) - #self.setMaximumWidth(16777215) - - #def sizeHint(self): - #if self.orientation == 'vertical': - #if hasattr(self, 'hint'): - #return QtCore.QSize(self.hint.height(), self.hint.width()) - #else: - #return QtCore.QSize(19, 50) - #else: - #if hasattr(self, 'hint'): - #return QtCore.QSize(self.hint.width(), self.hint.height()) - #else: - #return QtCore.QSize(50, 19) diff --git a/dockarea/DockArea.py b/dockarea/DockArea.py index 49dd95ff..d49f02ad 100644 --- a/dockarea/DockArea.py +++ b/dockarea/DockArea.py @@ -35,8 +35,18 @@ class DockArea(Container, QtGui.QWidget, DockDrop): def addDock(self, dock, position='bottom', relativeTo=None): """Adds a dock to this area. - position may be: bottom, top, left, right, over, under - If relativeTo specifies an existing dock, the new dock is added adjacent to it""" + + =========== ================================================================= + Arguments: + dock The new Dock object to add. + position 'bottom', 'top', 'left', 'right', 'over', or 'under' + relativeTo If relativeTo is None, then the new Dock is added to fill an + entire edge of the window. If relativeTo is another Dock, then + the new Dock is placed adjacent to it (or in a tabbed + configuration for 'over' and 'under'). + =========== ================================================================= + + """ ## Determine the container to insert this dock into. ## If there is no neighbor, then the container is the top. @@ -90,6 +100,17 @@ class DockArea(Container, QtGui.QWidget, DockDrop): dock.area = self self.docks[dock.name()] = dock + def moveDock(self, dock, position, neighbor): + """ + Move an existing Dock to a new location. + """ + old = dock.container() + ## Moving to the edge of a tabbed dock causes a drop outside the tab box + if position in ['left', 'right', 'top', 'bottom'] and neighbor is not None and neighbor.container() is not None and neighbor.container().type() == 'tab': + neighbor = neighbor.container() + self.addDock(dock, position, neighbor) + old.apoptose() + def getContainer(self, obj): if obj is None: return self @@ -131,13 +152,6 @@ class DockArea(Container, QtGui.QWidget, DockDrop): return 0 return 1 - def moveDock(self, dock, position, neighbor): - old = dock.container() - ## Moving to the edge of a tabbed dock causes a drop outside the tab box - if position in ['left', 'right', 'top', 'bottom'] and neighbor is not None and neighbor.container() is not None and neighbor.container().type() == 'tab': - neighbor = neighbor.container() - self.addDock(dock, position, neighbor) - old.apoptose() #def paintEvent(self, ev): #self.drawDockOverlay() @@ -159,6 +173,7 @@ class DockArea(Container, QtGui.QWidget, DockDrop): return area def floatDock(self, dock): + """Removes *dock* from this DockArea and places it in a new window.""" area = self.addTempArea() area.win.resize(dock.size()) area.moveDock(dock, 'top', None) @@ -170,6 +185,9 @@ class DockArea(Container, QtGui.QWidget, DockDrop): area.window().close() def saveState(self): + """ + Return a serialized (storable) representation of the state of + all Docks in this DockArea.""" state = {'main': self.childState(self.topContainer), 'float': []} for a in self.tempAreas: geo = a.win.geometry() @@ -188,6 +206,10 @@ class DockArea(Container, QtGui.QWidget, DockDrop): def restoreState(self, state): + """ + Restore Dock configuration as generated by saveState. + """ + ## 1) make dict of all docks and list of existing containers containers, docks = self.findAll() oldTemps = self.tempAreas[:]