From e87eaa652d5a2959608c4954ed900e5874202c33 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 14 Jul 2017 15:10:04 -0700 Subject: [PATCH 001/135] Docstring correction --- pyqtgraph/graphicsItems/ImageItem.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index 3d45ad77..9588c586 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -214,7 +214,8 @@ class ImageItem(GraphicsObject): border Sets the pen used when drawing the image border. Default is None. autoDownsample (bool) If True, the image is automatically downsampled to match the screen resolution. This improves performance for large images and - reduces aliasing. + reduces aliasing. If autoDownsample is not specified, then ImageItem will + choose whether to downsample the image based on its size. ================= ========================================================================= From d343eb044de8decf2e07ca6b9ed851398ee763e0 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 14 Jul 2017 15:10:16 -0700 Subject: [PATCH 002/135] Fix errors getting bounds on nanny data --- pyqtgraph/graphicsItems/PlotCurveItem.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index d66a8a99..fac9ee57 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -132,6 +132,8 @@ class PlotCurveItem(GraphicsObject): if any(np.isinf(b)): mask = np.isfinite(d) d = d[mask] + if len(d) == 0: + return (None, None) b = (d.min(), d.max()) elif frac <= 0.0: @@ -173,7 +175,7 @@ class PlotCurveItem(GraphicsObject): if self._boundingRect is None: (xmn, xmx) = self.dataBounds(ax=0) (ymn, ymx) = self.dataBounds(ax=1) - if xmn is None: + if xmn is None or ymn is None: return QtCore.QRectF() px = py = 0.0 From 5855aa8627fa804283c413a2ffbd0e1b14e36eff Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 27 Jul 2017 22:20:26 -0700 Subject: [PATCH 003/135] Code cleanup; no functional changes --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 130 ++------------------- 1 file changed, 12 insertions(+), 118 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 4cab8662..9af43614 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -85,7 +85,6 @@ class ViewBox(GraphicsWidget): sigXRangeChanged = QtCore.Signal(object, object) sigRangeChangedManually = QtCore.Signal(object) sigRangeChanged = QtCore.Signal(object, object) - #sigActionPositionChanged = QtCore.Signal(object) sigStateChanged = QtCore.Signal(object) sigTransformChanged = QtCore.Signal(object) sigResized = QtCore.Signal(object) @@ -128,8 +127,6 @@ class ViewBox(GraphicsWidget): self.name = None self.linksBlocked = False self.addedItems = [] - #self.gView = view - #self.showGrid = showGrid self._matrixNeedsUpdate = True ## indicates that range has changed, but matrix update was deferred self._autoRangeNeedsUpdate = True ## indicates auto-range needs to be recomputed. @@ -188,9 +185,6 @@ class ViewBox(GraphicsWidget): self.background.setPen(fn.mkPen(None)) self.updateBackground() - #self.useLeftButtonPan = pyqtgraph.getConfigOption('leftButtonPan') # normally use left button to pan - # this also enables capture of keyPressEvents. - ## Make scale box that is shown when dragging on the view self.rbScaleBox = QtGui.QGraphicsRectItem(0, 0, 1, 1) self.rbScaleBox.setPen(fn.mkPen((255,255,100), width=1)) @@ -239,7 +233,6 @@ class ViewBox(GraphicsWidget): ViewBox.updateAllViewLists() sid = id(self) self.destroyed.connect(lambda: ViewBox.forgetView(sid, name) if (ViewBox is not None and 'sid' in locals() and 'name' in locals()) else None) - #self.destroyed.connect(self.unregister) def unregister(self): """ @@ -288,16 +281,12 @@ class ViewBox(GraphicsWidget): self.prepareForPaint() self._lastScene = scene - - - def prepareForPaint(self): #autoRangeEnabled = (self.state['autoRange'][0] is not False) or (self.state['autoRange'][1] is not False) # don't check whether auto range is enabled here--only check when setting dirty flag. if self._autoRangeNeedsUpdate: # and autoRangeEnabled: self.updateAutoRange() - if self._matrixNeedsUpdate: - self.updateMatrix() + self.updateMatrix() def getState(self, copy=True): """Return the current state of the ViewBox. @@ -326,7 +315,6 @@ class ViewBox(GraphicsWidget): del state['linkedViews'] self.state.update(state) - #self.updateMatrix() self.updateViewRange() self.sigStateChanged.emit(self) @@ -353,12 +341,6 @@ class ViewBox(GraphicsWidget): self.state['mouseMode'] = mode self.sigStateChanged.emit(self) - #def toggleLeftAction(self, act): ## for backward compatibility - #if act.text() is 'pan': - #self.setLeftButtonAction('pan') - #elif act.text() is 'zoom': - #self.setLeftButtonAction('rect') - def setLeftButtonAction(self, mode='rect'): ## for backward compatibility if mode.lower() == 'rect': self.setMouseMode(ViewBox.RectMode) @@ -405,7 +387,6 @@ class ViewBox(GraphicsWidget): if not ignoreBounds: self.addedItems.append(item) self.updateAutoRange() - #print "addItem:", item, item.boundingRect() def removeItem(self, item): """Remove an item from this view.""" @@ -562,10 +543,6 @@ class ViewBox(GraphicsWidget): # If nothing has changed, we are done. if any(changed): - #if update and self.matrixNeedsUpdate: - #self.updateMatrix(changed) - #return - self.sigStateChanged.emit(self) # Update target rect for debugging @@ -581,21 +558,6 @@ class ViewBox(GraphicsWidget): self._autoRangeNeedsUpdate = True #self.updateAutoRange() - ## Update view matrix only if requested - #if update: - #self.updateMatrix(changed) - ## Otherwise, indicate that the matrix needs to be updated - #else: - #self.matrixNeedsUpdate = True - - ## Inform linked views that the range has changed <> - #for ax, range in changes.items(): - #link = self.linkedView(ax) - #if link is not None: - #link.linkedViewChanged(self, ax) - - - def setYRange(self, min, max, padding=None, update=True): """ Set the visible Y range of the view to [*min*, *max*]. @@ -675,10 +637,6 @@ class ViewBox(GraphicsWidget): for kwd in kwds: if kwd not in allowed: raise ValueError("Invalid keyword argument '%s'." % kwd) - #for kwd in ['xLimits', 'yLimits', 'minRange', 'maxRange']: - #if kwd in kwds and self.state['limits'][kwd] != kwds[kwd]: - #self.state['limits'][kwd] = kwds[kwd] - #update = True for axis in [0,1]: for mnmx in [0,1]: kwd = [['xMin', 'xMax'], ['yMin', 'yMax']][axis][mnmx] @@ -694,9 +652,6 @@ class ViewBox(GraphicsWidget): if update: self.updateViewRange() - - - def scaleBy(self, s=None, center=None, x=None, y=None): """ @@ -762,8 +717,6 @@ class ViewBox(GraphicsWidget): y = vr.top()+y, vr.bottom()+y if x is not None or y is not None: self.setRange(xRange=x, yRange=y, padding=0) - - def enableAutoRange(self, axis=None, enable=True, x=None, y=None): """ @@ -773,11 +726,6 @@ class ViewBox(GraphicsWidget): The argument *enable* may optionally be a float (0.0-1.0) which indicates the fraction of the data that should be visible (this only works with items implementing a dataRange method, such as PlotDataItem). """ - #print "autorange:", axis, enable - #if not enable: - #import traceback - #traceback.print_stack() - # support simpler interface: if x is not None or y is not None: if x is not None: @@ -813,10 +761,6 @@ class ViewBox(GraphicsWidget): self.state['autoRange'][ax] = enable self._autoRangeNeedsUpdate |= (enable is not False) self.update() - - - #if needAutoRangeUpdate: - # self.updateAutoRange() self.sigStateChanged.emit(self) @@ -828,6 +772,8 @@ class ViewBox(GraphicsWidget): return self.state['autoRange'][:] def setAutoPan(self, x=None, y=None): + """Set whether automatic range will only pan (not scale) the view. + """ if x is not None: self.state['autoPan'][0] = x if y is not None: @@ -836,6 +782,9 @@ class ViewBox(GraphicsWidget): self.updateAutoRange() def setAutoVisible(self, x=None, y=None): + """Set whether automatic range uses only visible data when determining + the range to show. + """ if x is not None: self.state['autoVisibleOnly'][0] = x if x is True: @@ -924,7 +873,6 @@ class ViewBox(GraphicsWidget): """Link this view's Y axis to another view. (see LinkView)""" self.linkView(self.YAxis, view) - def linkView(self, axis, view): """ Link X or Y axes of two views and unlink any previously connected axes. *axis* must be ViewBox.XAxis or ViewBox.YAxis. @@ -1118,7 +1066,6 @@ class ViewBox(GraphicsWidget): return self.state['aspectLocked'] = ratio if ratio != currentRatio: ## If this would change the current range, do that now - #self.setRange(0, self.state['viewRange'][0][0], self.state['viewRange'][0][1]) self.updateViewRange() self.updateAutoRange() @@ -1130,12 +1077,9 @@ class ViewBox(GraphicsWidget): Return the transform that maps from child(item in the childGroup) coordinates to local coordinates. (This maps from inside the viewbox to outside) """ - if self._matrixNeedsUpdate: - self.updateMatrix() + self.updateMatrix() m = self.childGroup.transform() - #m1 = QtGui.QTransform() - #m1.translate(self.childGroup.pos().x(), self.childGroup.pos().y()) - return m #*m1 + return m def mapToView(self, obj): """Maps from the local coordinates of the ViewBox to the coordinate system displayed inside the ViewBox""" @@ -1163,7 +1107,6 @@ class ViewBox(GraphicsWidget): def mapFromViewToItem(self, item, obj): """Maps *obj* from view coordinates to the local coordinate system of *item*.""" return self.childGroup.mapToItem(item, obj) - #return item.mapFromScene(self.mapViewToScene(obj)) def mapViewToDevice(self, obj): return self.mapToDevice(self.mapFromView(obj)) @@ -1177,25 +1120,9 @@ class ViewBox(GraphicsWidget): px, py = [Point(self.mapToView(v) - o) for v in self.pixelVectors()] return (px.length(), py.length()) - def itemBoundingRect(self, item): """Return the bounding rect of the item in view coordinates""" return self.mapSceneToView(item.sceneBoundingRect()).boundingRect() - - #def viewScale(self): - #vr = self.viewRect() - ##print "viewScale:", self.range - #xd = vr.width() - #yd = vr.height() - #if xd == 0 or yd == 0: - #print "Warning: 0 range in view:", xd, yd - #return np.array([1,1]) - - ##cs = self.canvas().size() - #cs = self.boundingRect() - #scale = np.array([cs.width() / xd, cs.height() / yd]) - ##print "view scale:", scale - #return scale def wheelEvent(self, ev, axis=None): mask = np.array(self.state['mouseEnabled'], dtype=np.float) @@ -1206,13 +1133,11 @@ class ViewBox(GraphicsWidget): s = ((mask * 0.02) + 1) ** (ev.delta() * self.state['wheelScaleFactor']) # actual scaling factor center = Point(fn.invertQTransform(self.childGroup.transform()).map(ev.pos())) - #center = ev.pos() self._resetTarget() self.scaleBy(s, center) self.sigRangeChangedManually.emit(self.state['mouseEnabled']) ev.accept() - def mouseClickEvent(self, ev): if ev.button() == QtCore.Qt.RightButton and self.menuEnabled(): @@ -1251,7 +1176,6 @@ class ViewBox(GraphicsWidget): if ev.isFinish(): ## This is the final move in the drag; change the view scale now #print "finish" self.rbScaleBox.hide() - #ax = QtCore.QRectF(Point(self.pressPos), Point(self.mousePos)) ax = QtCore.QRectF(Point(ev.buttonDownPos(ev.button())), Point(pos)) ax = self.childGroup.mapRectFromParent(ax) self.showAxRect(ax) @@ -1301,12 +1225,6 @@ class ViewBox(GraphicsWidget): ctrl-- : moves backward in the zooming stack (if it exists) """ - #print ev.key() - #print 'I intercepted a key press, but did not accept it' - - ## not implemented yet ? - #self.keypress.sigkeyPressEvent.emit() - ev.accept() if ev.text() == '-': self.scaleHistory(-1) @@ -1324,7 +1242,6 @@ class ViewBox(GraphicsWidget): if ptr != self.axHistoryPointer: self.axHistoryPointer = ptr self.showAxRect(self.axHistory[ptr]) - def updateScaleBox(self, p1, p2): r = QtCore.QRectF(p1, p2) @@ -1338,14 +1255,6 @@ class ViewBox(GraphicsWidget): self.setRange(ax.normalized()) # be sure w, h are correct coordinates self.sigRangeChangedManually.emit(self.state['mouseEnabled']) - #def mouseRect(self): - #vs = self.viewScale() - #vr = self.state['viewRange'] - ## Convert positions from screen (view) pixel coordinates to axis coordinates - #ax = QtCore.QRectF(self.pressPos[0]/vs[0]+vr[0][0], -(self.pressPos[1]/vs[1]-vr[1][1]), - #(self.mousePos[0]-self.pressPos[0])/vs[0], -(self.mousePos[1]-self.pressPos[1])/vs[1]) - #return(ax) - def allChildren(self, item=None): """Return a list of all children and grandchildren of this ViewBox""" if item is None: @@ -1356,8 +1265,6 @@ class ViewBox(GraphicsWidget): children.extend(self.allChildren(ch)) return children - - def childrenBounds(self, frac=None, orthoRange=(None,None), items=None): """Return the bounding range of all children. [[xmin, xmax], [ymin, ymax]] @@ -1380,8 +1287,6 @@ class ViewBox(GraphicsWidget): useY = True if hasattr(item, 'dataBounds'): - #bounds = self._itemBoundsCache.get(item, None) - #if bounds is None: if frac is None: frac = (1.0, 1.0) xr = item.dataBounds(0, frac=frac[0], orthoRange=orthoRange[0]) @@ -1414,9 +1319,6 @@ class ViewBox(GraphicsWidget): itemBounds.append((bounds, useX, useY, pxPad)) - #self._itemBoundsCache[item] = (bounds, useX, useY) - #else: - #bounds, useX, useY = bounds else: if int(item.flags() & item.ItemHasNoContents) > 0: continue @@ -1425,8 +1327,6 @@ class ViewBox(GraphicsWidget): bounds = self.mapFromItemToView(item, bounds).boundingRect() itemBounds.append((bounds, True, True, 0)) - #print itemBounds - ## determine tentative new range range = [None, None] for bounds, useX, useY, px in itemBounds: @@ -1442,14 +1342,11 @@ class ViewBox(GraphicsWidget): range[0] = [bounds.left(), bounds.right()] profiler() - #print "range", range - ## Now expand any bounds that have a pixel margin ## This must be done _after_ we have a good estimate of the new range ## to ensure that the pixel size is roughly accurate. w = self.width() h = self.height() - #print "w:", w, "h:", h if w > 0 and range[0] is not None: pxSize = (range[0][1] - range[0][0]) / w for bounds, useX, useY, px in itemBounds: @@ -1598,6 +1495,9 @@ class ViewBox(GraphicsWidget): link.linkedViewChanged(self, ax) def updateMatrix(self, changed=None): + if not self._matrixNeedsUpdate: + return + ## Make the childGroup's transform match the requested viewRange. bounds = self.rect() @@ -1648,7 +1548,6 @@ class ViewBox(GraphicsWidget): self.background.show() self.background.setBrush(fn.mkBrush(bg)) - def updateViewLists(self): try: self.window() @@ -1662,7 +1561,6 @@ 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: @@ -1676,16 +1574,11 @@ class ViewBox(GraphicsWidget): 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): @@ -1766,4 +1659,5 @@ class ViewBox(GraphicsWidget): self.scene().removeItem(self.locateGroup) self.locateGroup = None + from .ViewBoxMenu import ViewBoxMenu From 55d21a436f538c5f2c445398904024189298f071 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 27 Jul 2017 22:21:02 -0700 Subject: [PATCH 004/135] ViewBox: mark matrix dirty _before_ emitting change signal to ensure that slots can access the latest transform. --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 9af43614..8ade0c6b 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -1482,9 +1482,9 @@ class ViewBox(GraphicsWidget): self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][1])) if any(changed): + self._matrixNeedsUpdate = True self.sigRangeChanged.emit(self, self.state['viewRange']) self.update() - self._matrixNeedsUpdate = True # Inform linked views that the range has changed for ax in [0, 1]: From 6c7e0fae8ee13ae9b1e290bcef6be1ca13a40796 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 20 Oct 2016 12:10:53 -0700 Subject: [PATCH 005/135] Add signals for TreeWidget changes in check state, text, and columncount --- pyqtgraph/widgets/TreeWidget.py | 74 +++++++++++++++++++++++++++------ 1 file changed, 61 insertions(+), 13 deletions(-) diff --git a/pyqtgraph/widgets/TreeWidget.py b/pyqtgraph/widgets/TreeWidget.py index b98da6fa..b20f14fd 100644 --- a/pyqtgraph/widgets/TreeWidget.py +++ b/pyqtgraph/widgets/TreeWidget.py @@ -1,8 +1,6 @@ # -*- coding: utf-8 -*- -from weakref import * from ..Qt import QtGui, QtCore -from ..python2_3 import xrange - +from weakref import * __all__ = ['TreeWidget', 'TreeWidgetItem'] @@ -13,6 +11,9 @@ class TreeWidget(QtGui.QTreeWidget): This class demonstrates the absurd lengths one must go to to make drag/drop work.""" sigItemMoved = QtCore.Signal(object, object, object) # (item, parent, index) + sigItemCheckStateChanged = QtCore.Signal(object, object) + sigItemTextChanged = QtCore.Signal(object, object) + sigColumnCountChanged = QtCore.Signal(object, object) # self, count def __init__(self, parent=None): QtGui.QTreeWidget.__init__(self, parent) @@ -42,7 +43,7 @@ class TreeWidget(QtGui.QTreeWidget): def itemWidget(self, item, col): w = QtGui.QTreeWidget.itemWidget(self, item, col) - if w is not None: + if w is not None and hasattr(w, 'realChild'): w = w.realChild return w @@ -140,7 +141,6 @@ class TreeWidget(QtGui.QTreeWidget): def dropEvent(self, ev): QtGui.QTreeWidget.dropEvent(self, ev) self.updateDropFlags() - def updateDropFlags(self): ### intended to put a limit on how deep nests of children can go. @@ -165,9 +165,8 @@ class TreeWidget(QtGui.QTreeWidget): def informTreeWidgetChange(item): if hasattr(item, 'treeWidgetChanged'): item.treeWidgetChanged() - else: - for i in xrange(item.childCount()): - TreeWidget.informTreeWidgetChange(item.child(i)) + for i in xrange(item.childCount()): + TreeWidget.informTreeWidgetChange(item.child(i)) def addTopLevelItem(self, item): @@ -210,20 +209,51 @@ class TreeWidget(QtGui.QTreeWidget): #for item in items: #self.informTreeWidgetChange(item) - + def itemFromIndex(self, index): + """Return the item and column corresponding to a QModelIndex. + """ + col = index.column() + rows = [] + while index.row() >= 0: + rows.insert(0, index.row()) + index = index.parent() + item = self.topLevelItem(rows[0]) + for row in rows[1:]: + item = item.child(row) + return item, col + + def setColumnCount(self, c): + QtGui.QTreeWidget.setColumnCount(self, c) + self.sigColumnCountChanged.emit(self, c) + + class TreeWidgetItem(QtGui.QTreeWidgetItem): """ - TreeWidgetItem that keeps track of its own widgets. - Widgets may be added to columns before the item is added to a tree. + TreeWidgetItem that keeps track of its own widgets and expansion state. + + * Widgets may be added to columns before the item is added to a tree. + * Expanded state may be set before item is added to a tree. + * Adds setCheked and isChecked methods. + * Adds addChildren, insertChildren, and takeChildren methods. """ def __init__(self, *args): QtGui.QTreeWidgetItem.__init__(self, *args) self._widgets = {} # col: widget self._tree = None - + self._expanded = False def setChecked(self, column, checked): self.setCheckState(column, QtCore.Qt.Checked if checked else QtCore.Qt.Unchecked) + + def isChecked(self, col): + return self.checkState(col) == QtCore.Qt.Checked + + def setExpanded(self, exp): + self._expanded = exp + QtGui.QTreeWidgetItem.setExpanded(self, exp) + + def isExpanded(self): + return self._expanded def setWidget(self, column, widget): if column in self._widgets: @@ -251,7 +281,11 @@ class TreeWidgetItem(QtGui.QTreeWidgetItem): return for col, widget in self._widgets.items(): tree.setItemWidget(self, col, widget) - + QtGui.QTreeWidgetItem.setExpanded(self, self._expanded) + + def childItems(self): + return [self.child(i) for i in range(self.childCount())] + def addChild(self, child): QtGui.QTreeWidgetItem.addChild(self, child) TreeWidget.informTreeWidgetChange(child) @@ -285,4 +319,18 @@ class TreeWidgetItem(QtGui.QTreeWidgetItem): TreeWidget.informTreeWidgetChange(child) return childs + def setData(self, column, role, value): + # credit: ekhumoro + # http://stackoverflow.com/questions/13662020/how-to-implement-itemchecked-and-itemunchecked-signals-for-qtreewidget-in-pyqt4 + checkstate = self.checkState(column) + text = self.text(column) + QtGui.QTreeWidgetItem.setData(self, column, role, value) + treewidget = self.treeWidget() + if treewidget is None: + return + if (role == QtCore.Qt.CheckStateRole and checkstate != self.checkState(column)): + treewidget.sigItemCheckStateChanged.emit(self, column) + elif (role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole) and text != self.text(column)): + treewidget.sigItemTextChanged.emit(self, column) + From 0e06c504020fa1488dbd13538c578706f36b5b36 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 28 Jul 2017 15:57:45 -0700 Subject: [PATCH 006/135] Catch OSError from ForkedProcess that has already exited. --- pyqtgraph/multiprocess/processes.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/multiprocess/processes.py b/pyqtgraph/multiprocess/processes.py index c7e4a80c..02f259e5 100644 --- a/pyqtgraph/multiprocess/processes.py +++ b/pyqtgraph/multiprocess/processes.py @@ -321,9 +321,14 @@ class ForkedProcess(RemoteEventHandler): #os.kill(pid, 9) try: self.close(callSync='sync', timeout=timeout, noCleanup=True) ## ask the child process to exit and require that it return a confirmation. - os.waitpid(self.childPid, 0) except IOError: ## probably remote process has already quit pass + + try: + os.waitpid(self.childPid, 0) + except OSError: ## probably remote process has already quit + pass + self.hasJoined = True def kill(self): From 3fbc3864f2611d385bd88d4f2becf94a9a6f3722 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 28 Jul 2017 16:05:58 -0700 Subject: [PATCH 007/135] Wrap TreeWidget's invisible root item so that child items will receive tree change notifications --- pyqtgraph/widgets/TreeWidget.py | 51 +++++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/widgets/TreeWidget.py b/pyqtgraph/widgets/TreeWidget.py index b20f14fd..09ab8da5 100644 --- a/pyqtgraph/widgets/TreeWidget.py +++ b/pyqtgraph/widgets/TreeWidget.py @@ -141,7 +141,7 @@ class TreeWidget(QtGui.QTreeWidget): def dropEvent(self, ev): QtGui.QTreeWidget.dropEvent(self, ev) self.updateDropFlags() - + def updateDropFlags(self): ### intended to put a limit on how deep nests of children can go. ### self.childNestingLimit is upheld when moving items without children, but if the item being moved has children/grandchildren, the children/grandchildren @@ -168,7 +168,6 @@ class TreeWidget(QtGui.QTreeWidget): for i in xrange(item.childCount()): TreeWidget.informTreeWidgetChange(item.child(i)) - def addTopLevelItem(self, item): QtGui.QTreeWidget.addTopLevelItem(self, item) self.informTreeWidgetChange(item) @@ -208,6 +207,11 @@ class TreeWidget(QtGui.QTreeWidget): ## Why do we want to do this? It causes RuntimeErrors. #for item in items: #self.informTreeWidgetChange(item) + + def invisibleRootItem(self): + # wrap this item so that we can propagate tree change information + # to children. + return InvisibleRootItem(QtGui.QTreeWidget.invisibleRootItem(self)) def itemFromIndex(self, index): """Return the item and column corresponding to a QModelIndex. @@ -333,4 +337,47 @@ class TreeWidgetItem(QtGui.QTreeWidgetItem): treewidget.sigItemCheckStateChanged.emit(self, column) elif (role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole) and text != self.text(column)): treewidget.sigItemTextChanged.emit(self, column) + +class InvisibleRootItem(QtGui.QTreeWidgetItem): + """Wrapper around a TreeWidget's invisible root item that calls + TreeWidget.informTreeWidgetChange when child items are added/removed. + """ + def __init__(self, item): + self._real_item = item + + def addChild(self, child): + self._real_item.addChild(child) + TreeWidget.informTreeWidgetChange(child) + + def addChildren(self, childs): + self._real_item.addChildren(childs) + for child in childs: + TreeWidget.informTreeWidgetChange(child) + + def insertChild(self, index, child): + self._real_item.insertChild(index, child) + TreeWidget.informTreeWidgetChange(child) + + def insertChildren(self, index, childs): + self._real_item.addChildren(index, childs) + for child in childs: + TreeWidget.informTreeWidgetChange(child) + + def removeChild(self, child): + self._real_item.removeChild(child) + TreeWidget.informTreeWidgetChange(child) + + def takeChild(self, index): + child = self._real_item.takeChild(index) + TreeWidget.informTreeWidgetChange(child) + return child + + def takeChildren(self): + childs = self._real_item.takeChildren() + for child in childs: + TreeWidget.informTreeWidgetChange(child) + return childs + + def __getattr__(self, attr): + return getattr(self._real_item, attr) From f5775422c607195d7cc24ce360eabf48e0fee414 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 28 Jul 2017 16:18:31 -0700 Subject: [PATCH 008/135] py3 fix --- pyqtgraph/widgets/TreeWidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/widgets/TreeWidget.py b/pyqtgraph/widgets/TreeWidget.py index 09ab8da5..ace03b5c 100644 --- a/pyqtgraph/widgets/TreeWidget.py +++ b/pyqtgraph/widgets/TreeWidget.py @@ -165,7 +165,7 @@ class TreeWidget(QtGui.QTreeWidget): def informTreeWidgetChange(item): if hasattr(item, 'treeWidgetChanged'): item.treeWidgetChanged() - for i in xrange(item.childCount()): + for i in range(item.childCount()): TreeWidget.informTreeWidgetChange(item.child(i)) def addTopLevelItem(self, item): From ea51a65dfdbd4add0407f87cecc2922474176cbb Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 31 Jul 2017 10:03:13 -0700 Subject: [PATCH 009/135] Send click events to treewidgetitem --- pyqtgraph/widgets/TreeWidget.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/widgets/TreeWidget.py b/pyqtgraph/widgets/TreeWidget.py index ace03b5c..a37181cf 100644 --- a/pyqtgraph/widgets/TreeWidget.py +++ b/pyqtgraph/widgets/TreeWidget.py @@ -23,6 +23,7 @@ class TreeWidget(QtGui.QTreeWidget): self.setEditTriggers(QtGui.QAbstractItemView.EditKeyPressed|QtGui.QAbstractItemView.SelectedClicked) self.placeholders = [] self.childNestingLimit = None + self.itemClicked.connect(self._itemClicked) def setItemWidget(self, item, col, wid): """ @@ -230,7 +231,11 @@ class TreeWidget(QtGui.QTreeWidget): QtGui.QTreeWidget.setColumnCount(self, c) self.sigColumnCountChanged.emit(self, c) - + def _itemClicked(self, item, col): + if hasattr(item, 'itemClicked'): + item.itemClicked(col) + + class TreeWidgetItem(QtGui.QTreeWidgetItem): """ TreeWidgetItem that keeps track of its own widgets and expansion state. @@ -338,6 +343,12 @@ class TreeWidgetItem(QtGui.QTreeWidgetItem): elif (role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole) and text != self.text(column)): treewidget.sigItemTextChanged.emit(self, column) + def itemClicked(self, col): + """Called when this item is clicked on. + + Override this method to react to user clicks. + """ + class InvisibleRootItem(QtGui.QTreeWidgetItem): """Wrapper around a TreeWidget's invisible root item that calls From c719ad4355e5fae81c95cf1fb32206ae854dcb4a Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 31 Jul 2017 17:04:53 -0700 Subject: [PATCH 010/135] Check for existence of QtCore.QString before using it --- pyqtgraph/parametertree/parameterTypes.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py index e3ed8853..ace0c9a4 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -333,13 +333,14 @@ class SimpleParameter(Parameter): return fn(v) def _interpStr(self, v): + isQString = hasattr(QtCore, 'QString') and isinstance(v, QtCore.QString) if sys.version[0] == '2': - if isinstance(v, QtCore.QString): + if isQString: v = unicode(v) elif not isinstance(v, basestring): raise TypeError("Cannot set str parmeter from object %r" % v) else: - if isinstance(v, QtCore.QString): + if isQString: v = str(v) elif not isinstance(v, str): raise TypeError("Cannot set str parmeter from object %r" % v) From b4e722f07bd09e7adfbcead693c09e3f5ee1a974 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 31 Jul 2017 17:16:46 -0700 Subject: [PATCH 011/135] Loosen string type checking a bit; let asUnicode throw errors if it needs to. --- pyqtgraph/parametertree/parameterTypes.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py index ace0c9a4..8c1e587d 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -326,26 +326,12 @@ class SimpleParameter(Parameter): 'int': int, 'float': float, 'bool': bool, - 'str': self._interpStr, + 'str': asUnicode, 'color': self._interpColor, 'colormap': self._interpColormap, }[self.opts['type']] return fn(v) - def _interpStr(self, v): - isQString = hasattr(QtCore, 'QString') and isinstance(v, QtCore.QString) - if sys.version[0] == '2': - if isQString: - v = unicode(v) - elif not isinstance(v, basestring): - raise TypeError("Cannot set str parmeter from object %r" % v) - else: - if isQString: - v = str(v) - elif not isinstance(v, str): - raise TypeError("Cannot set str parmeter from object %r" % v) - return v - def _interpColor(self, v): return fn.mkColor(v) From 9094261c542684146a1813470012cd1fbfcabe12 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 2 Aug 2017 15:02:38 -0700 Subject: [PATCH 012/135] Fix eq() bug where calling catch_warnings raised an AttributeError, which would cause eq() to return False Add unit test coverage --- pyqtgraph/functions.py | 37 +++++++++++++----- pyqtgraph/tests/test_functions.py | 63 +++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 10 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index bdbf6d87..1aed6ace 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -200,7 +200,7 @@ def mkColor(*args): try: return Colors[c] except KeyError: - raise Exception('No color named "%s"' % c) + raise ValueError('No color named "%s"' % c) if len(c) == 3: r = int(c[0]*2, 16) g = int(c[1]*2, 16) @@ -235,18 +235,18 @@ def mkColor(*args): elif len(args[0]) == 2: return intColor(*args[0]) else: - raise Exception(err) + raise TypeError(err) elif type(args[0]) == int: return intColor(args[0]) else: - raise Exception(err) + raise TypeError(err) elif len(args) == 3: (r, g, b) = args a = 255 elif len(args) == 4: (r, g, b, a) = args else: - raise Exception(err) + raise TypeError(err) args = [r,g,b,a] args = [0 if np.isnan(a) or np.isinf(a) else a for a in args] @@ -404,22 +404,39 @@ def makeArrowPath(headLen=20, tipAngle=20, tailLen=20, tailWidth=3, baseAngle=0) def eq(a, b): - """The great missing equivalence function: Guaranteed evaluation to a single bool value.""" + """The great missing equivalence function: Guaranteed evaluation to a single bool value. + + This function has some important differences from the == operator: + + 1. Returns True if a IS b, even if a==b still evaluates to False, such as with nan values. + 2. Tests for equivalence using ==, but silently ignores some common exceptions that can occur + (AtrtibuteError, ValueError). + 3. When comparing arrays, returns False if the array shapes are not the same. + 4. When comparing arrays of the same shape, returns True only if all elements are equal (whereas + the == operator would return a boolean array). + """ if a is b: return True try: - with warnings.catch_warnings(module=np): # ignore numpy futurewarning (numpy v. 1.10) - e = a==b - except ValueError: - return False - except AttributeError: + try: + # Sometimes running catch_warnings(module=np) generates AttributeError ??? + catcher = warnings.catch_warnings(module=np) # ignore numpy futurewarning (numpy v. 1.10) + catcher.__enter__() + except Exception: + catcher = None + e = a==b + except (ValueError, AttributeError): return False except: print('failed to evaluate equivalence for:') print(" a:", str(type(a)), str(a)) print(" b:", str(type(b)), str(b)) raise + finally: + if catcher is not None: + catcher.__exit__(None, None, None) + t = type(e) if t is bool: return e diff --git a/pyqtgraph/tests/test_functions.py b/pyqtgraph/tests/test_functions.py index 7ad3bf91..eff56635 100644 --- a/pyqtgraph/tests/test_functions.py +++ b/pyqtgraph/tests/test_functions.py @@ -1,5 +1,6 @@ import pyqtgraph as pg import numpy as np +import sys from numpy.testing import assert_array_almost_equal, assert_almost_equal import pytest @@ -293,6 +294,68 @@ def test_makeARGB(): with AssertExc(): # 3d levels not allowed pg.makeARGB(np.zeros((2,2,3), dtype='float'), levels=np.zeros([3, 2, 2])) + +def test_eq(): + eq = pg.functions.eq + + zeros = [0, 0.0, np.float(0), np.int(0)] + if sys.version[0] < '3': + zeros.append(long(0)) + for i,x in enumerate(zeros): + for y in zeros[i:]: + assert eq(x, y) + assert eq(y, x) + + assert eq(np.nan, np.nan) + + # test + class NotEq(object): + def __eq__(self, x): + return False + + noteq = NotEq() + assert eq(noteq, noteq) # passes because they are the same object + assert not eq(noteq, NotEq()) + + + # Should be able to test for equivalence even if the test raises certain + # exceptions + class NoEq(object): + def __init__(self, err): + self.err = err + def __eq__(self, x): + raise self.err + + noeq1 = NoEq(AttributeError()) + noeq2 = NoEq(ValueError()) + noeq3 = NoEq(Exception()) + + assert eq(noeq1, noeq1) + assert not eq(noeq1, noeq2) + assert not eq(noeq2, noeq1) + with pytest.raises(Exception): + eq(noeq3, noeq2) + + # test array equivalence + # note that numpy has a weird behavior here--np.all() always returns True + # if one of the arrays has size=0; eq() will only return True if both arrays + # have the same shape. + a1 = np.zeros((10, 20)).astype('float') + a2 = a1 + 1 + a3 = a2.astype('int') + a4 = np.empty((0, 20)) + assert not eq(a1, a2) + assert not eq(a1, a3) + assert not eq(a1, a4) + + assert eq(a2, a3) + assert not eq(a2, a4) + + assert not eq(a3, a4) + + assert eq(a4, a4.copy()) + assert not eq(a4, a4.T) + if __name__ == '__main__': test_interpolateArray() \ No newline at end of file From 16f0e3034c1667776ca008630628fd4f85c3eb0e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 2 Aug 2017 15:03:58 -0700 Subject: [PATCH 013/135] Add tests for inpute/output type on a few parameter types --- .../tests/test_parametertypes.py | 120 +++++++++++++++++- 1 file changed, 118 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/parametertree/tests/test_parametertypes.py b/pyqtgraph/parametertree/tests/test_parametertypes.py index dc581019..a654a9ad 100644 --- a/pyqtgraph/parametertree/tests/test_parametertypes.py +++ b/pyqtgraph/parametertree/tests/test_parametertypes.py @@ -1,7 +1,19 @@ +# ~*~ coding: utf8 ~*~ +import sys +import pytest +from pyqtgraph.Qt import QtGui, QtCore import pyqtgraph.parametertree as pt import pyqtgraph as pg +from pyqtgraph.python2_3 import asUnicode +from pyqtgraph.functions import eq +import numpy as np + app = pg.mkQApp() +def _getWidget(param): + return list(param.items.keys())[0].widget + + def test_opts(): paramSpec = [ dict(name='bool', type='bool', readonly=True), @@ -12,7 +24,111 @@ def test_opts(): tree = pt.ParameterTree() tree.setParameters(param) - assert list(param.param('bool').items.keys())[0].widget.isEnabled() is False - assert list(param.param('color').items.keys())[0].widget.isEnabled() is False + assert _getWidget(param.param('bool')).isEnabled() is False + assert _getWidget(param.param('bool')).isEnabled() is False +def test_types(): + paramSpec = [ + dict(name='float', type='float'), + dict(name='int', type='int'), + dict(name='str', type='str'), + dict(name='list', type='list', values=['x','y','z']), + dict(name='dict', type='list', values={'x':1, 'y':3, 'z':7}), + dict(name='bool', type='bool'), + dict(name='color', type='color'), + ] + + param = pt.Parameter.create(name='params', type='group', children=paramSpec) + tree = pt.ParameterTree() + tree.setParameters(param) + + all_objs = { + 'int0': 0, 'int':7, 'float': -0.35, 'bigfloat': 1e129, 'npfloat': np.float(5), + 'npint': np.int(5),'npinf': np.inf, 'npnan': np.nan, 'bool': True, + 'complex': 5+3j, 'str': 'xxx', 'unicode': asUnicode('µ'), + 'list': [1,2,3], 'dict': {'1': 2}, 'color': pg.mkColor('k'), + 'brush': pg.mkBrush('k'), 'pen': pg.mkPen('k'), 'none': None + } + if hasattr(QtCore, 'QString'): + all_objs['qstring'] = QtCore.QString('xxxµ') + + # float + types = ['int0', 'int', 'float', 'bigfloat', 'npfloat', 'npint', 'npinf', 'npnan', 'bool'] + check_param_types(param.child('float'), float, float, 0.0, all_objs, types) + + # int + types = ['int0', 'int', 'float', 'bigfloat', 'npfloat', 'npint', 'bool'] + inttyps = int if sys.version[0] >= '3' else (int, long) + check_param_types(param.child('int'), inttyps, int, 0, all_objs, types) + + # str (should be able to make a string out of any type) + types = all_objs.keys() + strtyp = str if sys.version[0] >= '3' else unicode + check_param_types(param.child('str'), strtyp, asUnicode, '', all_objs, types) + + # bool (should be able to make a boolean out of any type?) + types = all_objs.keys() + check_param_types(param.child('bool'), bool, bool, False, all_objs, types) + + # color + types = ['color', 'int0', 'int', 'float', 'npfloat', 'npint', 'list'] + init = QtGui.QColor(128, 128, 128, 255) + check_param_types(param.child('color'), QtGui.QColor, pg.mkColor, init, all_objs, types) + + +def check_param_types(param, types, map_func, init, objs, keys): + """Check that parameter setValue() accepts or rejects the correct types and + that value() returns the correct type. + + Parameters + ---------- + param : Parameter instance + types : type or tuple of types + The allowed types for this parameter to return from value(). + map_func : function + Converts an input value to the expected output value. + init : object + The expected initial value of the parameter + objs : dict + Contains a variety of objects that will be tested as arguments to + param.setValue(). + keys : list + The list of keys indicating the valid objects in *objs*. When + param.setValue() is teasted with each value from *objs*, we expect + an exception to be raised if the associated key is not in *keys*. + """ + val = param.value() + if not isinstance(types, tuple): + types = (types,) + assert val == init and type(val) in types + + # test valid input types + good_inputs = [objs[k] for k in keys if k in objs] + good_outputs = map(map_func, good_inputs) + for x,y in zip(good_inputs, good_outputs): + param.setValue(x) + val = param.value() + if not (eq(val, y) and type(val) in types): + raise Exception("Setting parameter %s with value %r should have resulted in %r (types: %r), " + "but resulted in %r (type: %r) instead." % (param, x, y, types, val, type(val))) + + # test invalid input types + for k,v in objs.items(): + if k in keys: + continue + try: + param.setValue(v) + except (TypeError, ValueError, OverflowError): + continue + except Exception as exc: + raise Exception("Setting %s parameter value to %r raised %r." % (param, v, exc)) + + raise Exception("Setting %s parameter value to %r should have raised an exception." % (param, v)) + + + + + + + \ No newline at end of file From 8398e578b929e90932ba2be9cfe30123a6021320 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 3 Sep 2017 16:48:08 -0700 Subject: [PATCH 014/135] Add a collapsible QGroubBox widget --- pyqtgraph/__init__.py | 1 + pyqtgraph/widgets/GroupBox.py | 91 +++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 pyqtgraph/widgets/GroupBox.py diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index bc5081f7..24653207 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -262,6 +262,7 @@ from .widgets.GraphicsView import * from .widgets.LayoutWidget import * from .widgets.TableWidget import * from .widgets.ProgressDialog import * +from .widgets.GroupBox import GroupBox from .imageview import * from .WidgetGroup import * diff --git a/pyqtgraph/widgets/GroupBox.py b/pyqtgraph/widgets/GroupBox.py new file mode 100644 index 00000000..14a8dab5 --- /dev/null +++ b/pyqtgraph/widgets/GroupBox.py @@ -0,0 +1,91 @@ +from ..Qt import QtGui, QtCore +from .PathButton import PathButton + +class GroupBox(QtGui.QGroupBox): + """Subclass of QGroupBox that implements collapse handle. + """ + sigCollapseChanged = QtCore.Signal(object) + + def __init__(self, *args): + QtGui.QGroupBox.__init__(self, *args) + + self._collapsed = False + # We modify the size policy when the group box is collapsed, so + # keep track of the last requested policy: + self._lastSizePlocy = self.sizePolicy() + + self.closePath = QtGui.QPainterPath() + self.closePath.moveTo(0, -1) + self.closePath.lineTo(0, 1) + self.closePath.lineTo(1, 0) + self.closePath.lineTo(0, -1) + + self.openPath = QtGui.QPainterPath() + self.openPath.moveTo(-1, 0) + self.openPath.lineTo(1, 0) + self.openPath.lineTo(0, 1) + self.openPath.lineTo(-1, 0) + + self.collapseBtn = PathButton(path=self.openPath, size=(12, 12), margin=0) + self.collapseBtn.setStyleSheet(""" + border: none; + """) + self.collapseBtn.setPen('k') + self.collapseBtn.setBrush('w') + self.collapseBtn.setParent(self) + self.collapseBtn.move(3, 3) + self.collapseBtn.setFlat(True) + + self.collapseBtn.clicked.connect(self.toggleCollapsed) + + if len(args) > 0 and isinstance(args[0], basestring): + self.setTitle(args[0]) + + def toggleCollapsed(self): + self.setCollapsed(not self._collapsed) + + def collapsed(self): + return self._collapsed + + def setCollapsed(self, c): + if c == self._collapsed: + return + + if c is True: + self.collapseBtn.setPath(self.closePath) + self.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred, closing=True) + elif c is False: + self.collapseBtn.setPath(self.openPath) + self.setSizePolicy(self._lastSizePolicy) + else: + raise TypeError("Invalid argument %r; must be bool." % c) + + for ch in self.children(): + if isinstance(ch, QtGui.QWidget) and ch is not self.collapseBtn: + ch.setVisible(not c) + + self._collapsed = c + self.sigCollapseChanged.emit(c) + + def setSizePolicy(self, *args, **kwds): + QtGui.QGroupBox.setSizePolicy(self, *args) + if kwds.pop('closing', False) is True: + self._lastSizePolicy = self.sizePolicy() + + def setHorizontalPolicy(self, *args): + QtGui.QGroupBox.setHorizontalPolicy(self, *args) + self._lastSizePolicy = self.sizePolicy() + + def setVerticalPolicy(self, *args): + QtGui.QGroupBox.setVerticalPolicy(self, *args) + self._lastSizePolicy = self.sizePolicy() + + def setTitle(self, title): + # Leave room for button + QtGui.QGroupBox.setTitle(self, " " + title) + + def widgetGroupInterface(self): + return (self.sigCollapseChanged, + GroupBox.collapsed, + GroupBox.setCollapsed, + True) From 65fa58c2b10a9ca9dfd8e711f681c23e4de12d87 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 3 Sep 2017 16:51:11 -0700 Subject: [PATCH 015/135] Add targetitem class --- pyqtgraph/graphicsItems/TargetItem.py | 125 ++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 pyqtgraph/graphicsItems/TargetItem.py diff --git a/pyqtgraph/graphicsItems/TargetItem.py b/pyqtgraph/graphicsItems/TargetItem.py new file mode 100644 index 00000000..114c9e6e --- /dev/null +++ b/pyqtgraph/graphicsItems/TargetItem.py @@ -0,0 +1,125 @@ +from ..Qt import QtGui, QtCore +import numpy as np +from ..Point import Point +from .. import functions as fn +from .GraphicsObject import GraphicsObject +from .TextItem import TextItem + + +class TargetItem(GraphicsObject): + """Draws a draggable target symbol (circle plus crosshair). + + The size of TargetItem will remain fixed on screen even as the view is zoomed. + Includes an optional text label. + """ + sigDragged = QtCore.Signal(object) + + def __init__(self, movable=True, radii=(5, 10, 10), pen=(255, 255, 0), brush=(0, 0, 255, 100)): + GraphicsObject.__init__(self) + self._bounds = None + self._radii = radii + self._picture = None + self.movable = movable + self.moving = False + self.label = None + self.labelAngle = 0 + self.pen = fn.mkPen(pen) + self.brush = fn.mkBrush(brush) + + def setLabel(self, label): + if label is None: + if self.label is not None: + self.label.scene().removeItem(self.label) + self.label = None + else: + if self.label is None: + self.label = TextItem() + self.label.setParentItem(self) + self.label.setText(label) + self._updateLabel() + + def setLabelAngle(self, angle): + self.labelAngle = angle + self._updateLabel() + + def boundingRect(self): + if self._picture is None: + self._drawPicture() + return self._bounds + + def dataBounds(self, axis, frac=1.0, orthoRange=None): + return [0, 0] + + def viewTransformChanged(self): + self._picture = None + self.prepareGeometryChange() + self._updateLabel() + + def _updateLabel(self): + if self.label is None: + return + + # find an optimal location for text at the given angle + angle = self.labelAngle * np.pi / 180. + lbr = self.label.boundingRect() + center = lbr.center() + a = abs(np.sin(angle) * lbr.height()*0.5) + b = abs(np.cos(angle) * lbr.width()*0.5) + r = max(self._radii) + 2 + max(a, b) + pos = self.mapFromScene(self.mapToScene(QtCore.QPointF(0, 0)) + r * QtCore.QPointF(np.cos(angle), -np.sin(angle)) - center) + self.label.setPos(pos) + + def paint(self, p, *args): + if self._picture is None: + self._drawPicture() + self._picture.play(p) + + def _drawPicture(self): + self._picture = QtGui.QPicture() + p = QtGui.QPainter(self._picture) + p.setRenderHint(p.Antialiasing) + + # Note: could do this with self.pixelLength, but this is faster. + o = self.mapToScene(QtCore.QPointF(0, 0)) + px = abs(1.0 / (self.mapToScene(QtCore.QPointF(1, 0)) - o).x()) + py = abs(1.0 / (self.mapToScene(QtCore.QPointF(0, 1)) - o).y()) + + r, w, h = self._radii + w = w * px + h = h * py + rx = r * px + ry = r * py + rect = QtCore.QRectF(-rx, -ry, rx*2, ry*2) + p.setPen(self.pen) + p.setBrush(self.brush) + p.drawEllipse(rect) + p.drawLine(Point(-w, 0), Point(w, 0)) + p.drawLine(Point(0, -h), Point(0, h)) + p.end() + + bx = max(w, rx) + by = max(h, ry) + self._bounds = QtCore.QRectF(-bx, -by, bx*2, by*2) + + def mouseDragEvent(self, ev): + if not self.movable: + return + if ev.button() == QtCore.Qt.LeftButton: + if ev.isStart(): + self.moving = True + self.cursorOffset = self.pos() - self.mapToParent(ev.buttonDownPos()) + self.startPosition = self.pos() + ev.accept() + + if not self.moving: + return + + self.setPos(self.cursorOffset + self.mapToParent(ev.pos())) + if ev.isFinish(): + self.moving = False + self.sigDragged.emit(self) + + def hoverEvent(self, ev): + if self.movable: + ev.acceptDrags(QtCore.Qt.LeftButton) + From b6f9516678e23ad26357a10643dd2f98cf2bafde Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 3 Sep 2017 17:00:33 -0700 Subject: [PATCH 016/135] Make behavior configurable when a reloaded dock is missing. + other bugfixes --- pyqtgraph/dockarea/Container.py | 32 ++-- pyqtgraph/dockarea/Dock.py | 30 ++-- pyqtgraph/dockarea/DockArea.py | 98 ++++++++---- pyqtgraph/dockarea/tests/test_dockarea.py | 184 ++++++++++++++++++++++ 4 files changed, 288 insertions(+), 56 deletions(-) create mode 100644 pyqtgraph/dockarea/tests/test_dockarea.py diff --git a/pyqtgraph/dockarea/Container.py b/pyqtgraph/dockarea/Container.py index c3225edf..bc0b3648 100644 --- a/pyqtgraph/dockarea/Container.py +++ b/pyqtgraph/dockarea/Container.py @@ -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 diff --git a/pyqtgraph/dockarea/Dock.py b/pyqtgraph/dockarea/Dock.py index 4493d075..1d946062 100644 --- a/pyqtgraph/dockarea/Dock.py +++ b/pyqtgraph/dockarea/Dock.py @@ -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.""" diff --git a/pyqtgraph/dockarea/DockArea.py b/pyqtgraph/dockarea/DockArea.py index ffe75b61..560495ce 100644 --- a/pyqtgraph/dockarea/DockArea.py +++ b/pyqtgraph/dockarea/DockArea.py @@ -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) diff --git a/pyqtgraph/dockarea/tests/test_dockarea.py b/pyqtgraph/dockarea/tests/test_dockarea.py new file mode 100644 index 00000000..e6d14790 --- /dev/null +++ b/pyqtgraph/dockarea/tests/test_dockarea.py @@ -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() From e8128fa5e284fe65f0c19e403284c147d46b8206 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 3 Sep 2017 20:14:50 -0700 Subject: [PATCH 017/135] Make dockarea.restoreState behavior for extrra docks be configurable --- pyqtgraph/dockarea/DockArea.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/dockarea/DockArea.py b/pyqtgraph/dockarea/DockArea.py index 560495ce..a55d6bb0 100644 --- a/pyqtgraph/dockarea/DockArea.py +++ b/pyqtgraph/dockarea/DockArea.py @@ -208,7 +208,7 @@ class DockArea(Container, QtGui.QWidget, DockDrop): childs.append(self.childState(obj.widget(i))) return (obj.type(), childs, obj.saveState()) - def restoreState(self, state, missing='error'): + def restoreState(self, state, missing='error', extra='bottom'): """ Restore Dock configuration as generated by saveState. @@ -218,6 +218,10 @@ class DockArea(Container, QtGui.QWidget, DockDrop): 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'. + + Extra docks that are in the dockarea but that are not mentioned in + *state* will be added to the bottom of the dockarea, unless otherwise + specified by the *extra* argument. """ ## 1) make dict of all docks and list of existing containers @@ -238,9 +242,11 @@ class DockArea(Container, QtGui.QWidget, DockDrop): ## 4) Add any remaining docks to a float for d in docks.values(): - a = self.addTempArea() - a.addDock(d, 'below') - # self.moveDock(d, 'below', None) + if extra == 'float': + a = self.addTempArea() + a.addDock(d, 'below') + else: + self.moveDock(d, extra, None) #print "\nKill old containers:" ## 5) kill old containers From 715c3a008566650aa2a30b0712b7eff8852ffae9 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 3 Sep 2017 20:29:19 -0700 Subject: [PATCH 018/135] Minor changes to Transform3D - allow more types to be passed through map() and add some sanity checks --- pyqtgraph/Transform3D.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/Transform3D.py b/pyqtgraph/Transform3D.py index 43b12de3..56283351 100644 --- a/pyqtgraph/Transform3D.py +++ b/pyqtgraph/Transform3D.py @@ -1,13 +1,19 @@ # -*- coding: utf-8 -*- from .Qt import QtCore, QtGui from . import functions as fn +from .Vector import Vector import numpy as np + class Transform3D(QtGui.QMatrix4x4): """ Extension of QMatrix4x4 with some helpful methods added. """ def __init__(self, *args): + if len(args) == 1 and isinstance(args[0], (list, tuple, np.ndarray)): + args = [x for y in args[0] for x in y] + if len(args) != 16: + raise TypeError("Single argument to Transform3D must have 16 elements.") QtGui.QMatrix4x4.__init__(self, *args) def matrix(self, nd=3): @@ -25,8 +31,15 @@ class Transform3D(QtGui.QMatrix4x4): """ Extends QMatrix4x4.map() to allow mapping (3, ...) arrays of coordinates """ - if isinstance(obj, np.ndarray) and obj.ndim >= 2 and obj.shape[0] in (2,3): - return fn.transformCoordinates(self, obj) + if isinstance(obj, np.ndarray) and obj.shape[0] in (2,3): + if obj.ndim >= 2: + return fn.transformCoordinates(self, obj) + elif obj.ndim == 1: + v = QtGui.QMatrix4x4.map(self, Vector(obj)) + return np.array([v.x(), v.y(), v.z()])[:obj.shape[0]] + elif isinstance(obj, (list, tuple)): + v = QtGui.QMatrix4x4.map(self, Vector(obj)) + return type(obj)([v.x(), v.y(), v.z()])[:len(obj)] else: return QtGui.QMatrix4x4.map(self, obj) From 73d857750a0e310d17011e5ee116221241d1f498 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 3 Sep 2017 22:04:24 -0700 Subject: [PATCH 019/135] Add check for EINTR during example testing; this should help avoid sporadic test failures on travis --- examples/utils.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/examples/utils.py b/examples/utils.py index cbdf69c6..88adc9c9 100644 --- a/examples/utils.py +++ b/examples/utils.py @@ -3,6 +3,7 @@ import subprocess import time import os import sys +import errno from pyqtgraph.pgcollections import OrderedDict from pyqtgraph.python2_3 import basestring @@ -143,7 +144,14 @@ except: output = '' fail = False while True: - c = process.stdout.read(1).decode() + try: + c = process.stdout.read(1).decode() + except IOError as err: + if err.errno == errno.EINTR: + # Interrupted system call; just try again. + c = '' + else: + raise output += c #sys.stdout.write(c) #sys.stdout.flush() From 3dbbc7e53142cd9360c6651ef15acf142b35770f Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 3 Sep 2017 23:05:46 -0700 Subject: [PATCH 020/135] Fix unit test following previous commit --- pyqtgraph/dockarea/tests/test_dockarea.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/dockarea/tests/test_dockarea.py b/pyqtgraph/dockarea/tests/test_dockarea.py index e6d14790..a26646bc 100644 --- a/pyqtgraph/dockarea/tests/test_dockarea.py +++ b/pyqtgraph/dockarea/tests/test_dockarea.py @@ -156,10 +156,15 @@ def test_dockarea(): ], {'sizes': [1159, 116]})} a4.restoreState(state1, missing='create') - a4.restoreState(state2, missing='ignore') + # dock3 not mentioned in restored state; stays in dockarea by default + c, d = a4.findAll() + assert d['dock3'].area is a4 + + a4.restoreState(state2, missing='ignore', extra='float') a4.printState() c, d = a4.findAll() + # dock3 not mentioned in restored state; goes to float due to `extra` argument 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() From 30997d999d5098cf21d7a149e75e97ae4f3e98de Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 3 Sep 2017 23:18:17 -0700 Subject: [PATCH 021/135] Fix unit test for python 2.6 --- pyqtgraph/dockarea/tests/test_dockarea.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/dockarea/tests/test_dockarea.py b/pyqtgraph/dockarea/tests/test_dockarea.py index a26646bc..9575c298 100644 --- a/pyqtgraph/dockarea/tests/test_dockarea.py +++ b/pyqtgraph/dockarea/tests/test_dockarea.py @@ -2,7 +2,7 @@ import pytest import pyqtgraph as pg -from collections import OrderedDict +from pyqtgraph.ordereddict import OrderedDict pg.mkQApp() import pyqtgraph.dockarea as da From 2a70fd99321855d4ac67886a2f55da992b3a0e68 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 13 Sep 2017 09:05:54 -0700 Subject: [PATCH 022/135] Fix some issues with closing subprocesses --- pyqtgraph/multiprocess/__init__.py | 2 +- pyqtgraph/multiprocess/processes.py | 3 ++- pyqtgraph/multiprocess/remoteproxy.py | 6 +++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/multiprocess/__init__.py b/pyqtgraph/multiprocess/__init__.py index 843b42a3..32a250cb 100644 --- a/pyqtgraph/multiprocess/__init__.py +++ b/pyqtgraph/multiprocess/__init__.py @@ -21,4 +21,4 @@ TODO: from .processes import * from .parallelizer import Parallelize, CanceledError -from .remoteproxy import proxy \ No newline at end of file +from .remoteproxy import proxy, ClosedError, NoResultError diff --git a/pyqtgraph/multiprocess/processes.py b/pyqtgraph/multiprocess/processes.py index 02f259e5..11348c23 100644 --- a/pyqtgraph/multiprocess/processes.py +++ b/pyqtgraph/multiprocess/processes.py @@ -182,7 +182,8 @@ def startEventLoop(name, port, authkey, ppid, debug=False): HANDLER.processRequests() # exception raised when the loop should exit time.sleep(0.01) except ClosedError: - break + HANDLER.debugMsg('Exiting server loop.') + sys.exit(0) class ForkedProcess(RemoteEventHandler): diff --git a/pyqtgraph/multiprocess/remoteproxy.py b/pyqtgraph/multiprocess/remoteproxy.py index 208e17f4..805392e2 100644 --- a/pyqtgraph/multiprocess/remoteproxy.py +++ b/pyqtgraph/multiprocess/remoteproxy.py @@ -419,7 +419,7 @@ class RemoteEventHandler(object): if opts is None: opts = {} - assert callSync in ['off', 'sync', 'async'], 'callSync must be one of "off", "sync", or "async"' + assert callSync in ['off', 'sync', 'async'], 'callSync must be one of "off", "sync", or "async" (got %r)' % callSync if reqId is None: if callSync != 'off': ## requested return value; use the next available request ID reqId = self.nextRequestId @@ -572,6 +572,10 @@ class RemoteEventHandler(object): self.proxies[ref] = proxy._proxyId def deleteProxy(self, ref): + if self.send is None: + # this can happen during shutdown + return + with self.proxyLock: proxyId = self.proxies.pop(ref) From 16781636bfdabb112fd46a6e579718b035dae7fe Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 13 Sep 2017 09:09:05 -0700 Subject: [PATCH 023/135] API: calling remote methods in 'sync' mode no longer returns future on timeout When calling a function with callSync='sync', the assumption is that we either block until the result arrives or raise an exception if no result arrives. Previously, a timeout woud cause the Future object to be returned instead. --- pyqtgraph/multiprocess/remoteproxy.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pyqtgraph/multiprocess/remoteproxy.py b/pyqtgraph/multiprocess/remoteproxy.py index 805392e2..bc02da83 100644 --- a/pyqtgraph/multiprocess/remoteproxy.py +++ b/pyqtgraph/multiprocess/remoteproxy.py @@ -466,10 +466,7 @@ class RemoteEventHandler(object): return req if callSync == 'sync': - try: - return req.result() - except NoResultError: - return req + return req.result() def close(self, callSync='off', noCleanup=False, **kwds): try: From 5fb5858802dc7181c0ec40be3749f449f0efbb03 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 13 Sep 2017 09:13:03 -0700 Subject: [PATCH 024/135] Allow better control over sys.path in subprocesses Either add path to pyqtgraph, or copy entire path (anything else still requires manual effort) --- pyqtgraph/multiprocess/bootstrap.py | 19 ++++++++++++++----- pyqtgraph/multiprocess/processes.py | 14 +++++++++++--- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/pyqtgraph/multiprocess/bootstrap.py b/pyqtgraph/multiprocess/bootstrap.py index bb71a703..f9cb0b0e 100644 --- a/pyqtgraph/multiprocess/bootstrap.py +++ b/pyqtgraph/multiprocess/bootstrap.py @@ -13,16 +13,25 @@ if __name__ == '__main__': #print "key:", ' '.join([str(ord(x)) for x in authkey]) path = opts.pop('path', None) if path is not None: - ## rewrite sys.path without assigning a new object--no idea who already has a reference to the existing list. - while len(sys.path) > 0: - sys.path.pop() - sys.path.extend(path) + if isinstance(path, str): + # if string, just insert this into the path + sys.path.insert(0, path) + else: + # if list, then replace the entire sys.path + ## modify sys.path in place--no idea who already has a reference to the existing list. + while len(sys.path) > 0: + sys.path.pop() + sys.path.extend(path) if opts.pop('pyside', False): import PySide targetStr = opts.pop('targetStr') - target = pickle.loads(targetStr) ## unpickling the target should import everything we need + try: + target = pickle.loads(targetStr) ## unpickling the target should import everything we need + except: + print("Current sys.path:", sys.path) + raise target(**opts) ## Send all other options to the target function sys.exit(0) diff --git a/pyqtgraph/multiprocess/processes.py b/pyqtgraph/multiprocess/processes.py index 11348c23..c0cd829a 100644 --- a/pyqtgraph/multiprocess/processes.py +++ b/pyqtgraph/multiprocess/processes.py @@ -1,4 +1,4 @@ -import subprocess, atexit, os, sys, time, random, socket, signal +import subprocess, atexit, os, sys, time, random, socket, signal, inspect import multiprocessing.connection try: import cPickle as pickle @@ -50,7 +50,9 @@ class Process(RemoteEventHandler): process to process requests from the parent process until it is asked to quit. If you wish to specify a different target, it must be picklable (bound methods are not). - copySysPath If True, copy the contents of sys.path to the remote process + copySysPath If True, copy the contents of sys.path to the remote process. + If False, then only the path required to import pyqtgraph is + added. debug If True, print detailed information about communication with the child process. wrapStdout If True (default on windows) then stdout and stderr from the @@ -82,7 +84,13 @@ class Process(RemoteEventHandler): port = l.address[1] ## start remote process, instruct it to run target function - sysPath = sys.path if copySysPath else None + if copySysPath: + sysPath = sys.path + else: + # what path do we need to make target importable? + mod = inspect.getmodule(target) + modroot = sys.modules[mod.__name__.split('.')[0]] + sysPath = os.path.abspath(os.path.join(os.path.dirname(modroot.__file__), '..')) bootstrap = os.path.abspath(os.path.join(os.path.dirname(__file__), 'bootstrap.py')) self.debugMsg('Starting child process (%s %s)' % (executable, bootstrap)) From 182e9397854810f2f50022e07292dba2b36d511c Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 13 Sep 2017 09:13:31 -0700 Subject: [PATCH 025/135] Fix color output handling --- pyqtgraph/multiprocess/processes.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/multiprocess/processes.py b/pyqtgraph/multiprocess/processes.py index c0cd829a..7560ff70 100644 --- a/pyqtgraph/multiprocess/processes.py +++ b/pyqtgraph/multiprocess/processes.py @@ -471,21 +471,20 @@ class FileForwarder(threading.Thread): self.start() def run(self): - if self.output == 'stdout': + if self.output == 'stdout' and self.color is not False: while True: line = self.input.readline() with self.lock: cprint.cout(self.color, line, -1) - elif self.output == 'stderr': + elif self.output == 'stderr' and self.color is not False: while True: line = self.input.readline() with self.lock: cprint.cerr(self.color, line, -1) else: + if isinstance(self.output, str): + self.output = getattr(sys, self.output) while True: line = self.input.readline() with self.lock: self.output.write(line) - - - From 05176654731684fa84ceb43413bb85fda1a5b3fd Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 13 Sep 2017 20:50:31 -0700 Subject: [PATCH 026/135] Allow console to display any frame stack (even without an exception) --- pyqtgraph/console/Console.py | 76 +++++++++++++++++++++------- pyqtgraph/console/template.ui | 9 ++-- pyqtgraph/console/template_pyqt.py | 9 ++-- pyqtgraph/console/template_pyside.py | 4 +- 4 files changed, 71 insertions(+), 27 deletions(-) diff --git a/pyqtgraph/console/Console.py b/pyqtgraph/console/Console.py index ed4b7f08..23ae93d5 100644 --- a/pyqtgraph/console/Console.py +++ b/pyqtgraph/console/Console.py @@ -53,6 +53,7 @@ class ConsoleWidget(QtGui.QWidget): self.editor = editor self.multiline = None self.inCmd = False + self.frames = [] # stack frames to access when an item in the stack list is selected self.ui = template.Ui_Form() self.ui.setupUi(self) @@ -133,14 +134,14 @@ class ConsoleWidget(QtGui.QWidget): def globals(self): frame = self.currentFrame() if frame is not None and self.ui.runSelectedFrameCheck.isChecked(): - return self.currentFrame().tb_frame.f_globals + return self.currentFrame().f_globals else: return self.localNamespace def locals(self): frame = self.currentFrame() if frame is not None and self.ui.runSelectedFrameCheck.isChecked(): - return self.currentFrame().tb_frame.f_locals + return self.currentFrame().f_locals else: return self.localNamespace @@ -149,10 +150,7 @@ class ConsoleWidget(QtGui.QWidget): if self.currentTraceback is None: return None index = self.ui.exceptionStackList.currentRow() - tb = self.currentTraceback - for i in range(index): - tb = tb.tb_next - return tb + return self.frames[index] def execSingle(self, cmd): try: @@ -171,7 +169,6 @@ class ConsoleWidget(QtGui.QWidget): except: self.displayException() - def execMulti(self, nextLine): #self.stdout.write(nextLine+"\n") if nextLine.strip() != '': @@ -202,6 +199,10 @@ class ConsoleWidget(QtGui.QWidget): self.multiline = None def write(self, strn, html=False): + isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread() + if not isGuiThread: + self.stdout.write(strn) + return self.output.moveCursor(QtGui.QTextCursor.End) if html: self.output.textCursor().insertHtml(strn) @@ -293,14 +294,6 @@ class ConsoleWidget(QtGui.QWidget): fileName = tb.tb_frame.f_code.co_filename subprocess.Popen(self.editor.format(fileName=fileName, lineNum=lineNum), shell=True) - - #def allExceptionsHandler(self, *args): - #self.exceptionHandler(*args) - - #def nextExceptionHandler(self, *args): - #self.ui.catchNextExceptionBtn.setChecked(False) - #self.exceptionHandler(*args) - def updateSysTrace(self): ## Install or uninstall sys.settrace handler @@ -319,7 +312,7 @@ class ConsoleWidget(QtGui.QWidget): else: sys.settrace(self.systrace) - def exceptionHandler(self, excType, exc, tb): + def exceptionHandler(self, excType, exc, tb, systrace=False): if self.ui.catchNextExceptionBtn.isChecked(): self.ui.catchNextExceptionBtn.setChecked(False) elif not self.ui.catchAllExceptionsBtn.isChecked(): @@ -330,13 +323,60 @@ class ConsoleWidget(QtGui.QWidget): excMessage = ''.join(traceback.format_exception_only(excType, exc)) self.ui.exceptionInfoLabel.setText(excMessage) + + if systrace: + # exceptions caught using systrace don't need the usual + # call stack + traceback handling + self.setStack(sys._getframe().f_back.f_back) + else: + self.setStack(frame=sys._getframe().f_back, tb=tb) + + def setStack(self, frame=None, tb=None): + """Display a call stack and exception traceback. + + This allows the user to probe the contents of any frame in the given stack. + + *frame* may either be a Frame instance or None, in which case the current + frame is retrieved from ``sys._getframe()``. + + If *tb* is provided then the frames in the traceback will be appended to + the end of the stack list. If *tb* is None, then sys.exc_info() will + be checked instead. + """ + if frame is None: + frame = sys._getframe().f_back + + if tb is None: + tb = sys.exc_info()[2] + self.ui.exceptionStackList.clear() + self.frames = [] + + # Build stack up to this point + for index, line in enumerate(traceback.extract_stack(frame)): + self.ui.exceptionStackList.addItem('File "%s", line %s, in %s()\n %s' % line) + while frame is not None: + self.frames.insert(0, frame) + frame = frame.f_back + + if tb is None: + return + + self.ui.exceptionStackList.addItem('-- exception caught here: --') + item = self.ui.exceptionStackList.item(self.ui.exceptionStackList.count()-1) + item.setBackground(QtGui.QBrush(QtGui.QColor(200, 200, 200))) + self.frames.append(None) + + # And finish the rest of the stack up to the exception for index, line in enumerate(traceback.extract_tb(tb)): self.ui.exceptionStackList.addItem('File "%s", line %s, in %s()\n %s' % line) - + while tb is not None: + self.frames.append(tb.tb_frame) + tb = tb.tb_next + def systrace(self, frame, event, arg): if event == 'exception' and self.checkException(*arg): - self.exceptionHandler(*arg) + self.exceptionHandler(*arg, systrace=True) return self.systrace def checkException(self, excType, exc, tb): diff --git a/pyqtgraph/console/template.ui b/pyqtgraph/console/template.ui index 1a672c5e..1dd752d9 100644 --- a/pyqtgraph/console/template.ui +++ b/pyqtgraph/console/template.ui @@ -86,7 +86,10 @@ 0 - + + 2 + + 0 @@ -95,7 +98,7 @@ false - Clear Exception + Clear Stack @@ -149,7 +152,7 @@ - Exception Info + Stack Trace diff --git a/pyqtgraph/console/template_pyqt.py b/pyqtgraph/console/template_pyqt.py index 354fb1d6..e5fc4619 100644 --- a/pyqtgraph/console/template_pyqt.py +++ b/pyqtgraph/console/template_pyqt.py @@ -2,7 +2,7 @@ # Form implementation generated from reading ui file 'template.ui' # -# Created: Fri May 02 18:55:28 2014 +# Created: Wed Apr 08 16:28:53 2015 # by: PyQt4 UI code generator 4.10.4 # # WARNING! All changes made in this file will be lost! @@ -68,8 +68,9 @@ class Ui_Form(object): self.exceptionGroup = QtGui.QGroupBox(self.splitter) self.exceptionGroup.setObjectName(_fromUtf8("exceptionGroup")) self.gridLayout_2 = QtGui.QGridLayout(self.exceptionGroup) - self.gridLayout_2.setSpacing(0) self.gridLayout_2.setContentsMargins(-1, 0, -1, 0) + self.gridLayout_2.setHorizontalSpacing(2) + self.gridLayout_2.setVerticalSpacing(0) self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2")) self.clearExceptionBtn = QtGui.QPushButton(self.exceptionGroup) self.clearExceptionBtn.setEnabled(False) @@ -116,12 +117,12 @@ class Ui_Form(object): self.historyBtn.setText(_translate("Form", "History..", None)) self.exceptionBtn.setText(_translate("Form", "Exceptions..", None)) self.exceptionGroup.setTitle(_translate("Form", "Exception Handling", None)) - self.clearExceptionBtn.setText(_translate("Form", "Clear Exception", None)) + self.clearExceptionBtn.setText(_translate("Form", "Clear Stack", None)) self.catchAllExceptionsBtn.setText(_translate("Form", "Show All Exceptions", None)) self.catchNextExceptionBtn.setText(_translate("Form", "Show Next Exception", None)) self.onlyUncaughtCheck.setText(_translate("Form", "Only Uncaught Exceptions", None)) self.runSelectedFrameCheck.setText(_translate("Form", "Run commands in selected stack frame", None)) - self.exceptionInfoLabel.setText(_translate("Form", "Exception Info", None)) + self.exceptionInfoLabel.setText(_translate("Form", "Stack Trace", None)) self.label.setText(_translate("Form", "Filter (regex):", None)) from .CmdInput import CmdInput diff --git a/pyqtgraph/console/template_pyside.py b/pyqtgraph/console/template_pyside.py index 2db8ed95..36065afd 100644 --- a/pyqtgraph/console/template_pyside.py +++ b/pyqtgraph/console/template_pyside.py @@ -100,7 +100,7 @@ class Ui_Form(object): self.catchNextExceptionBtn.setText(QtGui.QApplication.translate("Form", "Show Next Exception", None, QtGui.QApplication.UnicodeUTF8)) self.onlyUncaughtCheck.setText(QtGui.QApplication.translate("Form", "Only Uncaught Exceptions", None, QtGui.QApplication.UnicodeUTF8)) self.runSelectedFrameCheck.setText(QtGui.QApplication.translate("Form", "Run commands in selected stack frame", None, QtGui.QApplication.UnicodeUTF8)) - self.exceptionInfoLabel.setText(QtGui.QApplication.translate("Form", "Exception Info", None, QtGui.QApplication.UnicodeUTF8)) - self.clearExceptionBtn.setText(QtGui.QApplication.translate("Form", "Clear Exception", None, QtGui.QApplication.UnicodeUTF8)) + self.exceptionInfoLabel.setText(QtGui.QApplication.translate("Form", "Stack Trace", None, QtGui.QApplication.UnicodeUTF8)) + self.clearExceptionBtn.setText(QtGui.QApplication.translate("Form", "Clear Stack", None, QtGui.QApplication.UnicodeUTF8)) from .CmdInput import CmdInput From 39d4c82d678b12138ac60144468720d1de06f0dd Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 13 Sep 2017 20:58:10 -0700 Subject: [PATCH 027/135] Fix stack clearing button --- pyqtgraph/console/Console.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/console/Console.py b/pyqtgraph/console/Console.py index 23ae93d5..72164f33 100644 --- a/pyqtgraph/console/Console.py +++ b/pyqtgraph/console/Console.py @@ -318,7 +318,6 @@ class ConsoleWidget(QtGui.QWidget): elif not self.ui.catchAllExceptionsBtn.isChecked(): return - self.ui.clearExceptionBtn.setEnabled(True) self.currentTraceback = tb excMessage = ''.join(traceback.format_exception_only(excType, exc)) @@ -343,6 +342,8 @@ class ConsoleWidget(QtGui.QWidget): the end of the stack list. If *tb* is None, then sys.exc_info() will be checked instead. """ + self.ui.clearExceptionBtn.setEnabled(True) + if frame is None: frame = sys._getframe().f_back From e88e3a4232e4b07ef50e7efa9faae0236f4f8930 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 13 Sep 2017 21:01:38 -0700 Subject: [PATCH 028/135] code cleanup --- pyqtgraph/canvas/Canvas.py | 135 +-------------------------------- pyqtgraph/canvas/CanvasItem.py | 56 +------------- 2 files changed, 3 insertions(+), 188 deletions(-) diff --git a/pyqtgraph/canvas/Canvas.py b/pyqtgraph/canvas/Canvas.py index 5b5ce2f7..a9f1d918 100644 --- a/pyqtgraph/canvas/Canvas.py +++ b/pyqtgraph/canvas/Canvas.py @@ -32,7 +32,6 @@ class Canvas(QtGui.QWidget): QtGui.QWidget.__init__(self, parent) self.ui = Ui_Form() self.ui.setupUi(self) - #self.view = self.ui.view self.view = ViewBox() self.ui.view.setCentralItem(self.view) self.itemList = self.ui.itemList @@ -49,9 +48,7 @@ class Canvas(QtGui.QWidget): self.redirect = None ## which canvas to redirect items to self.items = [] - #self.view.enableMouse() self.view.setAspectLocked(True) - #self.view.invertY() grid = GridItem() self.grid = CanvasItem(grid, name='Grid', movable=False) @@ -69,8 +66,6 @@ class Canvas(QtGui.QWidget): self.ui.itemList.sigItemMoved.connect(self.treeItemMoved) self.ui.itemList.itemSelectionChanged.connect(self.treeItemSelected) self.ui.autoRangeBtn.clicked.connect(self.autoRange) - #self.ui.storeSvgBtn.clicked.connect(self.storeSvg) - #self.ui.storePngBtn.clicked.connect(self.storePng) self.ui.redirectCheck.toggled.connect(self.updateRedirect) self.ui.redirectCombo.currentIndexChanged.connect(self.updateRedirect) self.multiSelectBox.sigRegionChanged.connect(self.multiSelectBoxChanged) @@ -88,21 +83,11 @@ class Canvas(QtGui.QWidget): self.ui.redirectCombo.setHostName(self.registeredName) self.menu = QtGui.QMenu() - #self.menu.setTitle("Image") remAct = QtGui.QAction("Remove item", self.menu) remAct.triggered.connect(self.removeClicked) self.menu.addAction(remAct) self.menu.remAct = remAct self.ui.itemList.contextMenuEvent = self.itemListContextMenuEvent - - - #def storeSvg(self): - #from pyqtgraph.GraphicsScene.exportDialog import ExportDialog - #ex = ExportDialog(self.ui.view) - #ex.show() - - #def storePng(self): - #self.ui.view.writeImage() def splitterMoved(self): self.resizeEvent() @@ -135,7 +120,6 @@ class Canvas(QtGui.QWidget): s = min(self.width(), max(100, min(200, self.width()*0.25))) s2 = self.width()-s self.ui.splitter.setSizes([s2, s]) - def updateRedirect(self, *args): ### Decide whether/where to redirect items and make it so @@ -154,7 +138,6 @@ class Canvas(QtGui.QWidget): self.reclaimItems() else: self.redirectItems(redirect) - def redirectItems(self, canvas): for i in self.items: @@ -171,12 +154,9 @@ class Canvas(QtGui.QWidget): else: parent.removeChild(li) canvas.addItem(i) - def reclaimItems(self): items = self.items - #self.items = {'Grid': items['Grid']} - #del items['Grid'] self.items = [self.grid] items.remove(self.grid) @@ -185,9 +165,6 @@ class Canvas(QtGui.QWidget): self.addItem(i) def treeItemChanged(self, item, col): - #gi = self.items.get(item.name, None) - #if gi is None: - #return try: citem = item.canvasItem() except AttributeError: @@ -203,25 +180,16 @@ class Canvas(QtGui.QWidget): def treeItemSelected(self): sel = self.selectedItems() - #sel = [] - #for listItem in self.itemList.selectedItems(): - #if hasattr(listItem, 'canvasItem') and listItem.canvasItem is not None: - #sel.append(listItem.canvasItem) - #sel = [self.items[item.name] for item in sel] - + if len(sel) == 0: - #self.selectWidget.hide() return multi = len(sel) > 1 for i in self.items: - #i.ctrlWidget().hide() ## updated the selected state of every item i.selectionChanged(i in sel, multi) if len(sel)==1: - #item = sel[0] - #item.ctrlWidget().show() self.multiSelectBox.hide() self.ui.mirrorSelectionBtn.hide() self.ui.reflectSelectionBtn.hide() @@ -229,14 +197,6 @@ class Canvas(QtGui.QWidget): elif len(sel) > 1: self.showMultiSelectBox() - #if item.isMovable(): - #self.selectBox.setPos(item.item.pos()) - #self.selectBox.setSize(item.item.sceneBoundingRect().size()) - #self.selectBox.show() - #else: - #self.selectBox.hide() - - #self.emit(QtCore.SIGNAL('itemSelected'), self, item) self.sigSelectionChanged.emit(self, sel) def selectedItems(self): @@ -245,19 +205,9 @@ class Canvas(QtGui.QWidget): """ return [item.canvasItem() for item in self.itemList.selectedItems() if item.canvasItem() is not None] - #def selectedItem(self): - #sel = self.itemList.selectedItems() - #if sel is None or len(sel) < 1: - #return - #return self.items.get(sel[0].name, None) - def selectItem(self, item): li = item.listItem - #li = self.getListItem(item.name()) - #print "select", li self.itemList.setCurrentItem(li) - - def showMultiSelectBox(self): ## Get list of selected canvas items @@ -281,7 +231,6 @@ class Canvas(QtGui.QWidget): self.ui.mirrorSelectionBtn.show() self.ui.reflectSelectionBtn.show() self.ui.resetTransformsBtn.show() - #self.multiSelectBoxBase = self.multiSelectBox.getState().copy() def mirrorSelectionClicked(self): for ci in self.selectedItems(): @@ -312,7 +261,6 @@ class Canvas(QtGui.QWidget): ci.setTemporaryTransform(transform) ci.sigTransformChanged.emit(ci) - def addGraphicsItem(self, item, **opts): """Add a new GraphicsItem to the scene at pos. Common options are name, pos, scale, and z @@ -321,13 +269,11 @@ class Canvas(QtGui.QWidget): item._canvasItem = citem self.addItem(citem) return citem - def addGroup(self, name, **kargs): group = GroupCanvasItem(name=name) self.addItem(group, **kargs) return group - def addItem(self, citem): """ @@ -363,7 +309,6 @@ class Canvas(QtGui.QWidget): #name = newname ## find parent and add item to tree - #currentNode = self.itemList.invisibleRootItem() insertLocation = 0 #print "Inserting node:", name @@ -413,11 +358,7 @@ class Canvas(QtGui.QWidget): node.setCheckState(0, QtCore.Qt.Unchecked) node.name = name - #if citem.opts['parent'] != None: - ## insertLocation is incorrect in this case parent.insertChild(insertLocation, node) - #else: - #root.insertChild(insertLocation, node) citem.name = name citem.listItem = node @@ -435,36 +376,6 @@ class Canvas(QtGui.QWidget): if len(self.items) == 2: self.autoRange() - - #for n in name: - #nextnode = None - #for x in range(currentNode.childCount()): - #ch = currentNode.child(x) - #if hasattr(ch, 'name'): ## check Z-value of current item to determine insert location - #zval = ch.canvasItem.zValue() - #if zval > z: - ###print " ->", x - #insertLocation = x+1 - #if n == ch.text(0): - #nextnode = ch - #break - #if nextnode is None: ## If name doesn't exist, create it - #nextnode = QtGui.QTreeWidgetItem([n]) - #nextnode.setFlags((nextnode.flags() | QtCore.Qt.ItemIsUserCheckable) & ~QtCore.Qt.ItemIsDropEnabled) - #nextnode.setCheckState(0, QtCore.Qt.Checked) - ### Add node to correct position in list by Z-value - ###print " ==>", insertLocation - #currentNode.insertChild(insertLocation, nextnode) - - #if n == name[-1]: ## This is the leaf; add some extra properties. - #nextnode.name = name - - #if n == name[0]: ## This is the root; make the item movable - #nextnode.setFlags(nextnode.flags() | QtCore.Qt.ItemIsDragEnabled) - #else: - #nextnode.setFlags(nextnode.flags() & ~QtCore.Qt.ItemIsDragEnabled) - - #currentNode = nextnode return citem def treeItemMoved(self, item, parent, index): @@ -481,31 +392,6 @@ class Canvas(QtGui.QWidget): for i in range(len(siblings)): item = siblings[i] item.setZValue(zvals[i]) - #item = self.itemList.topLevelItem(i) - - ##ci = self.items[item.name] - #ci = item.canvasItem - #if ci is None: - #continue - #if ci.zValue() != zvals[i]: - #ci.setZValue(zvals[i]) - - #if self.itemList.topLevelItemCount() < 2: - #return - #name = item.name - #gi = self.items[name] - #if index == 0: - #next = self.itemList.topLevelItem(1) - #z = self.items[next.name].zValue()+1 - #else: - #prev = self.itemList.topLevelItem(index-1) - #z = self.items[prev.name].zValue()-1 - #gi.setZValue(z) - - - - - def itemVisibilityChanged(self, item): listItem = item.listItem @@ -521,7 +407,6 @@ class Canvas(QtGui.QWidget): if isinstance(item, QtGui.QTreeWidgetItem): item = item.canvasItem() - if isinstance(item, CanvasItem): item.setCanvas(None) listItem = item.listItem @@ -559,15 +444,10 @@ class Canvas(QtGui.QWidget): def getListItem(self, name): return self.items[name] - #def scene(self): - #return self.view.scene() - def itemTransformChanged(self, item): - #self.emit(QtCore.SIGNAL('itemTransformChanged'), self, item) self.sigItemTransformChanged.emit(self, item) def itemTransformChangeFinished(self, item): - #self.emit(QtCore.SIGNAL('itemTransformChangeFinished'), self, item) self.sigItemTransformChangeFinished.emit(self, item) def itemListContextMenuEvent(self, ev): @@ -575,13 +455,13 @@ class Canvas(QtGui.QWidget): self.menu.popup(ev.globalPos()) def removeClicked(self): - #self.removeItem(self.menuItem) for item in self.selectedItems(): self.removeItem(item) self.menuItem = None import gc gc.collect() + class SelectBox(ROI): def __init__(self, scalable=False): #QtGui.QGraphicsRectItem.__init__(self, 0, 0, size[0], size[1]) @@ -593,14 +473,3 @@ class SelectBox(ROI): self.addScaleHandle([0, 0], center, lockAspect=True) self.addRotateHandle([0, 1], center) self.addRotateHandle([1, 0], center) - - - - - - - - - - - diff --git a/pyqtgraph/canvas/CanvasItem.py b/pyqtgraph/canvas/CanvasItem.py index a06235b2..bab89e89 100644 --- a/pyqtgraph/canvas/CanvasItem.py +++ b/pyqtgraph/canvas/CanvasItem.py @@ -87,14 +87,12 @@ class CanvasItem(QtCore.QObject): self.alphaSlider.valueChanged.connect(self.alphaChanged) self.alphaSlider.sliderPressed.connect(self.alphaPressed) self.alphaSlider.sliderReleased.connect(self.alphaReleased) - #self.canvas.sigSelectionChanged.connect(self.selectionChanged) self.resetTransformBtn.clicked.connect(self.resetTransformClicked) self.copyBtn.clicked.connect(self.copyClicked) self.pasteBtn.clicked.connect(self.pasteClicked) self.setMovable(self.opts['movable']) ## update gui to reflect this option - if 'transform' in self.opts: self.baseTransform = self.opts['transform'] else: @@ -114,7 +112,6 @@ class CanvasItem(QtCore.QObject): ## every CanvasItem implements its own individual selection box ## so that subclasses are free to make their own. self.selectBox = SelectBox(scalable=self.opts['scalable'], rotatable=self.opts['rotatable']) - #self.canvas.scene().addItem(self.selectBox) self.selectBox.hide() self.selectBox.setZValue(1e6) self.selectBox.sigRegionChanged.connect(self.selectBoxChanged) ## calls selectBoxMoved @@ -129,16 +126,7 @@ class CanvasItem(QtCore.QObject): self.tempTransform = SRTTransform() ## holds the additional transform that happens during a move - gets added to the userTransform when move is done. self.userTransform = SRTTransform() ## stores the total transform of the object self.resetUserTransform() - - ## now happens inside resetUserTransform -> selectBoxToItem - # self.selectBoxBase = self.selectBox.getState().copy() - - - #print "Created canvas item", self - #print " base:", self.baseTransform - #print " user:", self.userTransform - #print " temp:", self.tempTransform - #print " bounds:", self.item.sceneBoundingRect() + def setMovable(self, m): self.opts['movable'] = m @@ -239,7 +227,6 @@ class CanvasItem(QtCore.QObject): # s=self.updateTransform() # self.setTranslate(-2*s['pos'][0], -2*s['pos'][1]) # self.selectBoxFromUser() - def hasUserTransform(self): #print self.userRotate, self.userTranslate @@ -255,7 +242,6 @@ class CanvasItem(QtCore.QObject): def isMovable(self): return self.opts['movable'] - def selectBoxMoved(self): """The selection box has moved; get its transformation information and pass to the graphics item""" self.userTransform = self.selectBox.getGlobalTransform(relativeTo=self.selectBoxBase) @@ -290,7 +276,6 @@ class CanvasItem(QtCore.QObject): self.userTransform.setScale(x, y) self.selectBoxFromUser() self.updateTransform() - def setTemporaryTransform(self, transform): self.tempTransform = transform @@ -302,21 +287,6 @@ class CanvasItem(QtCore.QObject): self.resetTemporaryTransform() self.selectBoxFromUser() ## update the selection box to match the new userTransform - #st = self.userTransform.saveState() - - #self.userTransform = self.userTransform * self.tempTransform ## order is important! - - #### matrix multiplication affects the scale factors, need to reset - #if st['scale'][0] < 0 or st['scale'][1] < 0: - #nst = self.userTransform.saveState() - #self.userTransform.setScale([-nst['scale'][0], -nst['scale'][1]]) - - #self.resetTemporaryTransform() - #self.selectBoxFromUser() - #self.selectBoxChangeFinished() - - - def resetTemporaryTransform(self): self.tempTransform = SRTTransform() ## don't use Transform.reset()--this transform might be used elsewhere. self.updateTransform() @@ -339,20 +309,13 @@ class CanvasItem(QtCore.QObject): def displayTransform(self, transform): """Updates transform numbers in the ctrl widget.""" - tr = transform.saveState() self.transformGui.translateLabel.setText("Translate: (%f, %f)" %(tr['pos'][0], tr['pos'][1])) self.transformGui.rotateLabel.setText("Rotate: %f degrees" %tr['angle']) self.transformGui.scaleLabel.setText("Scale: (%f, %f)" %(tr['scale'][0], tr['scale'][1])) - #self.transformGui.mirrorImageCheck.setChecked(False) - #if tr['scale'][0] < 0: - # self.transformGui.mirrorImageCheck.setChecked(True) - def resetUserTransform(self): - #self.userRotate = 0 - #self.userTranslate = pg.Point(0,0) self.userTransform.reset() self.updateTransform() @@ -368,8 +331,6 @@ class CanvasItem(QtCore.QObject): def restoreTransform(self, tr): try: - #self.userTranslate = pg.Point(tr['trans']) - #self.userRotate = tr['rot'] self.userTransform = SRTTransform(tr) self.updateTransform() @@ -377,16 +338,11 @@ class CanvasItem(QtCore.QObject): self.sigTransformChanged.emit(self) self.sigTransformChangeFinished.emit(self) except: - #self.userTranslate = pg.Point([0,0]) - #self.userRotate = 0 self.userTransform = SRTTransform() debug.printExc("Failed to load transform:") - #print "set transform", self, self.userTranslate def saveTransform(self): """Return a dict containing the current user transform""" - #print "save transform", self, self.userTranslate - #return {'trans': list(self.userTranslate), 'rot': self.userRotate} return self.userTransform.saveState() def selectBoxFromUser(self): @@ -404,7 +360,6 @@ class CanvasItem(QtCore.QObject): #self.selectBox.setAngle(self.userRotate) #self.selectBox.setPos([x2, y2]) self.selectBox.blockSignals(False) - def selectBoxToItem(self): """Move/scale the selection box so it fits the item's bounding rect. (assumes item is not rotated)""" @@ -424,11 +379,6 @@ class CanvasItem(QtCore.QObject): self.opts['z'] = z if z is not None: self._graphicsItem.setZValue(z) - - #def selectionChanged(self, canvas, items): - #self.selected = len(items) == 1 and (items[0] is self) - #self.showSelectBox() - def selectionChanged(self, sel, multi): """ @@ -456,16 +406,12 @@ class CanvasItem(QtCore.QObject): def hideSelectBox(self): self.selectBox.hide() - def selectBoxChanged(self): self.selectBoxMoved() - #self.updateTransform(self.selectBox) - #self.emit(QtCore.SIGNAL('transformChanged'), self) self.sigTransformChanged.emit(self) def selectBoxChangeFinished(self): - #self.emit(QtCore.SIGNAL('transformChangeFinished'), self) self.sigTransformChangeFinished.emit(self) def alphaPressed(self): From ee117fd957c1f001a089c1dc6733ce35fb4dff69 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 13 Sep 2017 21:02:58 -0700 Subject: [PATCH 029/135] Give CanvasItem alpha/setAlpha methods --- pyqtgraph/canvas/CanvasItem.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pyqtgraph/canvas/CanvasItem.py b/pyqtgraph/canvas/CanvasItem.py index bab89e89..eb6d0a61 100644 --- a/pyqtgraph/canvas/CanvasItem.py +++ b/pyqtgraph/canvas/CanvasItem.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import numpy as np from ..Qt import QtGui, QtCore, QtSvg, QT_LIB from ..graphicsItems.ROI import ROI from .. import SRTTransform, ItemGroup @@ -239,6 +240,12 @@ class CanvasItem(QtCore.QObject): alpha = val / 1023. self._graphicsItem.setOpacity(alpha) + def setAlpha(self, alpha): + self.alphaSlider.setValue(int(np.clip(alpha * 1023, 0, 1023))) + + def alpha(self): + return self.alphaSlider.value() / 1023. + def isMovable(self): return self.opts['movable'] From 65b5b6a7bc40a88e6eea75cbe488406f5861c1db Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 13 Sep 2017 21:04:04 -0700 Subject: [PATCH 030/135] Add CanvasItem.saveState/restoreState --- pyqtgraph/canvas/Canvas.py | 9 +++++---- pyqtgraph/canvas/CanvasItem.py | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/canvas/Canvas.py b/pyqtgraph/canvas/Canvas.py index a9f1d918..2ebc2ba1 100644 --- a/pyqtgraph/canvas/Canvas.py +++ b/pyqtgraph/canvas/Canvas.py @@ -19,9 +19,11 @@ elif QT_LIB == 'PyQt5': import numpy as np from .. import debug import weakref +import gc from .CanvasManager import CanvasManager from .CanvasItem import CanvasItem, GroupCanvasItem + class Canvas(QtGui.QWidget): sigSelectionChanged = QtCore.Signal(object, object) @@ -417,25 +419,24 @@ class Canvas(QtGui.QWidget): ctrl = item.ctrlWidget() ctrl.hide() self.ui.ctrlLayout.removeWidget(ctrl) + ctrl.setParent(None) else: if hasattr(item, '_canvasItem'): self.removeItem(item._canvasItem) else: self.view.removeItem(item) - - ## disconnect signals, remove from list, etc.. + + gc.collect() def clear(self): while len(self.items) > 0: self.removeItem(self.items[0]) - def addToScene(self, item): self.view.addItem(item) def removeFromScene(self, item): self.view.removeItem(item) - def listItems(self): """Return a dictionary of name:item pairs""" diff --git a/pyqtgraph/canvas/CanvasItem.py b/pyqtgraph/canvas/CanvasItem.py index eb6d0a61..c406256c 100644 --- a/pyqtgraph/canvas/CanvasItem.py +++ b/pyqtgraph/canvas/CanvasItem.py @@ -453,6 +453,25 @@ class CanvasItem(QtCore.QObject): def isVisible(self): return self.opts['visible'] + def saveState(self): + return { + 'type': self.__class__.__name__, + 'name': self.name, + 'visible': self.isVisible(), + 'alpha': self.alpha(), + 'userTransform': self.saveTransform(), + 'z': self.zValue(), + 'scalable': self.opts['scalable'], + 'rotatable': self.opts['rotatable'], + 'movable': self.opts['movable'], + } + + def restoreState(self, state): + self.setVisible(state['visible']) + self.setAlpha(state['alpha']) + self.restoreTransform(state['userTransform']) + self.setZValue(state['z']) + class GroupCanvasItem(CanvasItem): """ From d8ffc21446d618c4464a10a738b7d4d7762bd58d Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 13 Sep 2017 21:04:43 -0700 Subject: [PATCH 031/135] Refactor canvas ui to make it easier to embed / extend --- pyqtgraph/canvas/CanvasTemplate.ui | 176 +++++++++++----------- pyqtgraph/canvas/CanvasTemplate_pyqt.py | 63 ++++---- pyqtgraph/canvas/CanvasTemplate_pyqt5.py | 66 ++++---- pyqtgraph/canvas/CanvasTemplate_pyside.py | 66 ++++---- 4 files changed, 198 insertions(+), 173 deletions(-) diff --git a/pyqtgraph/canvas/CanvasTemplate.ui b/pyqtgraph/canvas/CanvasTemplate.ui index b05c11cd..bfdacf38 100644 --- a/pyqtgraph/canvas/CanvasTemplate.ui +++ b/pyqtgraph/canvas/CanvasTemplate.ui @@ -6,14 +6,14 @@ 0 0 - 490 - 414 + 821 + 578 Form - + 0 @@ -26,88 +26,96 @@ Qt::Horizontal - - - - - - - 0 - 1 - - - - Auto Range - - - - - - - 0 - - - - - Check to display all local items in a remote canvas. - - - Redirect - - - - - - - - - - - - - 0 - 100 - - - - true - - - - 1 + + + Qt::Vertical + + + + + + + + 0 + 1 + - - - - - - - 0 - - - - - - - Reset Transforms - - - - - - - Mirror Selection - - - - - - - MirrorXY - - - - + + Auto Range + + + + + + + 0 + + + + + Check to display all local items in a remote canvas. + + + Redirect + + + + + + + + + + + + + 0 + 100 + + + + true + + + + 1 + + + + + + + + Reset Transforms + + + + + + + Mirror Selection + + + + + + + MirrorXY + + + + + + + + + 0 + + + 0 + + + diff --git a/pyqtgraph/canvas/CanvasTemplate_pyqt.py b/pyqtgraph/canvas/CanvasTemplate_pyqt.py index b65ef465..3569c8e7 100644 --- a/pyqtgraph/canvas/CanvasTemplate_pyqt.py +++ b/pyqtgraph/canvas/CanvasTemplate_pyqt.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'pyqtgraph/canvas/CanvasTemplate.ui' +# Form implementation generated from reading ui file 'CanvasTemplate.ui' # # Created by: PyQt4 UI code generator 4.11.4 # @@ -25,39 +25,42 @@ except AttributeError: class Ui_Form(object): def setupUi(self, Form): Form.setObjectName(_fromUtf8("Form")) - Form.resize(490, 414) - self.gridLayout = QtGui.QGridLayout(Form) - self.gridLayout.setMargin(0) - self.gridLayout.setSpacing(0) - self.gridLayout.setObjectName(_fromUtf8("gridLayout")) + Form.resize(821, 578) + self.gridLayout_2 = QtGui.QGridLayout(Form) + self.gridLayout_2.setMargin(0) + self.gridLayout_2.setSpacing(0) + self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2")) self.splitter = QtGui.QSplitter(Form) self.splitter.setOrientation(QtCore.Qt.Horizontal) self.splitter.setObjectName(_fromUtf8("splitter")) self.view = GraphicsView(self.splitter) self.view.setObjectName(_fromUtf8("view")) - self.layoutWidget = QtGui.QWidget(self.splitter) - self.layoutWidget.setObjectName(_fromUtf8("layoutWidget")) - self.gridLayout_2 = QtGui.QGridLayout(self.layoutWidget) - self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2")) - self.autoRangeBtn = QtGui.QPushButton(self.layoutWidget) + self.vsplitter = QtGui.QSplitter(self.splitter) + self.vsplitter.setOrientation(QtCore.Qt.Vertical) + self.vsplitter.setObjectName(_fromUtf8("vsplitter")) + self.canvasCtrlWidget = QtGui.QWidget(self.vsplitter) + self.canvasCtrlWidget.setObjectName(_fromUtf8("canvasCtrlWidget")) + self.gridLayout = QtGui.QGridLayout(self.canvasCtrlWidget) + self.gridLayout.setObjectName(_fromUtf8("gridLayout")) + self.autoRangeBtn = QtGui.QPushButton(self.canvasCtrlWidget) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(1) sizePolicy.setHeightForWidth(self.autoRangeBtn.sizePolicy().hasHeightForWidth()) self.autoRangeBtn.setSizePolicy(sizePolicy) self.autoRangeBtn.setObjectName(_fromUtf8("autoRangeBtn")) - self.gridLayout_2.addWidget(self.autoRangeBtn, 2, 0, 1, 2) + self.gridLayout.addWidget(self.autoRangeBtn, 0, 0, 1, 2) self.horizontalLayout = QtGui.QHBoxLayout() self.horizontalLayout.setSpacing(0) self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout")) - self.redirectCheck = QtGui.QCheckBox(self.layoutWidget) + self.redirectCheck = QtGui.QCheckBox(self.canvasCtrlWidget) self.redirectCheck.setObjectName(_fromUtf8("redirectCheck")) self.horizontalLayout.addWidget(self.redirectCheck) - self.redirectCombo = CanvasCombo(self.layoutWidget) + self.redirectCombo = CanvasCombo(self.canvasCtrlWidget) self.redirectCombo.setObjectName(_fromUtf8("redirectCombo")) self.horizontalLayout.addWidget(self.redirectCombo) - self.gridLayout_2.addLayout(self.horizontalLayout, 5, 0, 1, 2) - self.itemList = TreeWidget(self.layoutWidget) + self.gridLayout.addLayout(self.horizontalLayout, 1, 0, 1, 2) + self.itemList = TreeWidget(self.canvasCtrlWidget) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(100) @@ -66,21 +69,23 @@ class Ui_Form(object): self.itemList.setHeaderHidden(True) self.itemList.setObjectName(_fromUtf8("itemList")) self.itemList.headerItem().setText(0, _fromUtf8("1")) - self.gridLayout_2.addWidget(self.itemList, 6, 0, 1, 2) - self.ctrlLayout = QtGui.QGridLayout() + self.gridLayout.addWidget(self.itemList, 2, 0, 1, 2) + self.resetTransformsBtn = QtGui.QPushButton(self.canvasCtrlWidget) + self.resetTransformsBtn.setObjectName(_fromUtf8("resetTransformsBtn")) + self.gridLayout.addWidget(self.resetTransformsBtn, 3, 0, 1, 2) + self.mirrorSelectionBtn = QtGui.QPushButton(self.canvasCtrlWidget) + self.mirrorSelectionBtn.setObjectName(_fromUtf8("mirrorSelectionBtn")) + self.gridLayout.addWidget(self.mirrorSelectionBtn, 4, 0, 1, 1) + self.reflectSelectionBtn = QtGui.QPushButton(self.canvasCtrlWidget) + self.reflectSelectionBtn.setObjectName(_fromUtf8("reflectSelectionBtn")) + self.gridLayout.addWidget(self.reflectSelectionBtn, 4, 1, 1, 1) + self.canvasItemCtrl = QtGui.QWidget(self.vsplitter) + self.canvasItemCtrl.setObjectName(_fromUtf8("canvasItemCtrl")) + self.ctrlLayout = QtGui.QGridLayout(self.canvasItemCtrl) + self.ctrlLayout.setMargin(0) self.ctrlLayout.setSpacing(0) self.ctrlLayout.setObjectName(_fromUtf8("ctrlLayout")) - self.gridLayout_2.addLayout(self.ctrlLayout, 10, 0, 1, 2) - self.resetTransformsBtn = QtGui.QPushButton(self.layoutWidget) - self.resetTransformsBtn.setObjectName(_fromUtf8("resetTransformsBtn")) - self.gridLayout_2.addWidget(self.resetTransformsBtn, 7, 0, 1, 1) - self.mirrorSelectionBtn = QtGui.QPushButton(self.layoutWidget) - self.mirrorSelectionBtn.setObjectName(_fromUtf8("mirrorSelectionBtn")) - self.gridLayout_2.addWidget(self.mirrorSelectionBtn, 3, 0, 1, 1) - self.reflectSelectionBtn = QtGui.QPushButton(self.layoutWidget) - self.reflectSelectionBtn.setObjectName(_fromUtf8("reflectSelectionBtn")) - self.gridLayout_2.addWidget(self.reflectSelectionBtn, 3, 1, 1, 1) - self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1) + self.gridLayout_2.addWidget(self.splitter, 0, 0, 1, 1) self.retranslateUi(Form) QtCore.QMetaObject.connectSlotsByName(Form) diff --git a/pyqtgraph/canvas/CanvasTemplate_pyqt5.py b/pyqtgraph/canvas/CanvasTemplate_pyqt5.py index 20f5e339..03310d39 100644 --- a/pyqtgraph/canvas/CanvasTemplate_pyqt5.py +++ b/pyqtgraph/canvas/CanvasTemplate_pyqt5.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'pyqtgraph/canvas/CanvasTemplate.ui' +# Form implementation generated from reading ui file 'CanvasTemplate.ui' # -# Created by: PyQt5 UI code generator 5.5.1 +# Created by: PyQt5 UI code generator 5.7.1 # # WARNING! All changes made in this file will be lost! @@ -11,39 +11,43 @@ from PyQt5 import QtCore, QtGui, QtWidgets class Ui_Form(object): def setupUi(self, Form): Form.setObjectName("Form") - Form.resize(490, 414) - self.gridLayout = QtWidgets.QGridLayout(Form) - self.gridLayout.setContentsMargins(0, 0, 0, 0) - self.gridLayout.setSpacing(0) - self.gridLayout.setObjectName("gridLayout") + Form.resize(821, 578) + self.gridLayout_2 = QtWidgets.QGridLayout(Form) + self.gridLayout_2.setContentsMargins(0, 0, 0, 0) + self.gridLayout_2.setSpacing(0) + self.gridLayout_2.setObjectName("gridLayout_2") self.splitter = QtWidgets.QSplitter(Form) self.splitter.setOrientation(QtCore.Qt.Horizontal) self.splitter.setObjectName("splitter") self.view = GraphicsView(self.splitter) self.view.setObjectName("view") - self.layoutWidget = QtWidgets.QWidget(self.splitter) - self.layoutWidget.setObjectName("layoutWidget") - self.gridLayout_2 = QtWidgets.QGridLayout(self.layoutWidget) - self.gridLayout_2.setObjectName("gridLayout_2") - self.autoRangeBtn = QtWidgets.QPushButton(self.layoutWidget) + self.vsplitter = QtWidgets.QSplitter(self.splitter) + self.vsplitter.setOrientation(QtCore.Qt.Vertical) + self.vsplitter.setObjectName("vsplitter") + self.canvasCtrlWidget = QtWidgets.QWidget(self.vsplitter) + self.canvasCtrlWidget.setObjectName("canvasCtrlWidget") + self.gridLayout = QtWidgets.QGridLayout(self.canvasCtrlWidget) + self.gridLayout.setContentsMargins(0, 0, 0, 0) + self.gridLayout.setObjectName("gridLayout") + self.autoRangeBtn = QtWidgets.QPushButton(self.canvasCtrlWidget) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(1) sizePolicy.setHeightForWidth(self.autoRangeBtn.sizePolicy().hasHeightForWidth()) self.autoRangeBtn.setSizePolicy(sizePolicy) self.autoRangeBtn.setObjectName("autoRangeBtn") - self.gridLayout_2.addWidget(self.autoRangeBtn, 2, 0, 1, 2) + self.gridLayout.addWidget(self.autoRangeBtn, 0, 0, 1, 2) self.horizontalLayout = QtWidgets.QHBoxLayout() self.horizontalLayout.setSpacing(0) self.horizontalLayout.setObjectName("horizontalLayout") - self.redirectCheck = QtWidgets.QCheckBox(self.layoutWidget) + self.redirectCheck = QtWidgets.QCheckBox(self.canvasCtrlWidget) self.redirectCheck.setObjectName("redirectCheck") self.horizontalLayout.addWidget(self.redirectCheck) - self.redirectCombo = CanvasCombo(self.layoutWidget) + self.redirectCombo = CanvasCombo(self.canvasCtrlWidget) self.redirectCombo.setObjectName("redirectCombo") self.horizontalLayout.addWidget(self.redirectCombo) - self.gridLayout_2.addLayout(self.horizontalLayout, 5, 0, 1, 2) - self.itemList = TreeWidget(self.layoutWidget) + self.gridLayout.addLayout(self.horizontalLayout, 1, 0, 1, 2) + self.itemList = TreeWidget(self.canvasCtrlWidget) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(100) @@ -52,21 +56,23 @@ class Ui_Form(object): self.itemList.setHeaderHidden(True) self.itemList.setObjectName("itemList") self.itemList.headerItem().setText(0, "1") - self.gridLayout_2.addWidget(self.itemList, 6, 0, 1, 2) - self.ctrlLayout = QtWidgets.QGridLayout() + self.gridLayout.addWidget(self.itemList, 2, 0, 1, 2) + self.resetTransformsBtn = QtWidgets.QPushButton(self.canvasCtrlWidget) + self.resetTransformsBtn.setObjectName("resetTransformsBtn") + self.gridLayout.addWidget(self.resetTransformsBtn, 3, 0, 1, 2) + self.mirrorSelectionBtn = QtWidgets.QPushButton(self.canvasCtrlWidget) + self.mirrorSelectionBtn.setObjectName("mirrorSelectionBtn") + self.gridLayout.addWidget(self.mirrorSelectionBtn, 4, 0, 1, 1) + self.reflectSelectionBtn = QtWidgets.QPushButton(self.canvasCtrlWidget) + self.reflectSelectionBtn.setObjectName("reflectSelectionBtn") + self.gridLayout.addWidget(self.reflectSelectionBtn, 4, 1, 1, 1) + self.canvasItemCtrl = QtWidgets.QWidget(self.vsplitter) + self.canvasItemCtrl.setObjectName("canvasItemCtrl") + self.ctrlLayout = QtWidgets.QGridLayout(self.canvasItemCtrl) + self.ctrlLayout.setContentsMargins(0, 0, 0, 0) self.ctrlLayout.setSpacing(0) self.ctrlLayout.setObjectName("ctrlLayout") - self.gridLayout_2.addLayout(self.ctrlLayout, 10, 0, 1, 2) - self.resetTransformsBtn = QtWidgets.QPushButton(self.layoutWidget) - self.resetTransformsBtn.setObjectName("resetTransformsBtn") - self.gridLayout_2.addWidget(self.resetTransformsBtn, 7, 0, 1, 1) - self.mirrorSelectionBtn = QtWidgets.QPushButton(self.layoutWidget) - self.mirrorSelectionBtn.setObjectName("mirrorSelectionBtn") - self.gridLayout_2.addWidget(self.mirrorSelectionBtn, 3, 0, 1, 1) - self.reflectSelectionBtn = QtWidgets.QPushButton(self.layoutWidget) - self.reflectSelectionBtn.setObjectName("reflectSelectionBtn") - self.gridLayout_2.addWidget(self.reflectSelectionBtn, 3, 1, 1, 1) - self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1) + self.gridLayout_2.addWidget(self.splitter, 0, 0, 1, 1) self.retranslateUi(Form) QtCore.QMetaObject.connectSlotsByName(Form) diff --git a/pyqtgraph/canvas/CanvasTemplate_pyside.py b/pyqtgraph/canvas/CanvasTemplate_pyside.py index b0e05a07..570d5bd1 100644 --- a/pyqtgraph/canvas/CanvasTemplate_pyside.py +++ b/pyqtgraph/canvas/CanvasTemplate_pyside.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'pyqtgraph/canvas/CanvasTemplate.ui' +# Form implementation generated from reading ui file 'CanvasTemplate.ui' # -# Created: Wed Nov 9 18:02:00 2016 +# Created: Fri Mar 24 16:09:39 2017 # by: pyside-uic 0.2.15 running on PySide 1.2.2 # # WARNING! All changes made in this file will be lost! @@ -12,40 +12,43 @@ from PySide import QtCore, QtGui class Ui_Form(object): def setupUi(self, Form): Form.setObjectName("Form") - Form.resize(490, 414) - self.gridLayout = QtGui.QGridLayout(Form) - self.gridLayout.setContentsMargins(0, 0, 0, 0) - self.gridLayout.setSpacing(0) - self.gridLayout.setObjectName("gridLayout") + Form.resize(821, 578) + self.gridLayout_2 = QtGui.QGridLayout(Form) + self.gridLayout_2.setContentsMargins(0, 0, 0, 0) + self.gridLayout_2.setSpacing(0) + self.gridLayout_2.setObjectName("gridLayout_2") self.splitter = QtGui.QSplitter(Form) self.splitter.setOrientation(QtCore.Qt.Horizontal) self.splitter.setObjectName("splitter") self.view = GraphicsView(self.splitter) self.view.setObjectName("view") - self.layoutWidget = QtGui.QWidget(self.splitter) - self.layoutWidget.setObjectName("layoutWidget") - self.gridLayout_2 = QtGui.QGridLayout(self.layoutWidget) - self.gridLayout_2.setContentsMargins(0, 0, 0, 0) - self.gridLayout_2.setObjectName("gridLayout_2") - self.autoRangeBtn = QtGui.QPushButton(self.layoutWidget) + self.vsplitter = QtGui.QSplitter(self.splitter) + self.vsplitter.setOrientation(QtCore.Qt.Vertical) + self.vsplitter.setObjectName("vsplitter") + self.canvasCtrlWidget = QtGui.QWidget(self.vsplitter) + self.canvasCtrlWidget.setObjectName("canvasCtrlWidget") + self.gridLayout = QtGui.QGridLayout(self.canvasCtrlWidget) + self.gridLayout.setContentsMargins(0, 0, 0, 0) + self.gridLayout.setObjectName("gridLayout") + self.autoRangeBtn = QtGui.QPushButton(self.canvasCtrlWidget) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(1) sizePolicy.setHeightForWidth(self.autoRangeBtn.sizePolicy().hasHeightForWidth()) self.autoRangeBtn.setSizePolicy(sizePolicy) self.autoRangeBtn.setObjectName("autoRangeBtn") - self.gridLayout_2.addWidget(self.autoRangeBtn, 2, 0, 1, 2) + self.gridLayout.addWidget(self.autoRangeBtn, 0, 0, 1, 2) self.horizontalLayout = QtGui.QHBoxLayout() self.horizontalLayout.setSpacing(0) self.horizontalLayout.setObjectName("horizontalLayout") - self.redirectCheck = QtGui.QCheckBox(self.layoutWidget) + self.redirectCheck = QtGui.QCheckBox(self.canvasCtrlWidget) self.redirectCheck.setObjectName("redirectCheck") self.horizontalLayout.addWidget(self.redirectCheck) - self.redirectCombo = CanvasCombo(self.layoutWidget) + self.redirectCombo = CanvasCombo(self.canvasCtrlWidget) self.redirectCombo.setObjectName("redirectCombo") self.horizontalLayout.addWidget(self.redirectCombo) - self.gridLayout_2.addLayout(self.horizontalLayout, 5, 0, 1, 2) - self.itemList = TreeWidget(self.layoutWidget) + self.gridLayout.addLayout(self.horizontalLayout, 1, 0, 1, 2) + self.itemList = TreeWidget(self.canvasCtrlWidget) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(100) @@ -54,21 +57,24 @@ class Ui_Form(object): self.itemList.setHeaderHidden(True) self.itemList.setObjectName("itemList") self.itemList.headerItem().setText(0, "1") - self.gridLayout_2.addWidget(self.itemList, 6, 0, 1, 2) - self.ctrlLayout = QtGui.QGridLayout() - self.ctrlLayout.setSpacing(0) - self.ctrlLayout.setObjectName("ctrlLayout") - self.gridLayout_2.addLayout(self.ctrlLayout, 10, 0, 1, 2) - self.resetTransformsBtn = QtGui.QPushButton(self.layoutWidget) + self.gridLayout.addWidget(self.itemList, 2, 0, 1, 2) + self.resetTransformsBtn = QtGui.QPushButton(self.canvasCtrlWidget) self.resetTransformsBtn.setObjectName("resetTransformsBtn") - self.gridLayout_2.addWidget(self.resetTransformsBtn, 7, 0, 1, 1) - self.mirrorSelectionBtn = QtGui.QPushButton(self.layoutWidget) + self.gridLayout.addWidget(self.resetTransformsBtn, 3, 0, 1, 2) + self.mirrorSelectionBtn = QtGui.QPushButton(self.canvasCtrlWidget) self.mirrorSelectionBtn.setObjectName("mirrorSelectionBtn") - self.gridLayout_2.addWidget(self.mirrorSelectionBtn, 3, 0, 1, 1) - self.reflectSelectionBtn = QtGui.QPushButton(self.layoutWidget) + self.gridLayout.addWidget(self.mirrorSelectionBtn, 4, 0, 1, 1) + self.reflectSelectionBtn = QtGui.QPushButton(self.canvasCtrlWidget) self.reflectSelectionBtn.setObjectName("reflectSelectionBtn") - self.gridLayout_2.addWidget(self.reflectSelectionBtn, 3, 1, 1, 1) - self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1) + self.gridLayout.addWidget(self.reflectSelectionBtn, 4, 1, 1, 1) + self.canvasItemCtrl = QtGui.QWidget(self.vsplitter) + self.canvasItemCtrl.setObjectName("canvasItemCtrl") + self.ctrlLayout = QtGui.QGridLayout(self.canvasItemCtrl) + self.ctrlLayout.setContentsMargins(0, 0, 0, 0) + self.ctrlLayout.setSpacing(0) + self.ctrlLayout.setContentsMargins(0, 0, 0, 0) + self.ctrlLayout.setObjectName("ctrlLayout") + self.gridLayout_2.addWidget(self.splitter, 0, 0, 1, 1) self.retranslateUi(Form) QtCore.QMetaObject.connectSlotsByName(Form) From 5d6be5796b42f33f5ea91d7082cd51a351db4f22 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 13 Sep 2017 21:36:34 -0700 Subject: [PATCH 032/135] image export: add option to invert pixel values (but not hues) --- pyqtgraph/exporters/ImageExporter.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/exporters/ImageExporter.py b/pyqtgraph/exporters/ImageExporter.py index 4dc07d84..ffa59091 100644 --- a/pyqtgraph/exporters/ImageExporter.py +++ b/pyqtgraph/exporters/ImageExporter.py @@ -27,6 +27,7 @@ class ImageExporter(Exporter): {'name': 'height', 'type': 'int', 'value': int(tr.height()), 'limits': (0, None)}, {'name': 'antialias', 'type': 'bool', 'value': True}, {'name': 'background', 'type': 'color', 'value': bg}, + {'name': 'invertValue', 'type': 'bool', 'value': False} ]) self.params.param('width').sigValueChanged.connect(self.widthChanged) self.params.param('height').sigValueChanged.connect(self.heightChanged) @@ -67,13 +68,15 @@ class ImageExporter(Exporter): w, h = self.params['width'], self.params['height'] if w == 0 or h == 0: raise Exception("Cannot export image with size=0 (requested export size is %dx%d)" % (w,h)) - bg = np.empty((self.params['width'], self.params['height'], 4), dtype=np.ubyte) + bg = np.empty((self.params['height'], self.params['width'], 4), dtype=np.ubyte) color = self.params['background'] bg[:,:,0] = color.blue() bg[:,:,1] = color.green() bg[:,:,2] = color.red() bg[:,:,3] = color.alpha() - self.png = fn.makeQImage(bg, alpha=True) + + self.png = fn.makeQImage(bg, alpha=True, copy=False, transpose=False) + self.bg = bg ## set resolution of image: origTargetRect = self.getTargetRect() @@ -91,6 +94,12 @@ class ImageExporter(Exporter): self.setExportMode(False) painter.end() + if self.params['invertValue']: + mn = bg[...,:3].min(axis=2) + mx = bg[...,:3].max(axis=2) + d = (255 - mx) - mn + bg[...,:3] += d[...,np.newaxis] + if copy: QtGui.QApplication.clipboard().setImage(self.png) elif toBytes: From 6287874b5c3267d40128d637ab08a7957f687c5c Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 13 Sep 2017 21:37:19 -0700 Subject: [PATCH 033/135] Minor fix - check for ragged array length when exporting to hdf5 --- pyqtgraph/exporters/HDF5Exporter.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/exporters/HDF5Exporter.py b/pyqtgraph/exporters/HDF5Exporter.py index cc8b5733..584a9f71 100644 --- a/pyqtgraph/exporters/HDF5Exporter.py +++ b/pyqtgraph/exporters/HDF5Exporter.py @@ -42,14 +42,20 @@ class HDF5Exporter(Exporter): dsname = self.params['Name'] fd = h5py.File(fileName, 'a') # forces append to file... 'w' doesn't seem to "delete/overwrite" data = [] - + appendAllX = self.params['columnMode'] == '(x,y) per plot' - for i,c in enumerate(self.item.curves): + #print dir(self.item.curves[0]) + tlen = 0 + for i, c in enumerate(self.item.curves): d = c.getData() + if i > 0 and len(d[0]) != tlen: + raise ValueError ("HDF5 Export requires all curves in plot to have same length") if appendAllX or i == 0: data.append(d[0]) + tlen = len(d[0]) data.append(d[1]) - + + fdata = numpy.array(data).astype('double') dset = fd.create_dataset(dsname, data=fdata) fd.close() From e06fc101f5ba2e1313de0f33bc9f56b91b1a4611 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 13 Sep 2017 22:13:50 -0700 Subject: [PATCH 034/135] Add function to enable faulthandler on all threads --- pyqtgraph/debug.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/pyqtgraph/debug.py b/pyqtgraph/debug.py index 0da24d7c..61ae9fd5 100644 --- a/pyqtgraph/debug.py +++ b/pyqtgraph/debug.py @@ -1186,3 +1186,23 @@ class ThreadColor(object): c = (len(self.colors) % 15) + 1 self.colors[tid] = c return self.colors[tid] + + +def enableFaulthandler(): + """ Enable faulthandler for all threads. + + If the faulthandler package is available, this function disables and then + re-enables fault handling for all threads (this is necessary to ensure any + new threads are handled correctly), and returns True. + + If faulthandler is not available, then returns False. + """ + try: + import faulthandler + # necessary to disable first or else new threads may not be handled. + faulthandler.disable() + faulthandler.enable(all_threads=True) + return True + except ImportError: + return False + From 1911a26f8488689c4844c3a37acbd81eaea4696f Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 13 Sep 2017 22:14:29 -0700 Subject: [PATCH 035/135] Allow Mutex to be used as drop-in replacement for python's Lock --- pyqtgraph/util/mutex.py | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/util/mutex.py b/pyqtgraph/util/mutex.py index 4a193127..c03c65c4 100644 --- a/pyqtgraph/util/mutex.py +++ b/pyqtgraph/util/mutex.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- -from ..Qt import QtCore import traceback +from ..Qt import QtCore + class Mutex(QtCore.QMutex): """ @@ -17,7 +18,7 @@ class Mutex(QtCore.QMutex): QtCore.QMutex.__init__(self, *args) self.l = QtCore.QMutex() ## for serializing access to self.tb self.tb = [] - self.debug = True ## True to enable debugging functions + self.debug = kargs.pop('debug', False) ## True to enable debugging functions def tryLock(self, timeout=None, id=None): if timeout is None: @@ -72,6 +73,16 @@ class Mutex(QtCore.QMutex): finally: self.l.unlock() + def acquire(self, blocking=True): + """Mimics threading.Lock.acquire() to allow this class as a drop-in replacement. + """ + return self.tryLock() + + def release(self): + """Mimics threading.Lock.release() to allow this class as a drop-in replacement. + """ + self.unlock() + def depth(self): self.l.lock() n = len(self.tb) @@ -91,4 +102,13 @@ class Mutex(QtCore.QMutex): def __enter__(self): self.lock() - return self \ No newline at end of file + return self + + +class RecursiveMutex(Mutex): + """Mimics threading.RLock class. + """ + def __init__(self, **kwds): + kwds['recursive'] = True + Mutex.__init__(self, **kwds) + From d081e5495667eb4adc7dd5e6423d8e8278efc37b Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 15 Sep 2017 08:54:50 -0700 Subject: [PATCH 036/135] EvalNode: add method to set code --- pyqtgraph/flowchart/library/Data.py | 33 +++++++++++++++++------------ 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/pyqtgraph/flowchart/library/Data.py b/pyqtgraph/flowchart/library/Data.py index 5236de8d..0ad7742b 100644 --- a/pyqtgraph/flowchart/library/Data.py +++ b/pyqtgraph/flowchart/library/Data.py @@ -189,31 +189,36 @@ class EvalNode(Node): self.ui = QtGui.QWidget() self.layout = QtGui.QGridLayout() - #self.addInBtn = QtGui.QPushButton('+Input') - #self.addOutBtn = QtGui.QPushButton('+Output') self.text = QtGui.QTextEdit() self.text.setTabStopWidth(30) self.text.setPlainText("# Access inputs as args['input_name']\nreturn {'output': None} ## one key per output terminal") - #self.layout.addWidget(self.addInBtn, 0, 0) - #self.layout.addWidget(self.addOutBtn, 0, 1) self.layout.addWidget(self.text, 1, 0, 1, 2) self.ui.setLayout(self.layout) - #QtCore.QObject.connect(self.addInBtn, QtCore.SIGNAL('clicked()'), self.addInput) - #self.addInBtn.clicked.connect(self.addInput) - #QtCore.QObject.connect(self.addOutBtn, QtCore.SIGNAL('clicked()'), self.addOutput) - #self.addOutBtn.clicked.connect(self.addOutput) self.text.focusOutEvent = self.focusOutEvent self.lastText = None def ctrlWidget(self): return self.ui - #def addInput(self): - #Node.addInput(self, 'input', renamable=True) + def setCode(self, code): + # unindent code; this allows nicer inline code specification when + # calling this method. + ind = [] + lines = code.split('\n') + for line in lines: + stripped = line.lstrip() + if len(stripped) > 0: + ind.append(len(line) - len(stripped)) + if len(ind) > 0: + ind = min(ind) + code = '\n'.join([line[ind:] for line in lines]) - #def addOutput(self): - #Node.addOutput(self, 'output', renamable=True) + self.text.clear() + self.text.insertPlainText(code) + + def code(self): + return self.text.toPlainText() def focusOutEvent(self, ev): text = str(self.text.toPlainText()) @@ -247,10 +252,10 @@ class EvalNode(Node): def restoreState(self, state): Node.restoreState(self, state) - self.text.clear() - self.text.insertPlainText(state['text']) + self.setCode(state['text']) self.restoreTerminals(state['terminals']) self.update() + class ColumnJoinNode(Node): """Concatenates record arrays and/or adds new columns""" From 868d9ebf2995007b5ef9558493bec1ee3bfdf055 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 15 Sep 2017 08:55:17 -0700 Subject: [PATCH 037/135] Add several new data nodes --- pyqtgraph/flowchart/library/Data.py | 114 ++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/pyqtgraph/flowchart/library/Data.py b/pyqtgraph/flowchart/library/Data.py index 0ad7742b..18f1c948 100644 --- a/pyqtgraph/flowchart/library/Data.py +++ b/pyqtgraph/flowchart/library/Data.py @@ -359,3 +359,117 @@ class ColumnJoinNode(Node): self.update() +class Mean(CtrlNode): + """Calculate the mean of an array across an axis. + """ + nodeName = 'Mean' + uiTemplate = [ + ('axis', 'intSpin', {'value': 0, 'min': -1, 'max': 1000000}), + ] + + def processData(self, data): + s = self.stateGroup.state() + ax = None if s['axis'] == -1 else s['axis'] + return data.mean(axis=ax) + + +class Max(CtrlNode): + """Calculate the maximum of an array across an axis. + """ + nodeName = 'Max' + uiTemplate = [ + ('axis', 'intSpin', {'value': 0, 'min': -1, 'max': 1000000}), + ] + + def processData(self, data): + s = self.stateGroup.state() + ax = None if s['axis'] == -1 else s['axis'] + return data.max(axis=ax) + + +class Min(CtrlNode): + """Calculate the minimum of an array across an axis. + """ + nodeName = 'Min' + uiTemplate = [ + ('axis', 'intSpin', {'value': 0, 'min': -1, 'max': 1000000}), + ] + + def processData(self, data): + s = self.stateGroup.state() + ax = None if s['axis'] == -1 else s['axis'] + return data.min(axis=ax) + + +class Stdev(CtrlNode): + """Calculate the standard deviation of an array across an axis. + """ + nodeName = 'Stdev' + uiTemplate = [ + ('axis', 'intSpin', {'value': -0, 'min': -1, 'max': 1000000}), + ] + + def processData(self, data): + s = self.stateGroup.state() + ax = None if s['axis'] == -1 else s['axis'] + return data.std(axis=ax) + + +class Index(CtrlNode): + """Select an index from an array axis. + """ + nodeName = 'Index' + uiTemplate = [ + ('axis', 'intSpin', {'value': 0, 'min': 0, 'max': 1000000}), + ('index', 'intSpin', {'value': 0, 'min': 0, 'max': 1000000}), + ] + + def processData(self, data): + s = self.stateGroup.state() + ax = s['axis'] + ind = s['index'] + if ax == 0: + # allow support for non-ndarray sequence types + return data[ind] + else: + return data.take(ind, axis=ax) + + +class Slice(CtrlNode): + """Select a slice from an array axis. + """ + nodeName = 'Slice' + uiTemplate = [ + ('axis', 'intSpin', {'value': 0, 'min': 0, 'max': 1e6}), + ('start', 'intSpin', {'value': 0, 'min': -1e6, 'max': 1e6}), + ('stop', 'intSpin', {'value': -1, 'min': -1e6, 'max': 1e6}), + ('step', 'intSpin', {'value': 1, 'min': -1e6, 'max': 1e6}), + ] + + def processData(self, data): + s = self.stateGroup.state() + ax = s['axis'] + start = s['start'] + stop = s['stop'] + step = s['step'] + if ax == 0: + # allow support for non-ndarray sequence types + return data[start:stop:step] + else: + sl = [slice(None) for i in range(data.ndim)] + sl[ax] = slice(start, stop, step) + return data[sl] + + +class AsType(CtrlNode): + """Convert an array to a different dtype. + """ + nodeName = 'AsType' + uiTemplate = [ + ('dtype', 'combo', {'values': ['float', 'int', 'float32', 'float64', 'float128', 'int8', 'int16', 'int32', 'int64', 'uint8', 'uint16', 'uint32', 'uint64'], 'index': 0}), + ] + + def processData(self, data): + s = self.stateGroup.state() + return data.astype(s['dtype']) + From 2016dc0df1b2658c9da9d8606e82a4ae9b5dd724 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 15 Sep 2017 08:56:45 -0700 Subject: [PATCH 038/135] fix nodes spinbox handling --- pyqtgraph/flowchart/library/Filters.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/pyqtgraph/flowchart/library/Filters.py b/pyqtgraph/flowchart/library/Filters.py index 9392b037..ada09dfb 100644 --- a/pyqtgraph/flowchart/library/Filters.py +++ b/pyqtgraph/flowchart/library/Filters.py @@ -38,7 +38,7 @@ class Bessel(CtrlNode): nodeName = 'BesselFilter' uiTemplate = [ ('band', 'combo', {'values': ['lowpass', 'highpass'], 'index': 0}), - ('cutoff', 'spin', {'value': 1000., 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}), + ('cutoff', 'spin', {'value': 1000., 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}), ('order', 'intSpin', {'value': 4, 'min': 1, 'max': 16}), ('bidir', 'check', {'checked': True}) ] @@ -57,10 +57,10 @@ class Butterworth(CtrlNode): nodeName = 'ButterworthFilter' uiTemplate = [ ('band', 'combo', {'values': ['lowpass', 'highpass'], 'index': 0}), - ('wPass', 'spin', {'value': 1000., 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}), - ('wStop', 'spin', {'value': 2000., 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}), - ('gPass', 'spin', {'value': 2.0, 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'dB', 'siPrefix': True}), - ('gStop', 'spin', {'value': 20.0, 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'dB', 'siPrefix': True}), + ('wPass', 'spin', {'value': 1000., 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}), + ('wStop', 'spin', {'value': 2000., 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}), + ('gPass', 'spin', {'value': 2.0, 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'dB', 'siPrefix': True}), + ('gStop', 'spin', {'value': 20.0, 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'dB', 'siPrefix': True}), ('bidir', 'check', {'checked': True}) ] @@ -78,14 +78,14 @@ class ButterworthNotch(CtrlNode): """Butterworth notch filter""" nodeName = 'ButterworthNotchFilter' uiTemplate = [ - ('low_wPass', 'spin', {'value': 1000., 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}), - ('low_wStop', 'spin', {'value': 2000., 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}), - ('low_gPass', 'spin', {'value': 2.0, 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'dB', 'siPrefix': True}), - ('low_gStop', 'spin', {'value': 20.0, 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'dB', 'siPrefix': True}), - ('high_wPass', 'spin', {'value': 3000., 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}), - ('high_wStop', 'spin', {'value': 4000., 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}), - ('high_gPass', 'spin', {'value': 2.0, 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'dB', 'siPrefix': True}), - ('high_gStop', 'spin', {'value': 20.0, 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'dB', 'siPrefix': True}), + ('low_wPass', 'spin', {'value': 1000., 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}), + ('low_wStop', 'spin', {'value': 2000., 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}), + ('low_gPass', 'spin', {'value': 2.0, 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'dB', 'siPrefix': True}), + ('low_gStop', 'spin', {'value': 20.0, 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'dB', 'siPrefix': True}), + ('high_wPass', 'spin', {'value': 3000., 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}), + ('high_wStop', 'spin', {'value': 4000., 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}), + ('high_gPass', 'spin', {'value': 2.0, 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'dB', 'siPrefix': True}), + ('high_gStop', 'spin', {'value': 20.0, 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'dB', 'siPrefix': True}), ('bidir', 'check', {'checked': True}) ] From 19fc846b90955d1f7a9274ba0d10ef7fae1a59c3 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 15 Sep 2017 08:58:29 -0700 Subject: [PATCH 039/135] gaussian node uses internal gaussianFilter function --- pyqtgraph/flowchart/library/Filters.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/pyqtgraph/flowchart/library/Filters.py b/pyqtgraph/flowchart/library/Filters.py index ada09dfb..9a7fa401 100644 --- a/pyqtgraph/flowchart/library/Filters.py +++ b/pyqtgraph/flowchart/library/Filters.py @@ -160,19 +160,13 @@ class Gaussian(CtrlNode): @metaArrayWrapper def processData(self, data): + sigma = self.ctrls['sigma'].value() try: import scipy.ndimage + return scipy.ndimage.gaussian_filter(data, sigma) except ImportError: - raise Exception("GaussianFilter node requires the package scipy.ndimage.") + return pgfn.gaussianFilter(data, sigma) - if hasattr(data, 'implements') and data.implements('MetaArray'): - info = data.infoCopy() - filt = pgfn.gaussianFilter(data.asarray(), self.ctrls['sigma'].value()) - if 'values' in info[0]: - info[0]['values'] = info[0]['values'][:filt.shape[0]] - return metaarray.MetaArray(filt, info=info) - else: - return pgfn.gaussianFilter(data, self.ctrls['sigma'].value()) class Derivative(CtrlNode): """Returns the pointwise derivative of the input""" From 237b8488371a7b68f224927c8b269ec6bbb2b41e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 15 Sep 2017 08:59:15 -0700 Subject: [PATCH 040/135] Allow binary operator nodes to select output type --- pyqtgraph/flowchart/library/Operators.py | 29 ++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/flowchart/library/Operators.py b/pyqtgraph/flowchart/library/Operators.py index 579d2cd2..596e8854 100644 --- a/pyqtgraph/flowchart/library/Operators.py +++ b/pyqtgraph/flowchart/library/Operators.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- from ..Node import Node +from .common import CtrlNode + class UniOpNode(Node): """Generic node for performing any operation like Out = In.fn()""" @@ -13,11 +15,22 @@ class UniOpNode(Node): def process(self, **args): return {'Out': getattr(args['In'], self.fn)()} -class BinOpNode(Node): +class BinOpNode(CtrlNode): """Generic node for performing any operation like A.fn(B)""" + + _dtypes = [ + 'float64', 'float32', 'float16', + 'int64', 'int32', 'int16', 'int8', + 'uint64', 'uint32', 'uint16', 'uint8' + ] + + uiTemplate = [ + ('outputType', 'combo', {'values': ['no change', 'input A', 'input B'] + _dtypes , 'index': 0}) + ] + def __init__(self, name, fn): self.fn = fn - Node.__init__(self, name, terminals={ + CtrlNode.__init__(self, name, terminals={ 'A': {'io': 'in'}, 'B': {'io': 'in'}, 'Out': {'io': 'out', 'bypass': 'A'} @@ -36,6 +49,18 @@ class BinOpNode(Node): out = fn(args['B']) if out is NotImplemented: raise Exception("Operation %s not implemented between %s and %s" % (fn, str(type(args['A'])), str(type(args['B'])))) + + # Coerce dtype if requested + typ = self.stateGroup.state()['outputType'] + if typ == 'no change': + pass + elif typ == 'input A': + out = out.astype(args['A'].dtype) + elif typ == 'input B': + out = out.astype(args['B'].dtype) + else: + out = out.astype(typ) + #print " ", fn, out return {'Out': out} From d65026f73d4d12913defcb77fb149fb21078c2b5 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 15 Sep 2017 08:59:31 -0700 Subject: [PATCH 041/135] add floor division node --- pyqtgraph/flowchart/library/Operators.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyqtgraph/flowchart/library/Operators.py b/pyqtgraph/flowchart/library/Operators.py index 596e8854..d1483c16 100644 --- a/pyqtgraph/flowchart/library/Operators.py +++ b/pyqtgraph/flowchart/library/Operators.py @@ -96,4 +96,10 @@ class DivideNode(BinOpNode): # try truediv first, followed by div BinOpNode.__init__(self, name, ('__truediv__', '__div__')) +class FloorDivideNode(BinOpNode): + """Returns A // B. Does not check input types.""" + nodeName = 'FloorDivide' + def __init__(self, name): + BinOpNode.__init__(self, name, '__floordiv__') + From fedecc5808e41a8c7d10e8803e758cbe13f90572 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 15 Sep 2017 09:00:50 -0700 Subject: [PATCH 042/135] minor fixes --- pyqtgraph/flowchart/Flowchart.py | 3 ++- pyqtgraph/flowchart/library/common.py | 12 +++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/flowchart/Flowchart.py b/pyqtgraph/flowchart/Flowchart.py index b623f5c7..e31f3999 100644 --- a/pyqtgraph/flowchart/Flowchart.py +++ b/pyqtgraph/flowchart/Flowchart.py @@ -189,7 +189,8 @@ class Flowchart(Node): self.viewBox.addItem(item) item.moveBy(*pos) self._nodes[name] = node - self.widget().addNode(node) + if node is not self.inputNode and node is not self.outputNode: + self.widget().addNode(node) node.sigClosed.connect(self.nodeClosed) node.sigRenamed.connect(self.nodeRenamed) node.sigOutputChanged.connect(self.nodeOutputChanged) diff --git a/pyqtgraph/flowchart/library/common.py b/pyqtgraph/flowchart/library/common.py index 425fe86c..8b3376c3 100644 --- a/pyqtgraph/flowchart/library/common.py +++ b/pyqtgraph/flowchart/library/common.py @@ -30,6 +30,11 @@ def generateUi(opts): k, t, o = opt else: raise Exception("Widget specification must be (name, type) or (name, type, {opts})") + + ## clean out these options so they don't get sent to SpinBox + hidden = o.pop('hidden', False) + tip = o.pop('tip', None) + if t == 'intSpin': w = QtGui.QSpinBox() if 'max' in o: @@ -63,11 +68,12 @@ def generateUi(opts): w = ColorButton() else: raise Exception("Unknown widget type '%s'" % str(t)) - if 'tip' in o: - w.setToolTip(o['tip']) + + if tip is not None: + w.setToolTip(tip) w.setObjectName(k) l.addRow(k, w) - if o.get('hidden', False): + if hidden: w.hide() label = l.labelForField(w) label.hide() From 698f37bd10a7bd089727404f0edb0f0f9389eace Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 15 Sep 2017 09:00:59 -0700 Subject: [PATCH 043/135] code cleanup --- pyqtgraph/flowchart/Flowchart.py | 61 ++++++++++++++++---------------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/pyqtgraph/flowchart/Flowchart.py b/pyqtgraph/flowchart/Flowchart.py index e31f3999..cbfd084e 100644 --- a/pyqtgraph/flowchart/Flowchart.py +++ b/pyqtgraph/flowchart/Flowchart.py @@ -166,6 +166,8 @@ class Flowchart(Node): n[oldName].rename(newName) def createNode(self, nodeType, name=None, pos=None): + """Create a new Node and add it to this flowchart. + """ if name is None: n = 0 while True: @@ -179,6 +181,10 @@ class Flowchart(Node): return node def addNode(self, node, name, pos=None): + """Add an existing Node to this flowchart. + + See also: createNode() + """ if pos is None: pos = [0, 0] if type(pos) in [QtCore.QPoint, QtCore.QPointF]: @@ -197,6 +203,8 @@ class Flowchart(Node): self.sigChartChanged.emit(self, 'add', node) def removeNode(self, node): + """Remove a Node from this flowchart. + """ node.close() def nodeClosed(self, node): @@ -234,7 +242,6 @@ class Flowchart(Node): term2 = self.internalTerminal(term2) term1.connectTo(term2) - def process(self, **args): """ Process data through the flowchart, returning the output. @@ -326,7 +333,6 @@ class Flowchart(Node): #print "DEPS:", deps ## determine correct node-processing order - #deps[self] = [] order = fn.toposort(deps) #print "ORDER1:", order @@ -350,7 +356,6 @@ class Flowchart(Node): if lastNode is None or ind > lastInd: lastNode = n lastInd = ind - #tdeps[t] = lastNode if lastInd is not None: dels.append((lastInd+1, t)) dels.sort(key=lambda a: a[0], reverse=True) @@ -405,27 +410,25 @@ class Flowchart(Node): self.inputWasSet = False else: self.sigStateChanged.emit() - - def chartGraphicsItem(self): - """Return the graphicsItem which displays the internals of this flowchart. - (graphicsItem() still returns the external-view item)""" - #return self._chartGraphicsItem + """Return the graphicsItem that displays the internal nodes and + connections of this flowchart. + + Note that the similar method `graphicsItem()` is inherited from Node + and returns the *external* graphical representation of this flowchart.""" return self.viewBox def widget(self): + """Return the control widget for this flowchart. + + This widget provides GUI access to the parameters for each node and a + graphical representation of the flowchart. + """ if self._widget is None: self._widget = FlowchartCtrlWidget(self) self.scene = self._widget.scene() self.viewBox = self._widget.viewBox() - #self._scene = QtGui.QGraphicsScene() - #self._widget.setScene(self._scene) - #self.scene.addItem(self.chartGraphicsItem()) - - #ci = self.chartGraphicsItem() - #self.viewBox.addItem(ci) - #self.viewBox.autoRange() return self._widget def listConnections(self): @@ -438,10 +441,11 @@ class Flowchart(Node): return conn def saveState(self): + """Return a serializable data structure representing the current state of this flowchart. + """ state = Node.saveState(self) state['nodes'] = [] state['connects'] = [] - #state['terminals'] = self.saveTerminals() for name, node in self._nodes.items(): cls = type(node) @@ -461,6 +465,8 @@ class Flowchart(Node): return state def restoreState(self, state, clear=False): + """Restore the state of this flowchart from a previous call to `saveState()`. + """ self.blockSignals(True) try: if clear: @@ -470,7 +476,6 @@ class Flowchart(Node): nodes.sort(key=lambda a: a['pos'][0]) for n in nodes: if n['name'] in self._nodes: - #self._nodes[n['name']].graphicsItem().moveBy(*n['pos']) self._nodes[n['name']].restoreState(n['state']) continue try: @@ -478,7 +483,6 @@ class Flowchart(Node): node.restoreState(n['state']) except: printExc("Error creating node %s: (continuing anyway)" % n['name']) - #node.graphicsItem().moveBy(*n['pos']) self.inputNode.restoreState(state.get('inputNode', {})) self.outputNode.restoreState(state.get('outputNode', {})) @@ -491,7 +495,6 @@ class Flowchart(Node): print(self._nodes[n1].terminals) print(self._nodes[n2].terminals) printExc("Error connecting terminals %s.%s - %s.%s:" % (n1, t1, n2, t2)) - finally: self.blockSignals(False) @@ -499,48 +502,46 @@ class Flowchart(Node): self.sigChartLoaded.emit() self.outputChanged() self.sigStateChanged.emit() - #self.sigOutputChanged.emit() def loadFile(self, fileName=None, startDir=None): + """Load a flowchart (*.fc) file. + """ if fileName is None: if startDir is None: startDir = self.filePath if startDir is None: startDir = '.' self.fileDialog = FileDialog(None, "Load Flowchart..", startDir, "Flowchart (*.fc)") - #self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile) - #self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) self.fileDialog.show() self.fileDialog.fileSelected.connect(self.loadFile) return ## NOTE: was previously using a real widget for the file dialog's parent, but this caused weird mouse event bugs.. - #fileName = QtGui.QFileDialog.getOpenFileName(None, "Load Flowchart..", startDir, "Flowchart (*.fc)") fileName = unicode(fileName) state = configfile.readConfigFile(fileName) self.restoreState(state, clear=True) self.viewBox.autoRange() - #self.emit(QtCore.SIGNAL('fileLoaded'), fileName) self.sigFileLoaded.emit(fileName) def saveFile(self, fileName=None, startDir=None, suggestedFileName='flowchart.fc'): + """Save this flowchart to a .fc file + """ if fileName is None: if startDir is None: startDir = self.filePath if startDir is None: startDir = '.' self.fileDialog = FileDialog(None, "Save Flowchart..", startDir, "Flowchart (*.fc)") - #self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile) self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) - #self.fileDialog.setDirectory(startDir) self.fileDialog.show() self.fileDialog.fileSelected.connect(self.saveFile) return - #fileName = QtGui.QFileDialog.getSaveFileName(None, "Save Flowchart..", startDir, "Flowchart (*.fc)") fileName = unicode(fileName) configfile.writeConfigFile(self.saveState(), fileName) self.sigFileSaved.emit(fileName) def clear(self): + """Remove all nodes from this flowchart except the original input/output nodes. + """ for n in list(self._nodes.values()): if n is self.inputNode or n is self.outputNode: continue @@ -553,18 +554,15 @@ class Flowchart(Node): self.inputNode.clearTerminals() self.outputNode.clearTerminals() -#class FlowchartGraphicsItem(QtGui.QGraphicsItem): + class FlowchartGraphicsItem(GraphicsObject): def __init__(self, chart): - #print "FlowchartGraphicsItem.__init__" - #QtGui.QGraphicsItem.__init__(self) GraphicsObject.__init__(self) self.chart = chart ## chart is an instance of Flowchart() self.updateTerminals() def updateTerminals(self): - #print "FlowchartGraphicsItem.updateTerminals" self.terminals = {} bounds = self.boundingRect() inp = self.chart.inputs() @@ -760,6 +758,7 @@ class FlowchartCtrlWidget(QtGui.QWidget): item = self.items[node] self.ui.ctrlList.setCurrentItem(item) + class FlowchartWidget(dockarea.DockArea): """Includes the actual graphical flowchart and debugging interface""" def __init__(self, chart, ctrl): From ee0ea5669520346e7c0ae420a6be6f935d276c1e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 15 Sep 2017 09:05:24 -0700 Subject: [PATCH 044/135] PlotItem.addLegend will not try to add more than once --- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index 41011df3..7321702c 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -602,6 +602,9 @@ class PlotItem(GraphicsWidget): #item.connect(item, QtCore.SIGNAL('plotChanged'), self.plotChanged) #item.sigPlotChanged.connect(self.plotChanged) + if self.legend is not None: + self.legend.removeItem(item) + def clear(self): """ Remove all items from the ViewBox. @@ -646,9 +649,13 @@ class PlotItem(GraphicsWidget): Create a new LegendItem and anchor it over the internal ViewBox. Plots will be automatically displayed in the legend if they are created with the 'name' argument. + + If a LegendItem has already been created using this method, that + item will be returned rather than creating a new one. """ - self.legend = LegendItem(size, offset) - self.legend.setParentItem(self.vb) + if self.legend is None: + self.legend = LegendItem(size, offset) + self.legend.setParentItem(self.vb) return self.legend def scatterPlot(self, *args, **kargs): From b88a96c08c6a8d449c4ce315513d28488b765145 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 15 Sep 2017 09:06:18 -0700 Subject: [PATCH 045/135] ViewBox: make sure transform is up to date in all mapping functions --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 8ade0c6b..e7d932d9 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -404,6 +404,7 @@ class ViewBox(GraphicsWidget): ch.setParentItem(None) def resizeEvent(self, ev): + self._matrixNeedsUpdate = True self.linkedXChanged() self.linkedYChanged() self.updateAutoRange() @@ -553,11 +554,9 @@ class ViewBox(GraphicsWidget): # Note that aspect ratio constraints and auto-visible probably do not work together.. if changed[0] and self.state['autoVisibleOnly'][1] and (self.state['autoRange'][0] is not False): self._autoRangeNeedsUpdate = True - #self.updateAutoRange() ## Maybe just indicate that auto range needs to be updated? elif changed[1] and self.state['autoVisibleOnly'][0] and (self.state['autoRange'][1] is not False): self._autoRangeNeedsUpdate = True - #self.updateAutoRange() - + def setYRange(self, min, max, padding=None, update=True): """ Set the visible Y range of the view to [*min*, *max*]. @@ -1083,35 +1082,43 @@ class ViewBox(GraphicsWidget): def mapToView(self, obj): """Maps from the local coordinates of the ViewBox to the coordinate system displayed inside the ViewBox""" + self.updateMatrix() m = fn.invertQTransform(self.childTransform()) return m.map(obj) def mapFromView(self, obj): """Maps from the coordinate system displayed inside the ViewBox to the local coordinates of the ViewBox""" + self.updateMatrix() m = self.childTransform() return m.map(obj) def mapSceneToView(self, obj): """Maps from scene coordinates to the coordinate system displayed inside the ViewBox""" + self.updateMatrix() return self.mapToView(self.mapFromScene(obj)) def mapViewToScene(self, obj): """Maps from the coordinate system displayed inside the ViewBox to scene coordinates""" + self.updateMatrix() return self.mapToScene(self.mapFromView(obj)) def mapFromItemToView(self, item, obj): """Maps *obj* from the local coordinate system of *item* to the view coordinates""" + self.updateMatrix() return self.childGroup.mapFromItem(item, obj) #return self.mapSceneToView(item.mapToScene(obj)) def mapFromViewToItem(self, item, obj): """Maps *obj* from view coordinates to the local coordinate system of *item*.""" + self.updateMatrix() return self.childGroup.mapToItem(item, obj) def mapViewToDevice(self, obj): + self.updateMatrix() return self.mapToDevice(self.mapFromView(obj)) def mapDeviceToView(self, obj): + self.updateMatrix() return self.mapToView(self.mapFromDevice(obj)) def viewPixelSize(self): From ea9e8a720b4dd79465fd8c12f6fbf8331a3b1c65 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 15 Sep 2017 09:09:45 -0700 Subject: [PATCH 046/135] ArrowItem: rotate painterpath instead of the item This makes it easier to attach text to the arrow. --- pyqtgraph/graphicsItems/ArrowItem.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/graphicsItems/ArrowItem.py b/pyqtgraph/graphicsItems/ArrowItem.py index 77e6195f..897cbc50 100644 --- a/pyqtgraph/graphicsItems/ArrowItem.py +++ b/pyqtgraph/graphicsItems/ArrowItem.py @@ -39,7 +39,6 @@ class ArrowItem(QtGui.QGraphicsPathItem): self.setStyle(**defaultOpts) - self.rotate(self.opts['angle']) self.moveBy(*self.opts['pos']) def setStyle(self, **opts): @@ -72,7 +71,10 @@ class ArrowItem(QtGui.QGraphicsPathItem): self.opts.update(opts) opt = dict([(k,self.opts[k]) for k in ['headLen', 'tipAngle', 'baseAngle', 'tailLen', 'tailWidth']]) - self.path = fn.makeArrowPath(**opt) + tr = QtGui.QTransform() + tr.rotate(self.opts['angle']) + self.path = tr.map(fn.makeArrowPath(**opt)) + self.setPath(self.path) self.setPen(fn.mkPen(self.opts['pen'])) @@ -82,7 +84,8 @@ class ArrowItem(QtGui.QGraphicsPathItem): self.setFlags(self.flags() | self.ItemIgnoresTransformations) else: self.setFlags(self.flags() & ~self.ItemIgnoresTransformations) - + + def paint(self, p, *args): p.setRenderHint(QtGui.QPainter.Antialiasing) QtGui.QGraphicsPathItem.paint(self, p, *args) From 653c91a683da40e7d0d72b70c3c6a41fcb68a903 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 15 Sep 2017 09:14:26 -0700 Subject: [PATCH 047/135] InfiniteLine: add markers and ability to limit drawing region --- pyqtgraph/graphicsItems/InfiniteLine.py | 200 +++++++++++++++++++++--- 1 file changed, 174 insertions(+), 26 deletions(-) diff --git a/pyqtgraph/graphicsItems/InfiniteLine.py b/pyqtgraph/graphicsItems/InfiniteLine.py index 3da82327..7aeb1620 100644 --- a/pyqtgraph/graphicsItems/InfiniteLine.py +++ b/pyqtgraph/graphicsItems/InfiniteLine.py @@ -31,7 +31,8 @@ class InfiniteLine(GraphicsObject): sigPositionChanged = QtCore.Signal(object) def __init__(self, pos=None, angle=90, pen=None, movable=False, bounds=None, - hoverPen=None, label=None, labelOpts=None, name=None): + hoverPen=None, label=None, labelOpts=None, span=(0, 1), markers=None, + name=None): """ =============== ================================================================== **Arguments:** @@ -41,22 +42,28 @@ class InfiniteLine(GraphicsObject): pen Pen to use when drawing line. Can be any arguments that are valid for :func:`mkPen `. Default pen is transparent yellow. + hoverPen Pen to use when the mouse cursor hovers over the line. + Only used when movable=True. movable If True, the line can be dragged to a new position by the user. + bounds Optional [min, max] bounding values. Bounds are only valid if the + line is vertical or horizontal. hoverPen Pen to use when drawing line when hovering over it. Can be any arguments that are valid for :func:`mkPen `. Default pen is red. - bounds Optional [min, max] bounding values. Bounds are only valid if the - line is vertical or horizontal. label Text to be displayed in a label attached to the line, or None to show no label (default is None). May optionally include formatting strings to display the line value. labelOpts A dict of keyword arguments to use when constructing the text label. See :class:`InfLineLabel`. + span Optional tuple (min, max) giving the range over the view to draw + the line. For example, with a vertical line, use span=(0.5, 1) + to draw only on the top half of the view. + markers List of (marker, position, size) tuples, one per marker to display + on the line. See the addMarker method. name Name of the item =============== ================================================================== """ self._boundingRect = None - self._line = None self._name = name @@ -79,11 +86,25 @@ class InfiniteLine(GraphicsObject): if pen is None: pen = (200, 200, 100) self.setPen(pen) + if hoverPen is None: self.setHoverPen(color=(255,0,0), width=self.pen.width()) else: self.setHoverPen(hoverPen) + + self.span = span self.currentPen = self.pen + + self.markers = [] + self._maxMarkerSize = 0 + if markers is not None: + for m in markers: + self.addMarker(*m) + + # Cache variables for managing bounds + self._endPoints = [0, 1] # + self._bounds = None + self._lastViewSize = None if label is not None: labelOpts = {} if labelOpts is None else labelOpts @@ -98,7 +119,12 @@ class InfiniteLine(GraphicsObject): """Set the (minimum, maximum) allowable values when dragging.""" self.maxRange = bounds self.setValue(self.value()) - + + def bounds(self): + """Return the (minimum, maximum) values allowed when dragging. + """ + return self.maxRange[:] + def setPen(self, *args, **kwargs): """Set the pen for drawing the line. Allowable arguments are any that are valid for :func:`mkPen `.""" @@ -115,11 +141,70 @@ class InfiniteLine(GraphicsObject): If the line is not movable, then hovering is also disabled. Added in version 0.9.9.""" + # If user did not supply a width, then copy it from pen + widthSpecified = ((len(args) == 1 and + (isinstance(args[0], QtGui.QPen) or + (isinstance(args[0], dict) and 'width' in args[0])) + ) or 'width' in kwargs) self.hoverPen = fn.mkPen(*args, **kwargs) + if not widthSpecified: + self.hoverPen.setWidth(self.pen.width()) + if self.mouseHovering: self.currentPen = self.hoverPen self.update() + + def addMarker(self, marker, position=0.5, size=10.0): + """Add a marker to be displayed on the line. + + ============= ========================================================= + **Arguments** + marker String indicating the style of marker to add: + '<|', '|>', '>|', '|<', '<|>', '>|<', '^', 'v', 'o' + position Position (0.0-1.0) along the visible extent of the line + to place the marker. Default is 0.5. + size Size of the marker in pixels. Default is 10.0. + ============= ========================================================= + """ + path = QtGui.QPainterPath() + if marker == 'o': + path.addEllipse(QtCore.QRectF(-0.5, -0.5, 1, 1)) + if '<|' in marker: + p = QtGui.QPolygonF([Point(0.5, 0), Point(0, -0.5), Point(-0.5, 0)]) + path.addPolygon(p) + path.closeSubpath() + if '|>' in marker: + p = QtGui.QPolygonF([Point(0.5, 0), Point(0, 0.5), Point(-0.5, 0)]) + path.addPolygon(p) + path.closeSubpath() + if '>|' in marker: + p = QtGui.QPolygonF([Point(0.5, -0.5), Point(0, 0), Point(-0.5, -0.5)]) + path.addPolygon(p) + path.closeSubpath() + if '|<' in marker: + p = QtGui.QPolygonF([Point(0.5, 0.5), Point(0, 0), Point(-0.5, 0.5)]) + path.addPolygon(p) + path.closeSubpath() + if '^' in marker: + p = QtGui.QPolygonF([Point(0, -0.5), Point(0.5, 0), Point(0, 0.5)]) + path.addPolygon(p) + path.closeSubpath() + if 'v' in marker: + p = QtGui.QPolygonF([Point(0, -0.5), Point(-0.5, 0), Point(0, 0.5)]) + path.addPolygon(p) + path.closeSubpath() + + self.markers.append((path, position, size)) + self._maxMarkerSize = max([m[2] / 2. for m in self.markers]) + self.update() + def clearMarkers(self): + """ Remove all markers from this line. + """ + self.markers = [] + self._maxMarkerSize = 0 + self.update() + def setAngle(self, angle): """ Takes angle argument in degrees. @@ -128,7 +213,7 @@ class InfiniteLine(GraphicsObject): Note that the use of value() and setValue() changes if the line is not vertical or horizontal. """ - self.angle = ((angle+45) % 180) - 45 ## -45 <= angle < 135 + self.angle = angle #((angle+45) % 180) - 45 ## -45 <= angle < 135 self.resetTransform() self.rotate(self.angle) self.update() @@ -199,35 +284,98 @@ class InfiniteLine(GraphicsObject): #else: #print "ignore", change #return GraphicsObject.itemChange(self, change, val) + + def setSpan(self, mn, mx): + if self.span != (mn, mx): + self.span = (mn, mx) + self.update() def _invalidateCache(self): - self._line = None self._boundingRect = None + def _computeBoundingRect(self): + #br = UIGraphicsItem.boundingRect(self) + vr = self.viewRect() # bounds of containing ViewBox mapped to local coords. + if vr is None: + return QtCore.QRectF() + + ## add a 4-pixel radius around the line for mouse interaction. + + px = self.pixelLength(direction=Point(1,0), ortho=True) ## get pixel length orthogonal to the line + if px is None: + px = 0 + pw = max(self.pen.width() / 2, self.hoverPen.width() / 2) + w = max(4, self._maxMarkerSize + pw) + 1 + w = w * px + br = QtCore.QRectF(vr) + br.setBottom(-w) + br.setTop(w) + + length = br.width() + left = br.left() + length * self.span[0] + right = br.left() + length * self.span[1] + br.setLeft(left - w) + br.setRight(right + w) + br = br.normalized() + + vs = self.getViewBox().size() + + if self._bounds != br or self._lastViewSize != vs: + self._bounds = br + self._lastViewSize = vs + self.prepareGeometryChange() + + self._endPoints = (left, right) + self._lastViewRect = vr + + return self._bounds + def boundingRect(self): if self._boundingRect is None: - #br = UIGraphicsItem.boundingRect(self) - br = self.viewRect() - if br is None: - return QtCore.QRectF() - - ## add a 4-pixel radius around the line for mouse interaction. - px = self.pixelLength(direction=Point(1,0), ortho=True) ## get pixel length orthogonal to the line - if px is None: - px = 0 - w = (max(4, self.pen.width()/2, self.hoverPen.width()/2)+1) * px - br.setBottom(-w) - br.setTop(w) - - br = br.normalized() - self._boundingRect = br - self._line = QtCore.QLineF(br.right(), 0.0, br.left(), 0.0) + self._boundingRect = self._computeBoundingRect() return self._boundingRect def paint(self, p, *args): - p.setPen(self.currentPen) - p.drawLine(self._line) - + p.setRenderHint(p.Antialiasing) + + left, right = self._endPoints + pen = self.currentPen + pen.setJoinStyle(QtCore.Qt.MiterJoin) + p.setPen(pen) + p.drawLine(Point(left, 0), Point(right, 0)) + + + if len(self.markers) == 0: + return + + # paint markers in native coordinate system + tr = p.transform() + p.resetTransform() + + start = tr.map(Point(left, 0)) + end = tr.map(Point(right, 0)) + up = tr.map(Point(left, 1)) + dif = end - start + length = Point(dif).length() + angle = np.arctan2(dif.y(), dif.x()) * 180 / np.pi + + p.translate(start) + p.rotate(angle) + + up = up - start + det = up.x() * dif.y() - dif.x() * up.y() + p.scale(1, 1 if det > 0 else -1) + + p.setBrush(fn.mkBrush(self.currentPen.color())) + #p.setPen(fn.mkPen(None)) + tr = p.transform() + for path, pos, size in self.markers: + p.setTransform(tr) + x = length * pos + p.translate(x, 0) + p.scale(size, size) + p.drawPath(path) + def dataBounds(self, axis, frac=1.0, orthoRange=None): if axis == 0: return None ## x axis should never be auto-scaled From 4beea8a153b98a24740132225a62f0ecae6d41d4 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 18 Sep 2017 13:31:32 -0700 Subject: [PATCH 048/135] Prevent viewbox auto-scaling to items that are not in the same scene. This can happen when an item that was previously added to the viewbox is then removed using scene.removeItem(). --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 8ade0c6b..c125babf 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -1280,7 +1280,7 @@ class ViewBox(GraphicsWidget): ## First collect all boundary information itemBounds = [] for item in items: - if not item.isVisible(): + if not item.isVisible() or not item.scene() is self.scene(): continue useX = True From fe1dff5ad196226a96824e18dfb616f9a75962e8 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 19 Sep 2017 09:51:17 -0700 Subject: [PATCH 049/135] Allow console exception label to wrap text This prevents the console window from growing if the exception message contains a very long line --- pyqtgraph/console/template.ui | 5 +++- pyqtgraph/console/template_pyqt.py | 9 +++--- pyqtgraph/console/template_pyqt5.py | 41 ++++++++++++++++------------ pyqtgraph/console/template_pyside.py | 39 ++++++++++++++++---------- 4 files changed, 56 insertions(+), 38 deletions(-) diff --git a/pyqtgraph/console/template.ui b/pyqtgraph/console/template.ui index 1dd752d9..1237b5f3 100644 --- a/pyqtgraph/console/template.ui +++ b/pyqtgraph/console/template.ui @@ -6,7 +6,7 @@ 0 0 - 694 + 739 497 @@ -154,6 +154,9 @@ Stack Trace + + true + diff --git a/pyqtgraph/console/template_pyqt.py b/pyqtgraph/console/template_pyqt.py index e5fc4619..9b39d14a 100644 --- a/pyqtgraph/console/template_pyqt.py +++ b/pyqtgraph/console/template_pyqt.py @@ -1,9 +1,8 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'template.ui' +# Form implementation generated from reading ui file 'pyqtgraph/console/template.ui' # -# Created: Wed Apr 08 16:28:53 2015 -# by: PyQt4 UI code generator 4.10.4 +# Created by: PyQt4 UI code generator 4.11.4 # # WARNING! All changes made in this file will be lost! @@ -26,7 +25,7 @@ except AttributeError: class Ui_Form(object): def setupUi(self, Form): Form.setObjectName(_fromUtf8("Form")) - Form.resize(694, 497) + Form.resize(739, 497) self.gridLayout = QtGui.QGridLayout(Form) self.gridLayout.setMargin(0) self.gridLayout.setSpacing(0) @@ -37,7 +36,6 @@ class Ui_Form(object): self.layoutWidget = QtGui.QWidget(self.splitter) self.layoutWidget.setObjectName(_fromUtf8("layoutWidget")) self.verticalLayout = QtGui.QVBoxLayout(self.layoutWidget) - self.verticalLayout.setMargin(0) self.verticalLayout.setObjectName(_fromUtf8("verticalLayout")) self.output = QtGui.QPlainTextEdit(self.layoutWidget) font = QtGui.QFont() @@ -97,6 +95,7 @@ class Ui_Form(object): self.runSelectedFrameCheck.setObjectName(_fromUtf8("runSelectedFrameCheck")) self.gridLayout_2.addWidget(self.runSelectedFrameCheck, 3, 0, 1, 7) self.exceptionInfoLabel = QtGui.QLabel(self.exceptionGroup) + self.exceptionInfoLabel.setWordWrap(True) self.exceptionInfoLabel.setObjectName(_fromUtf8("exceptionInfoLabel")) self.gridLayout_2.addWidget(self.exceptionInfoLabel, 1, 0, 1, 7) spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) diff --git a/pyqtgraph/console/template_pyqt5.py b/pyqtgraph/console/template_pyqt5.py index 1fbc5bed..c8c2cbac 100644 --- a/pyqtgraph/console/template_pyqt5.py +++ b/pyqtgraph/console/template_pyqt5.py @@ -1,9 +1,8 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file './pyqtgraph/console/template.ui' +# Form implementation generated from reading ui file 'pyqtgraph/console/template.ui' # -# Created: Wed Mar 26 15:09:29 2014 -# by: PyQt5 UI code generator 5.0.1 +# Created by: PyQt5 UI code generator 5.5.1 # # WARNING! All changes made in this file will be lost! @@ -12,7 +11,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets class Ui_Form(object): def setupUi(self, Form): Form.setObjectName("Form") - Form.resize(710, 497) + Form.resize(739, 497) self.gridLayout = QtWidgets.QGridLayout(Form) self.gridLayout.setContentsMargins(0, 0, 0, 0) self.gridLayout.setSpacing(0) @@ -23,7 +22,6 @@ class Ui_Form(object): self.layoutWidget = QtWidgets.QWidget(self.splitter) self.layoutWidget.setObjectName("layoutWidget") self.verticalLayout = QtWidgets.QVBoxLayout(self.layoutWidget) - self.verticalLayout.setContentsMargins(0, 0, 0, 0) self.verticalLayout.setObjectName("verticalLayout") self.output = QtWidgets.QPlainTextEdit(self.layoutWidget) font = QtGui.QFont() @@ -54,9 +52,14 @@ class Ui_Form(object): self.exceptionGroup = QtWidgets.QGroupBox(self.splitter) self.exceptionGroup.setObjectName("exceptionGroup") self.gridLayout_2 = QtWidgets.QGridLayout(self.exceptionGroup) - self.gridLayout_2.setSpacing(0) self.gridLayout_2.setContentsMargins(-1, 0, -1, 0) + self.gridLayout_2.setHorizontalSpacing(2) + self.gridLayout_2.setVerticalSpacing(0) self.gridLayout_2.setObjectName("gridLayout_2") + self.clearExceptionBtn = QtWidgets.QPushButton(self.exceptionGroup) + self.clearExceptionBtn.setEnabled(False) + self.clearExceptionBtn.setObjectName("clearExceptionBtn") + self.gridLayout_2.addWidget(self.clearExceptionBtn, 0, 6, 1, 1) self.catchAllExceptionsBtn = QtWidgets.QPushButton(self.exceptionGroup) self.catchAllExceptionsBtn.setCheckable(True) self.catchAllExceptionsBtn.setObjectName("catchAllExceptionsBtn") @@ -68,24 +71,27 @@ class Ui_Form(object): self.onlyUncaughtCheck = QtWidgets.QCheckBox(self.exceptionGroup) self.onlyUncaughtCheck.setChecked(True) self.onlyUncaughtCheck.setObjectName("onlyUncaughtCheck") - self.gridLayout_2.addWidget(self.onlyUncaughtCheck, 0, 2, 1, 1) + self.gridLayout_2.addWidget(self.onlyUncaughtCheck, 0, 4, 1, 1) self.exceptionStackList = QtWidgets.QListWidget(self.exceptionGroup) self.exceptionStackList.setAlternatingRowColors(True) self.exceptionStackList.setObjectName("exceptionStackList") - self.gridLayout_2.addWidget(self.exceptionStackList, 2, 0, 1, 5) + self.gridLayout_2.addWidget(self.exceptionStackList, 2, 0, 1, 7) self.runSelectedFrameCheck = QtWidgets.QCheckBox(self.exceptionGroup) self.runSelectedFrameCheck.setChecked(True) self.runSelectedFrameCheck.setObjectName("runSelectedFrameCheck") - self.gridLayout_2.addWidget(self.runSelectedFrameCheck, 3, 0, 1, 5) + self.gridLayout_2.addWidget(self.runSelectedFrameCheck, 3, 0, 1, 7) self.exceptionInfoLabel = QtWidgets.QLabel(self.exceptionGroup) + self.exceptionInfoLabel.setWordWrap(True) self.exceptionInfoLabel.setObjectName("exceptionInfoLabel") - self.gridLayout_2.addWidget(self.exceptionInfoLabel, 1, 0, 1, 5) - self.clearExceptionBtn = QtWidgets.QPushButton(self.exceptionGroup) - self.clearExceptionBtn.setEnabled(False) - self.clearExceptionBtn.setObjectName("clearExceptionBtn") - self.gridLayout_2.addWidget(self.clearExceptionBtn, 0, 4, 1, 1) + self.gridLayout_2.addWidget(self.exceptionInfoLabel, 1, 0, 1, 7) spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.gridLayout_2.addItem(spacerItem, 0, 3, 1, 1) + self.gridLayout_2.addItem(spacerItem, 0, 5, 1, 1) + self.label = QtWidgets.QLabel(self.exceptionGroup) + self.label.setObjectName("label") + self.gridLayout_2.addWidget(self.label, 0, 2, 1, 1) + self.filterText = QtWidgets.QLineEdit(self.exceptionGroup) + self.filterText.setObjectName("filterText") + self.gridLayout_2.addWidget(self.filterText, 0, 3, 1, 1) self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1) self.retranslateUi(Form) @@ -97,11 +103,12 @@ class Ui_Form(object): self.historyBtn.setText(_translate("Form", "History..")) self.exceptionBtn.setText(_translate("Form", "Exceptions..")) self.exceptionGroup.setTitle(_translate("Form", "Exception Handling")) + self.clearExceptionBtn.setText(_translate("Form", "Clear Stack")) self.catchAllExceptionsBtn.setText(_translate("Form", "Show All Exceptions")) self.catchNextExceptionBtn.setText(_translate("Form", "Show Next Exception")) self.onlyUncaughtCheck.setText(_translate("Form", "Only Uncaught Exceptions")) self.runSelectedFrameCheck.setText(_translate("Form", "Run commands in selected stack frame")) - self.exceptionInfoLabel.setText(_translate("Form", "Exception Info")) - self.clearExceptionBtn.setText(_translate("Form", "Clear Exception")) + self.exceptionInfoLabel.setText(_translate("Form", "Stack Trace")) + self.label.setText(_translate("Form", "Filter (regex):")) from .CmdInput import CmdInput diff --git a/pyqtgraph/console/template_pyside.py b/pyqtgraph/console/template_pyside.py index 36065afd..1579cb1f 100644 --- a/pyqtgraph/console/template_pyside.py +++ b/pyqtgraph/console/template_pyside.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file './pyqtgraph/console/template.ui' +# Form implementation generated from reading ui file 'pyqtgraph/console/template.ui' # -# Created: Mon Dec 23 10:10:53 2013 -# by: pyside-uic 0.2.14 running on PySide 1.1.2 +# Created: Tue Sep 19 09:45:18 2017 +# by: pyside-uic 0.2.15 running on PySide 1.2.2 # # WARNING! All changes made in this file will be lost! @@ -12,7 +12,7 @@ from PySide import QtCore, QtGui class Ui_Form(object): def setupUi(self, Form): Form.setObjectName("Form") - Form.resize(710, 497) + Form.resize(739, 497) self.gridLayout = QtGui.QGridLayout(Form) self.gridLayout.setContentsMargins(0, 0, 0, 0) self.gridLayout.setSpacing(0) @@ -54,9 +54,14 @@ class Ui_Form(object): self.exceptionGroup = QtGui.QGroupBox(self.splitter) self.exceptionGroup.setObjectName("exceptionGroup") self.gridLayout_2 = QtGui.QGridLayout(self.exceptionGroup) - self.gridLayout_2.setSpacing(0) self.gridLayout_2.setContentsMargins(-1, 0, -1, 0) + self.gridLayout_2.setHorizontalSpacing(2) + self.gridLayout_2.setVerticalSpacing(0) self.gridLayout_2.setObjectName("gridLayout_2") + self.clearExceptionBtn = QtGui.QPushButton(self.exceptionGroup) + self.clearExceptionBtn.setEnabled(False) + self.clearExceptionBtn.setObjectName("clearExceptionBtn") + self.gridLayout_2.addWidget(self.clearExceptionBtn, 0, 6, 1, 1) self.catchAllExceptionsBtn = QtGui.QPushButton(self.exceptionGroup) self.catchAllExceptionsBtn.setCheckable(True) self.catchAllExceptionsBtn.setObjectName("catchAllExceptionsBtn") @@ -68,24 +73,27 @@ class Ui_Form(object): self.onlyUncaughtCheck = QtGui.QCheckBox(self.exceptionGroup) self.onlyUncaughtCheck.setChecked(True) self.onlyUncaughtCheck.setObjectName("onlyUncaughtCheck") - self.gridLayout_2.addWidget(self.onlyUncaughtCheck, 0, 2, 1, 1) + self.gridLayout_2.addWidget(self.onlyUncaughtCheck, 0, 4, 1, 1) self.exceptionStackList = QtGui.QListWidget(self.exceptionGroup) self.exceptionStackList.setAlternatingRowColors(True) self.exceptionStackList.setObjectName("exceptionStackList") - self.gridLayout_2.addWidget(self.exceptionStackList, 2, 0, 1, 5) + self.gridLayout_2.addWidget(self.exceptionStackList, 2, 0, 1, 7) self.runSelectedFrameCheck = QtGui.QCheckBox(self.exceptionGroup) self.runSelectedFrameCheck.setChecked(True) self.runSelectedFrameCheck.setObjectName("runSelectedFrameCheck") - self.gridLayout_2.addWidget(self.runSelectedFrameCheck, 3, 0, 1, 5) + self.gridLayout_2.addWidget(self.runSelectedFrameCheck, 3, 0, 1, 7) self.exceptionInfoLabel = QtGui.QLabel(self.exceptionGroup) + self.exceptionInfoLabel.setWordWrap(True) self.exceptionInfoLabel.setObjectName("exceptionInfoLabel") - self.gridLayout_2.addWidget(self.exceptionInfoLabel, 1, 0, 1, 5) - self.clearExceptionBtn = QtGui.QPushButton(self.exceptionGroup) - self.clearExceptionBtn.setEnabled(False) - self.clearExceptionBtn.setObjectName("clearExceptionBtn") - self.gridLayout_2.addWidget(self.clearExceptionBtn, 0, 4, 1, 1) + self.gridLayout_2.addWidget(self.exceptionInfoLabel, 1, 0, 1, 7) spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) - self.gridLayout_2.addItem(spacerItem, 0, 3, 1, 1) + self.gridLayout_2.addItem(spacerItem, 0, 5, 1, 1) + self.label = QtGui.QLabel(self.exceptionGroup) + self.label.setObjectName("label") + self.gridLayout_2.addWidget(self.label, 0, 2, 1, 1) + self.filterText = QtGui.QLineEdit(self.exceptionGroup) + self.filterText.setObjectName("filterText") + self.gridLayout_2.addWidget(self.filterText, 0, 3, 1, 1) self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1) self.retranslateUi(Form) @@ -96,11 +104,12 @@ class Ui_Form(object): self.historyBtn.setText(QtGui.QApplication.translate("Form", "History..", None, QtGui.QApplication.UnicodeUTF8)) self.exceptionBtn.setText(QtGui.QApplication.translate("Form", "Exceptions..", None, QtGui.QApplication.UnicodeUTF8)) self.exceptionGroup.setTitle(QtGui.QApplication.translate("Form", "Exception Handling", None, QtGui.QApplication.UnicodeUTF8)) + self.clearExceptionBtn.setText(QtGui.QApplication.translate("Form", "Clear Stack", None, QtGui.QApplication.UnicodeUTF8)) self.catchAllExceptionsBtn.setText(QtGui.QApplication.translate("Form", "Show All Exceptions", None, QtGui.QApplication.UnicodeUTF8)) self.catchNextExceptionBtn.setText(QtGui.QApplication.translate("Form", "Show Next Exception", None, QtGui.QApplication.UnicodeUTF8)) self.onlyUncaughtCheck.setText(QtGui.QApplication.translate("Form", "Only Uncaught Exceptions", None, QtGui.QApplication.UnicodeUTF8)) self.runSelectedFrameCheck.setText(QtGui.QApplication.translate("Form", "Run commands in selected stack frame", None, QtGui.QApplication.UnicodeUTF8)) self.exceptionInfoLabel.setText(QtGui.QApplication.translate("Form", "Stack Trace", None, QtGui.QApplication.UnicodeUTF8)) - self.clearExceptionBtn.setText(QtGui.QApplication.translate("Form", "Clear Stack", None, QtGui.QApplication.UnicodeUTF8)) + self.label.setText(QtGui.QApplication.translate("Form", "Filter (regex):", None, QtGui.QApplication.UnicodeUTF8)) from .CmdInput import CmdInput From 98cdc65049a8b61169982bb7add833f923979bf4 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 21 Sep 2017 09:04:06 -0700 Subject: [PATCH 050/135] Update LinearRegionItem to support new InfiniteLine features Also add methods for setting hover brush and configurable line swap behavior (block/push/sort) --- pyqtgraph/graphicsItems/LinearRegionItem.py | 222 ++++++++++---------- 1 file changed, 114 insertions(+), 108 deletions(-) diff --git a/pyqtgraph/graphicsItems/LinearRegionItem.py b/pyqtgraph/graphicsItems/LinearRegionItem.py index e139190b..9903dac5 100644 --- a/pyqtgraph/graphicsItems/LinearRegionItem.py +++ b/pyqtgraph/graphicsItems/LinearRegionItem.py @@ -1,14 +1,14 @@ from ..Qt import QtGui, QtCore -from .UIGraphicsItem import UIGraphicsItem +from .GraphicsObject import GraphicsObject from .InfiniteLine import InfiniteLine from .. import functions as fn from .. import debug as debug __all__ = ['LinearRegionItem'] -class LinearRegionItem(UIGraphicsItem): +class LinearRegionItem(GraphicsObject): """ - **Bases:** :class:`UIGraphicsItem ` + **Bases:** :class:`GraphicsObject ` Used for marking a horizontal or vertical region in plots. The region can be dragged and is bounded by lines which can be dragged individually. @@ -26,65 +26,110 @@ class LinearRegionItem(UIGraphicsItem): sigRegionChanged = QtCore.Signal(object) Vertical = 0 Horizontal = 1 + _orientation_axis = { + Vertical: 0, + Horizontal: 1, + 'vertical': 0, + 'horizontal': 1, + } - def __init__(self, values=[0,1], orientation=None, brush=None, movable=True, bounds=None): + def __init__(self, values=(0, 1), orientation='vertical', brush=None, pen=None, + hoverBrush=None, hoverPen=None, movable=True, bounds=None, + span=(0, 1), swapMode='sort'): """Create a new LinearRegionItem. ============== ===================================================================== **Arguments:** values A list of the positions of the lines in the region. These are not limits; limits can be set by specifying bounds. - orientation Options are LinearRegionItem.Vertical or LinearRegionItem.Horizontal. - If not specified it will be vertical. + orientation Options are 'vertical' or 'horizontal', indicating the + The default is 'vertical', indicating that the brush Defines the brush that fills the region. Can be any arguments that are valid for :func:`mkBrush `. Default is transparent blue. + pen The pen to use when drawing the lines that bound the region. + hoverBrush The brush to use when the mouse is hovering over the region. + hoverPen The pen to use when the mouse is hovering over the region. movable If True, the region and individual lines are movable by the user; if False, they are static. bounds Optional [min, max] bounding values for the region + span Optional [min, max] giving the range over the view to draw + the region. For example, with a vertical line, use span=(0.5, 1) + to draw only on the top half of the view. + swapMode Sets the behavior of the region when the lines are moved such that + their order reverses: + * "block" means the user cannot drag one line past the other + * "push" causes both lines to be moved if one would cross the other + * "sort" means that lines may trade places, but the output of + getRegion always gives the line positions in ascending order. + * None means that no attempt is made to handle swapped line + positions. + The default is "sort". ============== ===================================================================== """ - UIGraphicsItem.__init__(self) - if orientation is None: - orientation = LinearRegionItem.Vertical + GraphicsObject.__init__(self) self.orientation = orientation self.bounds = QtCore.QRectF() self.blockLineSignal = False self.moving = False self.mouseHovering = False + self.span = span + self.swapMode = swapMode + self._bounds = None - if orientation == LinearRegionItem.Horizontal: + # note LinearRegionItem.Horizontal and LinearRegionItem.Vertical + # are kept for backward compatibility. + lineKwds = dict( + movable=movable, + bounds=bounds, + span=span, + pen=pen, + hoverPen=hoverPen, + ) + + if orientation in ('horizontal', LinearRegionItem.Horizontal): self.lines = [ - InfiniteLine(QtCore.QPointF(0, values[0]), 0, movable=movable, bounds=bounds), - InfiniteLine(QtCore.QPointF(0, values[1]), 0, movable=movable, bounds=bounds)] - elif orientation == LinearRegionItem.Vertical: + # rotate lines to 180 to preserve expected line orientation + # with respect to region. This ensures that placing a '<|' + # marker on lines[0] causes it to point left in vertical mode + # and down in horizontal mode. + InfiniteLine(QtCore.QPointF(0, values[0]), angle=0, **lineKwds), + InfiniteLine(QtCore.QPointF(0, values[1]), angle=0, **lineKwds)] + self.lines[0].scale(1, -1) + self.lines[1].scale(1, -1) + elif orientation in ('vertical', LinearRegionItem.Vertical): self.lines = [ - InfiniteLine(QtCore.QPointF(values[1], 0), 90, movable=movable, bounds=bounds), - InfiniteLine(QtCore.QPointF(values[0], 0), 90, movable=movable, bounds=bounds)] + InfiniteLine(QtCore.QPointF(values[0], 0), angle=90, **lineKwds), + InfiniteLine(QtCore.QPointF(values[1], 0), angle=90, **lineKwds)] else: - raise Exception('Orientation must be one of LinearRegionItem.Vertical or LinearRegionItem.Horizontal') - + raise Exception("Orientation must be 'vertical' or 'horizontal'.") for l in self.lines: l.setParentItem(self) l.sigPositionChangeFinished.connect(self.lineMoveFinished) - l.sigPositionChanged.connect(self.lineMoved) + self.lines[0].sigPositionChanged.connect(lambda: self.lineMoved(0)) + self.lines[1].sigPositionChanged.connect(lambda: self.lineMoved(1)) if brush is None: brush = QtGui.QBrush(QtGui.QColor(0, 0, 255, 50)) self.setBrush(brush) + if hoverBrush is None: + c = self.brush.color() + c.setAlpha(min(c.alpha() * 2, 255)) + hoverBrush = fn.mkBrush(c) + self.setHoverBrush(hoverBrush) + self.setMovable(movable) def getRegion(self): """Return the values at the edges of the region.""" - #if self.orientation[0] == 'h': - #r = (self.bounds.top(), self.bounds.bottom()) - #else: - #r = (self.bounds.left(), self.bounds.right()) - r = [self.lines[0].value(), self.lines[1].value()] - return (min(r), max(r)) + r = (self.lines[0].value(), self.lines[1].value()) + if self.swapMode == 'sort': + return (min(r), max(r)) + else: + return r def setRegion(self, rgn): """Set the values for the edges of the region. @@ -101,7 +146,8 @@ class LinearRegionItem(UIGraphicsItem): self.blockLineSignal = False self.lines[1].setValue(rgn[1]) #self.blockLineSignal = False - self.lineMoved() + self.lineMoved(0) + self.lineMoved(1) self.lineMoveFinished() def setBrush(self, *br, **kargs): @@ -111,6 +157,13 @@ class LinearRegionItem(UIGraphicsItem): self.brush = fn.mkBrush(*br, **kargs) self.currentBrush = self.brush + def setHoverBrush(self, *br, **kargs): + """Set the brush that fills the region when the mouse is hovering over. + Can have any arguments that are valid + for :func:`mkBrush `. + """ + self.hoverBrush = fn.mkBrush(*br, **kargs) + def setBounds(self, bounds): """Optional [min, max] bounding values for the region. To have no bounds on the region use [None, None]. @@ -128,81 +181,67 @@ class LinearRegionItem(UIGraphicsItem): self.movable = m self.setAcceptHoverEvents(m) + def setSpan(self, mn, mx): + if self.span == (mn, mx): + return + self.span = (mn, mx) + self.lines[0].setSpan(mn, mx) + self.lines[1].setSpan(mn, mx) + self.update() + def boundingRect(self): - br = UIGraphicsItem.boundingRect(self) + br = self.viewRect() # bounds of containing ViewBox mapped to local coords. + rng = self.getRegion() - if self.orientation == LinearRegionItem.Vertical: + if self.orientation in ('vertical', LinearRegionItem.Vertical): br.setLeft(rng[0]) br.setRight(rng[1]) + length = br.height() + br.setBottom(br.top() + length * self.span[1]) + br.setTop(br.top() + length * self.span[0]) else: br.setTop(rng[0]) br.setBottom(rng[1]) - return br.normalized() + length = br.width() + br.setRight(br.left() + length * self.span[1]) + br.setLeft(br.left() + length * self.span[0]) + + br = br.normalized() + + if self._bounds != br: + self._bounds = br + self.prepareGeometryChange() + + return br def paint(self, p, *args): profiler = debug.Profiler() - UIGraphicsItem.paint(self, p, *args) p.setBrush(self.currentBrush) p.setPen(fn.mkPen(None)) p.drawRect(self.boundingRect()) def dataBounds(self, axis, frac=1.0, orthoRange=None): - if axis == self.orientation: + if axis == self._orientation_axis[self.orientation]: return self.getRegion() else: return None - def lineMoved(self): + def lineMoved(self, i): if self.blockLineSignal: return + + # lines swapped + if self.lines[0].value() > self.lines[1].value(): + if self.swapMode == 'block': + self.lines[i].setValue(self.lines[1-i].value()) + elif self.swapMode == 'push': + self.lines[1-i].setValue(self.lines[i].value()) + self.prepareGeometryChange() - #self.emit(QtCore.SIGNAL('regionChanged'), self) self.sigRegionChanged.emit(self) def lineMoveFinished(self): - #self.emit(QtCore.SIGNAL('regionChangeFinished'), self) self.sigRegionChangeFinished.emit(self) - - - #def updateBounds(self): - #vb = self.view().viewRect() - #vals = [self.lines[0].value(), self.lines[1].value()] - #if self.orientation[0] == 'h': - #vb.setTop(min(vals)) - #vb.setBottom(max(vals)) - #else: - #vb.setLeft(min(vals)) - #vb.setRight(max(vals)) - #if vb != self.bounds: - #self.bounds = vb - #self.rect.setRect(vb) - - #def mousePressEvent(self, ev): - #if not self.movable: - #ev.ignore() - #return - #for l in self.lines: - #l.mousePressEvent(ev) ## pass event to both lines so they move together - ##if self.movable and ev.button() == QtCore.Qt.LeftButton: - ##ev.accept() - ##self.pressDelta = self.mapToParent(ev.pos()) - QtCore.QPointF(*self.p) - ##else: - ##ev.ignore() - - #def mouseReleaseEvent(self, ev): - #for l in self.lines: - #l.mouseReleaseEvent(ev) - - #def mouseMoveEvent(self, ev): - ##print "move", ev.pos() - #if not self.movable: - #return - #self.lines[0].blockSignals(True) # only want to update once - #for l in self.lines: - #l.mouseMoveEvent(ev) - #self.lines[0].blockSignals(False) - ##self.setPos(self.mapToParent(ev.pos()) - self.pressDelta) - ##self.emit(QtCore.SIGNAL('dragged'), self) def mouseDragEvent(self, ev): if not self.movable or int(ev.button() & QtCore.Qt.LeftButton) == 0: @@ -218,12 +257,9 @@ class LinearRegionItem(UIGraphicsItem): if not self.moving: return - #delta = ev.pos() - ev.lastPos() self.lines[0].blockSignals(True) # only want to update once for i, l in enumerate(self.lines): l.setPos(self.cursorOffsets[i] + ev.pos()) - #l.setPos(l.pos()+delta) - #l.mouseDragEvent(ev) self.lines[0].blockSignals(False) self.prepareGeometryChange() @@ -242,7 +278,6 @@ class LinearRegionItem(UIGraphicsItem): self.sigRegionChanged.emit(self) self.sigRegionChangeFinished.emit(self) - def hoverEvent(self, ev): if self.movable and (not ev.isExit()) and ev.acceptDrags(QtCore.Qt.LeftButton): self.setMouseHover(True) @@ -255,36 +290,7 @@ class LinearRegionItem(UIGraphicsItem): return self.mouseHovering = hover if hover: - c = self.brush.color() - c.setAlpha(c.alpha() * 2) - self.currentBrush = fn.mkBrush(c) + self.currentBrush = self.hoverBrush else: self.currentBrush = self.brush self.update() - - #def hoverEnterEvent(self, ev): - #print "rgn hover enter" - #ev.ignore() - #self.updateHoverBrush() - - #def hoverMoveEvent(self, ev): - #print "rgn hover move" - #ev.ignore() - #self.updateHoverBrush() - - #def hoverLeaveEvent(self, ev): - #print "rgn hover leave" - #ev.ignore() - #self.updateHoverBrush(False) - - #def updateHoverBrush(self, hover=None): - #if hover is None: - #scene = self.scene() - #hover = scene.claimEvent(self, QtCore.Qt.LeftButton, scene.Drag) - - #if hover: - #self.currentBrush = fn.mkBrush(255, 0,0,100) - #else: - #self.currentBrush = self.brush - #self.update() - From b5e339145306f7ca1de6909db73d54458244c8d2 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 22 Sep 2017 16:44:53 -0700 Subject: [PATCH 051/135] Allow calling sip.setapi in subprocess before pyqtgraph is imported --- pyqtgraph/multiprocess/bootstrap.py | 6 ++++++ pyqtgraph/multiprocess/processes.py | 9 ++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/multiprocess/bootstrap.py b/pyqtgraph/multiprocess/bootstrap.py index f9cb0b0e..a8a03d41 100644 --- a/pyqtgraph/multiprocess/bootstrap.py +++ b/pyqtgraph/multiprocess/bootstrap.py @@ -22,6 +22,12 @@ if __name__ == '__main__': while len(sys.path) > 0: sys.path.pop() sys.path.extend(path) + + pyqtapis = opts.pop('pyqtapis', None) + if pyqtapis is not None: + import sip + for k,v in pyqtapis.items(): + sip.setapi(k, v) if opts.pop('pyside', False): import PySide diff --git a/pyqtgraph/multiprocess/processes.py b/pyqtgraph/multiprocess/processes.py index 7560ff70..1be7e50b 100644 --- a/pyqtgraph/multiprocess/processes.py +++ b/pyqtgraph/multiprocess/processes.py @@ -39,7 +39,7 @@ class Process(RemoteEventHandler): """ _process_count = 1 # just used for assigning colors to each process for debugging - def __init__(self, name=None, target=None, executable=None, copySysPath=True, debug=False, timeout=20, wrapStdout=None): + def __init__(self, name=None, target=None, executable=None, copySysPath=True, debug=False, timeout=20, wrapStdout=None, pyqtapis=None): """ ============== ============================================================= **Arguments:** @@ -47,7 +47,7 @@ class Process(RemoteEventHandler): from the remote process. target Optional function to call after starting remote process. By default, this is startEventLoop(), which causes the remote - process to process requests from the parent process until it + process to handle requests from the parent process until it is asked to quit. If you wish to specify a different target, it must be picklable (bound methods are not). copySysPath If True, copy the contents of sys.path to the remote process. @@ -61,6 +61,8 @@ class Process(RemoteEventHandler): for a python bug: http://bugs.python.org/issue3905 but has the side effect that child output is significantly delayed relative to the parent output. + pyqtapis Optional dictionary of PyQt API version numbers to set before + importing pyqtgraph in the remote process. ============== ============================================================= """ if target is None: @@ -130,7 +132,8 @@ class Process(RemoteEventHandler): targetStr=targetStr, path=sysPath, pyside=USE_PYSIDE, - debug=procDebug + debug=procDebug, + pyqtapis=pyqtapis, ) pickle.dump(data, self.proc.stdin) self.proc.stdin.close() From 6962777b9202497fc91d201dc4ec64d7243390dd Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 26 Sep 2017 08:29:04 -0700 Subject: [PATCH 052/135] HistogramLUTItem: add rgb level mode, save/restore methods --- pyqtgraph/graphicsItems/GraphicsItem.py | 3 +- pyqtgraph/graphicsItems/HistogramLUTItem.py | 217 +++++++++++++++----- 2 files changed, 163 insertions(+), 57 deletions(-) diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py index d45818dc..f88069bc 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -146,7 +146,8 @@ class GraphicsItem(object): return parents def viewRect(self): - """Return the bounds (in item coordinates) of this item's ViewBox or GraphicsWidget""" + """Return the visible bounds of this item's ViewBox or GraphicsWidget, + in the local coordinate system of the item.""" view = self.getViewBox() if view is None: return None diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index 31764250..6919cfba 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -36,7 +36,7 @@ class HistogramLUTItem(GraphicsWidget): sigLevelsChanged = QtCore.Signal(object) sigLevelChangeFinished = QtCore.Signal(object) - def __init__(self, image=None, fillHistogram=True): + def __init__(self, image=None, fillHistogram=True, rgbHistogram=False, levelMode='mono'): """ If *image* (ImageItem) is provided, then the control will be automatically linked to the image and changes to the control will be immediately reflected in the image's appearance. By default, the histogram is rendered with a fill. For performance, set *fillHistogram* = False. @@ -44,6 +44,8 @@ class HistogramLUTItem(GraphicsWidget): GraphicsWidget.__init__(self) self.lut = None self.imageItem = lambda: None # fake a dead weakref + self.levelMode = levelMode + self.rgbHistogram = rgbHistogram self.layout = QtGui.QGraphicsGridLayout() self.setLayout(self.layout) @@ -56,9 +58,27 @@ class HistogramLUTItem(GraphicsWidget): self.gradient = GradientEditorItem() self.gradient.setOrientation('right') self.gradient.loadPreset('grey') - self.region = LinearRegionItem([0, 1], LinearRegionItem.Horizontal) - self.region.setZValue(1000) - self.vb.addItem(self.region) + self.regions = [ + LinearRegionItem([0, 1], 'horizontal', swapMode='block'), + LinearRegionItem([0, 1], 'horizontal', swapMode='block', pen='r', + brush=fn.mkBrush((255, 50, 50, 50)), span=(0., 1/3.)), + LinearRegionItem([0, 1], 'horizontal', swapMode='block', pen='g', + brush=fn.mkBrush((50, 255, 50, 50)), span=(1/3., 2/3.)), + LinearRegionItem([0, 1], 'horizontal', swapMode='block', pen='b', + brush=fn.mkBrush((50, 50, 255, 80)), span=(2/3., 1.)), + LinearRegionItem([0, 1], 'horizontal', swapMode='block', pen='w', + brush=fn.mkBrush((255, 255, 255, 50)), span=(2/3., 1.))] + for region in self.regions: + region.setZValue(1000) + self.vb.addItem(region) + region.lines[0].addMarker('<|', 0.5) + region.lines[1].addMarker('|>', 0.5) + region.sigRegionChanged.connect(self.regionChanging) + region.sigRegionChangeFinished.connect(self.regionChanged) + + + self.region = self.regions[0] # for backward compatibility. + self.axis = AxisItem('left', linkView=self.vb, maxTickLength=-10, parent=self) self.layout.addItem(self.axis, 0, 0) self.layout.addItem(self.vb, 0, 1) @@ -71,12 +91,23 @@ class HistogramLUTItem(GraphicsWidget): #self.vb.addItem(self.grid) self.gradient.sigGradientChanged.connect(self.gradientChanged) - self.region.sigRegionChanged.connect(self.regionChanging) - self.region.sigRegionChangeFinished.connect(self.regionChanged) self.vb.sigRangeChanged.connect(self.viewRangeChanged) - self.plot = PlotDataItem() - self.plot.rotate(90) + add = QtGui.QPainter.CompositionMode_Plus + self.plots = [ + PlotCurveItem(pen=(200, 200, 200, 100)), # mono + PlotCurveItem(pen=(255, 0, 0, 100), compositionMode=add), # r + PlotCurveItem(pen=(0, 255, 0, 100), compositionMode=add), # g + PlotCurveItem(pen=(0, 0, 255, 100), compositionMode=add), # b + PlotCurveItem(pen=(200, 200, 200, 100), compositionMode=add), # a + ] + + self.plot = self.plots[0] # for backward compatibility. + for plot in self.plots: + plot.rotate(90) + self.vb.addItem(plot) + self.fillHistogram(fillHistogram) + self._showRegions() self.vb.addItem(self.plot) self.autoHistogramRange() @@ -86,25 +117,30 @@ class HistogramLUTItem(GraphicsWidget): #self.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Expanding) def fillHistogram(self, fill=True, level=0.0, color=(100, 100, 200)): - if fill: - self.plot.setFillLevel(level) - self.plot.setFillBrush(color) - else: - self.plot.setFillLevel(None) + colors = [color, (255, 0, 0, 50), (0, 255, 0, 50), (0, 0, 255, 50), (255, 255, 255, 50)] + for i,plot in enumerate(self.plots): + if fill: + plot.setFillLevel(level) + plot.setBrush(colors[i]) + else: + plot.setFillLevel(None) #def sizeHint(self, *args): #return QtCore.QSizeF(115, 200) def paint(self, p, *args): + if self.levelMode != 'mono': + return + pen = self.region.lines[0].pen rgn = self.getLevels() p1 = self.vb.mapFromViewToItem(self, Point(self.vb.viewRect().center().x(), rgn[0])) p2 = self.vb.mapFromViewToItem(self, Point(self.vb.viewRect().center().x(), rgn[1])) gradRect = self.gradient.mapRectToParent(self.gradient.gradRect.rect()) - for pen in [fn.mkPen('k', width=3), pen]: + for pen in [fn.mkPen((0, 0, 0, 100), width=3), pen]: p.setPen(pen) - p.drawLine(p1, gradRect.bottomLeft()) - p.drawLine(p2, gradRect.topLeft()) + p.drawLine(p1 + Point(0, 5), gradRect.bottomLeft()) + p.drawLine(p2 - Point(0, 5), gradRect.topLeft()) p.drawLine(gradRect.topLeft(), gradRect.topRight()) p.drawLine(gradRect.bottomLeft(), gradRect.bottomRight()) #p.drawRect(self.boundingRect()) @@ -115,28 +151,9 @@ class HistogramLUTItem(GraphicsWidget): self.vb.enableAutoRange(self.vb.YAxis, False) self.vb.setYRange(mn, mx, padding) - #d = mx-mn - #mn -= d*padding - #mx += d*padding - #self.range = [mn,mx] - #self.updateRange() - #self.vb.setMouseEnabled(False, True) - #self.region.setBounds([mn,mx]) - def autoHistogramRange(self): """Enable auto-scaling on the histogram plot.""" self.vb.enableAutoRange(self.vb.XYAxes) - #self.range = None - #self.updateRange() - #self.vb.setMouseEnabled(False, False) - - #def updateRange(self): - #self.vb.autoRange() - #if self.range is not None: - #self.vb.setYRange(*self.range) - #vr = self.vb.viewRect() - - #self.region.setBounds([vr.top(), vr.bottom()]) def setImageItem(self, img): """Set an ImageItem to have its levels and LUT automatically controlled @@ -145,10 +162,8 @@ class HistogramLUTItem(GraphicsWidget): self.imageItem = weakref.ref(img) img.sigImageChanged.connect(self.imageChanged) img.setLookupTable(self.getLookupTable) ## send function pointer, not the result - #self.gradientChanged() self.regionChanged() self.imageChanged(autoLevel=True) - #self.vb.autoRange() def viewRangeChanged(self): self.update() @@ -161,14 +176,14 @@ class HistogramLUTItem(GraphicsWidget): self.imageItem().setLookupTable(self.getLookupTable) ## send function pointer, not the result self.lut = None - #if self.imageItem is not None: - #self.imageItem.setLookupTable(self.gradient.getLookupTable(512)) self.sigLookupTableChanged.emit(self) def getLookupTable(self, img=None, n=None, alpha=None): """Return a lookup table from the color gradient defined by this HistogramLUTItem. """ + if self.levelMode is not 'mono': + return None if n is None: if img.dtype == np.uint8: n = 256 @@ -182,34 +197,124 @@ class HistogramLUTItem(GraphicsWidget): if self.imageItem() is not None: self.imageItem().setLevels(self.region.getRegion()) self.sigLevelChangeFinished.emit(self) - #self.update() def regionChanging(self): if self.imageItem() is not None: - self.imageItem().setLevels(self.region.getRegion()) + self.imageItem().setLevels(self.getLevels()) self.sigLevelsChanged.emit(self) self.update() def imageChanged(self, autoLevel=False, autoRange=False): - profiler = debug.Profiler() - h = self.imageItem().getHistogram() - profiler('get histogram') - if h[0] is None: + if self.imageItem() is None: return - self.plot.setData(*h) - profiler('set plot') - if autoLevel: - mn = h[0][0] - mx = h[0][-1] - self.region.setRegion([mn, mx]) - profiler('set region') + + if self.levelMode == 'mono': + for plt in self.plots[1:]: + plt.setVisible(False) + self.plots[0].setVisible(True) + # plot one histogram for all image data + profiler = debug.Profiler() + h = self.imageItem().getHistogram() + profiler('get histogram') + if h[0] is None: + return + self.plot.setData(*h) + profiler('set plot') + if autoLevel: + mn = h[0][0] + mx = h[0][-1] + self.region.setRegion([mn, mx]) + profiler('set region') + else: + mn, mx = self.imageItem().levels + self.region.setRegion([mn, mx]) + else: + # plot one histogram for each channel + self.plots[0].setVisible(False) + ch = self.imageItem().getHistogram(perChannel=True) + if ch[0] is None: + return + for i in range(1, 5): + if len(ch) >= i: + h = ch[i-1] + self.plots[i].setVisible(True) + self.plots[i].setData(*h) + if autoLevel: + mn = h[0][0] + mx = h[0][-1] + self.region[i].setRegion([mn, mx]) + else: + # hide channels not present in image data + self.plots[i].setVisible(False) + # make sure we are displaying the correct number of channels + self._showRegions() def getLevels(self): """Return the min and max levels. """ - return self.region.getRegion() + if self.levelMode == 'mono': + return self.region.getRegion() + else: + nch = self.imageItem().channels() + if nch is None: + nch = 3 + return [r.getRegion() for r in self.regions[1:nch+1]] - def setLevels(self, mn, mx): - """Set the min and max levels. + def setLevels(self, min=None, max=None, rgba=None): + """Set the min/max (bright and dark) levels. + + Arguments may be *min* and *max* for single-channel data, or + *rgba* = [(rmin, rmax), ...] for multi-channel data. """ - self.region.setRegion([mn, mx]) + if self.levelMode == 'mono': + if min is None: + min, max = rgba[0] + assert None not in (min, max) + self.region.setRegion((min, max)) + else: + if rgba is None: + raise TypeError("Must specify rgba argument when levelMode != 'mono'.") + for i, levels in enumerate(rgba): + self.regions[i+1].setRegion(levels) + + def setLevelMode(self, mode): + """ Set the method of controlling the image levels offered to the user. + Options are 'mono' or 'rgba'. + """ + assert mode in ('mono', 'rgba') + self.levelMode = mode + self._showRegions() + self.imageChanged() + self.update() + + def _showRegions(self): + for i in range(len(self.regions)): + self.regions[i].setVisible(False) + + if self.levelMode == 'rgba': + imax = 4 + if self.imageItem() is not None: + # Only show rgb channels if connected image lacks alpha. + nch = self.imageItem().channels() + if nch is None: + nch = 3 + xdif = 1.0 / nch + for i in range(1, nch+1): + self.regions[i].setVisible(True) + self.regions[i].setSpan((i-1) * xdif, i * xdif) + self.gradient.hide() + elif self.levelMode == 'mono': + self.regions[0].setVisible(True) + self.gradient.show() + else: + raise ValueError("Unknown level mode %r" % self.levelMode) + + def saveState(self): + return { + 'gradient': self.gradient.saveState(), + 'levels': self.getLevels(), + } + + def restoreState(self, state): + self.gradient.restoreState(state['gradient']) + self.setLevels(*state['levels']) From 07d1a62bfc0d26d9242d348dee0dbb16c63a33f7 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 26 Sep 2017 08:31:28 -0700 Subject: [PATCH 053/135] ImageItem: add support for rgb handling by histogramlut --- pyqtgraph/graphicsItems/ImageItem.py | 52 +++++++++++++++++++++------- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index 9588c586..2ae8b812 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -98,6 +98,11 @@ class ImageItem(GraphicsObject): axis = 1 if self.axisOrder == 'col-major' else 0 return self.image.shape[axis] + def channels(self): + if self.image is None: + return None + return self.image.shape[2] if self.image.ndim == 3 else 1 + def boundingRect(self): if self.image is None: return QtCore.QRectF(0., 0., 0., 0.) @@ -348,10 +353,15 @@ class ImageItem(GraphicsObject): profile = debug.Profiler() if self.image is None or self.image.size == 0: return - if isinstance(self.lut, collections.Callable): - lut = self.lut(self.image) + + # Request a lookup table if this image has only one channel + if self.image.ndim == 2 or self.image.shape[2] == 1: + if isinstance(self.lut, collections.Callable): + lut = self.lut(self.image) + else: + lut = self.lut else: - lut = self.lut + lut = None if self.autoDownsample: # reduce dimensions of image based on screen resolution @@ -395,9 +405,12 @@ class ImageItem(GraphicsObject): lut = self._effectiveLut levels = None + # Convert single-channel image to 2D array + if image.ndim == 3 and image.shape[-1] == 1: + image = image[..., 0] + # Assume images are in column-major order for backward compatibility # (most images are in row-major order) - if self.axisOrder == 'col-major': image = image.transpose((1, 0, 2)[:image.ndim]) @@ -430,7 +443,8 @@ class ImageItem(GraphicsObject): self.render() self.qimage.save(fileName, *args) - def getHistogram(self, bins='auto', step='auto', targetImageSize=200, targetHistogramSize=500, **kwds): + def getHistogram(self, bins='auto', step='auto', perChannel=False, targetImageSize=200, + targetHistogramSize=500, **kwds): """Returns x and y arrays containing the histogram values for the current image. For an explanation of the return format, see numpy.histogram(). @@ -446,6 +460,9 @@ class ImageItem(GraphicsObject): with each bin having an integer width. * All other types will have *targetHistogramSize* bins. + If *perChannel* is True, then the histogram is computed once per channel + and the output is a list of the results. + This method is also used when automatically computing levels. """ if self.image is None: @@ -458,21 +475,30 @@ class ImageItem(GraphicsObject): stepData = self.image[::step[0], ::step[1]] if bins == 'auto': + mn = stepData.min() + mx = stepData.max() if stepData.dtype.kind in "ui": - mn = stepData.min() - mx = stepData.max() + # For integer data, we select the bins carefully to avoid aliasing step = np.ceil((mx-mn) / 500.) bins = np.arange(mn, mx+1.01*step, step, dtype=np.int) - if len(bins) == 0: - bins = [mn, mx] else: - bins = 500 + # for float data, let numpy select the bins. + bins = np.linspace(mn, mx, 500) + + if len(bins) == 0: + bins = [mn, mx] kwds['bins'] = bins stepData = stepData[np.isfinite(stepData)] - hist = np.histogram(stepData, **kwds) - - return hist[1][:-1], hist[0] + if perChannel: + hist = [] + for i in range(stepData.shape[-1]): + h = np.histogram(stepData[..., i], **kwds) + hist.append((h[1][:-1], h[0])) + return hist + else: + hist = np.histogram(stepData, **kwds) + return hist[1][:-1], hist[0] def setPxMode(self, b): """ From 4a4a7383bc3cf549a7e2fb54c8a7379bd6031168 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 26 Sep 2017 08:33:34 -0700 Subject: [PATCH 054/135] ImageView: add support for RGB levels mode --- pyqtgraph/imageview/ImageView.py | 132 +++++++++++++++++++++---------- 1 file changed, 91 insertions(+), 41 deletions(-) diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index 5cc00f68..f6cacde0 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -12,7 +12,7 @@ Widget used for displaying 2D or 3D data. Features: - ROI plotting - Image normalization through a variety of methods """ -import os +import os, sys import numpy as np from ..Qt import QtCore, QtGui, USE_PYSIDE @@ -26,6 +26,7 @@ from ..graphicsItems.ROI import * from ..graphicsItems.LinearRegionItem import * from ..graphicsItems.InfiniteLine import * from ..graphicsItems.ViewBox import * +from ..graphicsItems.VTickGroup import VTickGroup from ..graphicsItems.GradientEditorItem import addGradientListToDocstring from .. import ptime as ptime from .. import debug as debug @@ -79,7 +80,8 @@ class ImageView(QtGui.QWidget): sigTimeChanged = QtCore.Signal(object, object) sigProcessingChanged = QtCore.Signal(object) - def __init__(self, parent=None, name="ImageView", view=None, imageItem=None, *args): + def __init__(self, parent=None, name="ImageView", view=None, imageItem=None, + levelMode='mono', *args): """ By default, this class creates an :class:`ImageItem ` to display image data and a :class:`ViewBox ` to contain the ImageItem. @@ -101,6 +103,9 @@ class ImageView(QtGui.QWidget): imageItem (ImageItem) If specified, this object will be used to display the image. Must be an instance of ImageItem or other compatible object. + levelMode See the *levelMode* argument to + :func:`HistogramLUTItem.__init__() + ` ============= ========================================================= Note: to display axis ticks inside the ImageView, instantiate it @@ -109,8 +114,10 @@ class ImageView(QtGui.QWidget): pg.ImageView(view=pg.PlotItem()) """ QtGui.QWidget.__init__(self, parent, *args) - self.levelMax = 4096 - self.levelMin = 0 + self._imageLevels = None # [(min, max), ...] per channel image metrics + self.levelMin = None # min / max levels across all channels + self.levelMax = None + self.name = name self.image = None self.axes = {} @@ -118,6 +125,7 @@ class ImageView(QtGui.QWidget): self.ui = Ui_Form() self.ui.setupUi(self) self.scene = self.ui.graphicsView.scene() + self.ui.histogram.setLevelMode(levelMode) self.ignoreTimeLine = False @@ -151,13 +159,15 @@ class ImageView(QtGui.QWidget): self.normRoi.setZValue(20) self.view.addItem(self.normRoi) self.normRoi.hide() - self.roiCurve = self.ui.roiPlot.plot() - self.timeLine = InfiniteLine(0, movable=True) + self.roiCurves = [] + self.timeLine = InfiniteLine(0, movable=True, markers=[('^', 0), ('v', 1)]) self.timeLine.setPen((255, 255, 0, 200)) self.timeLine.setZValue(1) self.ui.roiPlot.addItem(self.timeLine) self.ui.splitter.setSizes([self.height()-35, 35]) self.ui.roiPlot.hideAxis('left') + self.frameTicks = VTickGroup(yrange=[0.8, 1], pen=0.4) + self.ui.roiPlot.addItem(self.frameTicks, ignoreBounds=True) self.keysPressed = {} self.playTimer = QtCore.QTimer() @@ -200,7 +210,7 @@ class ImageView(QtGui.QWidget): self.roiClicked() ## initialize roi plot to correct shape / visibility - def setImage(self, img, autoRange=True, autoLevels=True, levels=None, axes=None, xvals=None, pos=None, scale=None, transform=None, autoHistogramRange=True): + def setImage(self, img, autoRange=True, autoLevels=True, levels=None, axes=None, xvals=None, pos=None, scale=None, transform=None, autoHistogramRange=True, levelMode=None): """ Set the image to be displayed in the widget. @@ -208,8 +218,9 @@ class ImageView(QtGui.QWidget): **Arguments:** img (numpy array) the image to be displayed. See :func:`ImageItem.setImage` and *notes* below. - xvals (numpy array) 1D array of z-axis values corresponding to the third axis - in a 3D image. For video, this array should contain the time of each frame. + xvals (numpy array) 1D array of z-axis values corresponding to the first axis + in a 3D image. For video, this array should contain the time of each + frame. autoRange (bool) whether to scale/pan the view to fit the image. autoLevels (bool) whether to update the white/black levels to fit the image. levels (min, max); the white and black level values to use. @@ -224,7 +235,11 @@ class ImageView(QtGui.QWidget): and *scale*. autoHistogramRange If True, the histogram y-range is automatically scaled to fit the image data. - ================== =========================================================================== + levelMode If specified, this sets the user interaction mode for setting image + levels. Options are 'mono', which provides a single level control for + all image channels, and 'rgb' or 'rgba', which provide individual + controls for each channel. + ================== ======================================================================= **Notes:** @@ -252,6 +267,8 @@ class ImageView(QtGui.QWidget): self.image = img self.imageDisp = None + if levelMode is not None: + self.ui.histogram.setLevelMode(levelMode) profiler() @@ -310,10 +327,9 @@ class ImageView(QtGui.QWidget): profiler() if self.axes['t'] is not None: - #self.ui.roiPlot.show() self.ui.roiPlot.setXRange(self.tVals.min(), self.tVals.max()) + self.frameTicks.setXVals(self.tVals) self.timeLine.setValue(0) - #self.ui.roiPlot.setMouseEnabled(False, False) if len(self.tVals) > 1: start = self.tVals.min() stop = self.tVals.max() + abs(self.tVals[-1] - self.tVals[0]) * 0.02 @@ -325,8 +341,7 @@ class ImageView(QtGui.QWidget): stop = 1 for s in [self.timeLine, self.normRgn]: s.setBounds([start, stop]) - #else: - #self.ui.roiPlot.hide() + profiler() self.imageItem.resetTransform() @@ -364,11 +379,14 @@ class ImageView(QtGui.QWidget): def autoLevels(self): """Set the min/max intensity levels automatically to match the image data.""" - self.setLevels(self.levelMin, self.levelMax) + self.setLevels(rgba=self._imageLevels) - def setLevels(self, min, max): - """Set the min/max (bright and dark) levels.""" - self.ui.histogram.setLevels(min, max) + def setLevels(self, *args, **kwds): + """Set the min/max (bright and dark) levels. + + See :func:`HistogramLUTItem.setLevels `. + """ + self.ui.histogram.setLevels(*args, **kwds) def autoRange(self): """Auto scale and pan the view around the image such that the image fills the view.""" @@ -377,12 +395,13 @@ class ImageView(QtGui.QWidget): def getProcessedImage(self): """Returns the image data after it has been processed by any normalization options in use. - This method also sets the attributes self.levelMin and self.levelMax - to indicate the range of data in the image.""" + """ if self.imageDisp is None: image = self.normalize(self.image) self.imageDisp = image - self.levelMin, self.levelMax = list(map(float, self.quickMinMax(self.imageDisp))) + self._imageLevels = self.quickMinMax(self.imageDisp) + self.levelMin = min([level[0] for level in self._imageLevels]) + self.levelMax = max([level[1] for level in self._imageLevels]) return self.imageDisp @@ -527,13 +546,15 @@ class ImageView(QtGui.QWidget): #self.ui.roiPlot.show() self.ui.roiPlot.setMouseEnabled(True, True) self.ui.splitter.setSizes([self.height()*0.6, self.height()*0.4]) - self.roiCurve.show() + for c in self.roiCurves: + c.show() self.roiChanged() self.ui.roiPlot.showAxis('left') else: self.roi.hide() self.ui.roiPlot.setMouseEnabled(False, False) - self.roiCurve.hide() + for c in self.roiCurves: + c.hide() self.ui.roiPlot.hideAxis('left') if self.hasTimeAxis(): @@ -557,36 +578,65 @@ class ImageView(QtGui.QWidget): return image = self.getProcessedImage() - if image.ndim == 2: - axes = (0, 1) - elif image.ndim == 3: - axes = (1, 2) - else: - return - + + # Extract image data from ROI + axes = (self.axes['x'], self.axes['y']) + data, coords = self.roi.getArrayRegion(image.view(np.ndarray), self.imageItem, axes, returnMappedCoords=True) - if data is not None: - while data.ndim > 1: - data = data.mean(axis=1) - if image.ndim == 3: - self.roiCurve.setData(y=data, x=self.tVals) + if data is None: + return + + # Convert extracted data into 1D plot data + if self.axes['t'] is None: + # Average across y-axis of ROI + data = data.mean(axis=axes[1]) + coords = coords[:,:,0] - coords[:,0:1,0] + xvals = (coords**2).sum(axis=0) ** 0.5 + else: + # Average data within entire ROI for each frame + data = data.mean(axis=max(axes)).mean(axis=min(axes)) + xvals = self.tVals + + # Handle multi-channel data + if data.ndim == 1: + plots = [(xvals, data, 'w')] + if data.ndim == 2: + if data.shape[1] == 1: + colors = 'w' else: - while coords.ndim > 2: - coords = coords[:,:,0] - coords = coords - coords[:,0,np.newaxis] - xvals = (coords**2).sum(axis=0) ** 0.5 - self.roiCurve.setData(y=data, x=xvals) + colors = 'rgbw' + plots = [] + for i in range(data.shape[1]): + d = data[:,i] + plots.append((xvals, d, colors[i])) + + # Update plot line(s) + while len(plots) < len(self.roiCurves): + c = self.roiCurves.pop() + c.scene().removeItem(c) + while len(plots) > len(self.roiCurves): + self.roiCurves.append(self.ui.roiPlot.plot()) + for i in range(len(plots)): + x, y, p = plots[i] + self.roiCurves[i].setData(x, y, pen=p) def quickMinMax(self, data): """ Estimate the min/max values of *data* by subsampling. + Returns [(min, max), ...] with one item per channel """ while data.size > 1e6: ax = np.argmax(data.shape) sl = [slice(None)] * data.ndim sl[ax] = slice(None, None, 2) data = data[sl] - return nanmin(data), nanmax(data) + + cax = self.axes['c'] + if cax is None: + return [(float(nanmin(data)), float(nanmax(data)))] + else: + return [(float(nanmin(data.take(i, axis=cax))), + float(nanmax(data.take(i, axis=cax)))) for i in range(data.shape[-1])] def normalize(self, image): """ From bde358ffaf17d6ec401d06a51c9df123a2839a56 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 26 Sep 2017 08:34:37 -0700 Subject: [PATCH 055/135] Fix colormapwidget.restorestate --- pyqtgraph/widgets/ColorMapWidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/widgets/ColorMapWidget.py b/pyqtgraph/widgets/ColorMapWidget.py index f6e28960..bd5668ae 100644 --- a/pyqtgraph/widgets/ColorMapWidget.py +++ b/pyqtgraph/widgets/ColorMapWidget.py @@ -152,7 +152,7 @@ class ColorMapParameter(ptree.types.GroupParameter): def restoreState(self, state): if 'fields' in state: self.setFields(state['fields']) - for itemState in state['items']: + for name, itemState in state['items'].items(): item = self.addNew(itemState['field']) item.restoreState(itemState) From 6e22524ac28f484ff74d12853b0a5bf6ba6b0fb2 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 26 Sep 2017 08:50:31 -0700 Subject: [PATCH 056/135] Update histogramlut example to allow rgb mode --- examples/HistogramLUT.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/examples/HistogramLUT.py b/examples/HistogramLUT.py index 4d89dd3f..082a963c 100644 --- a/examples/HistogramLUT.py +++ b/examples/HistogramLUT.py @@ -28,19 +28,27 @@ v = pg.GraphicsView() vb = pg.ViewBox() vb.setAspectLocked() v.setCentralItem(vb) -l.addWidget(v, 0, 0) +l.addWidget(v, 0, 0, 3, 1) w = pg.HistogramLUTWidget() l.addWidget(w, 0, 1) -data = pg.gaussianFilter(np.random.normal(size=(256, 256)), (20, 20)) +monoRadio = QtGui.QRadioButton('mono') +rgbaRadio = QtGui.QRadioButton('rgba') +l.addWidget(monoRadio, 1, 1) +l.addWidget(rgbaRadio, 2, 1) +monoRadio.setChecked(True) + +def setLevelMode(): + mode = 'mono' if monoRadio.isChecked() else 'rgba' + w.setLevelMode(mode) +monoRadio.toggled.connect(setLevelMode) + +data = pg.gaussianFilter(np.random.normal(size=(256, 256, 3)), (20, 20, 0)) for i in range(32): for j in range(32): data[i*8, j*8] += .1 img = pg.ImageItem(data) -#data2 = np.zeros((2,) + data.shape + (2,)) -#data2[0,:,:,0] = data ## make non-contiguous array for testing purposes -#img = pg.ImageItem(data2[0,:,:,0]) vb.addItem(img) vb.autoRange() From ef9871885193ebc562169fac38cb7abdeb7f4ecd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arthur=20Crippa=20B=C3=BArigo?= Date: Wed, 27 Sep 2017 22:13:59 -0300 Subject: [PATCH 057/135] BarGraphItem can plot horizontal bars. Proposed fix to https://github.com/pyqtgraph/pyqtgraph/issues/576 --- pyqtgraph/graphicsItems/BarGraphItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/BarGraphItem.py b/pyqtgraph/graphicsItems/BarGraphItem.py index a1d5d029..657222ba 100644 --- a/pyqtgraph/graphicsItems/BarGraphItem.py +++ b/pyqtgraph/graphicsItems/BarGraphItem.py @@ -120,7 +120,7 @@ class BarGraphItem(GraphicsObject): p.setPen(fn.mkPen(pen)) p.setBrush(fn.mkBrush(brush)) - for i in range(len(x0)): + for i in range(len(x0 if not np.isscalar(x0) else y0)): if pens is not None: p.setPen(fn.mkPen(pens[i])) if brushes is not None: From 6cd94402992d67e74abc79f5388cf2e3769e2d2e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 28 Sep 2017 08:56:06 -0700 Subject: [PATCH 058/135] LegendItem: make it possible to remove items directly, rather than by name --- pyqtgraph/graphicsItems/LegendItem.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/graphicsItems/LegendItem.py b/pyqtgraph/graphicsItems/LegendItem.py index 20d6416e..2c0114a7 100644 --- a/pyqtgraph/graphicsItems/LegendItem.py +++ b/pyqtgraph/graphicsItems/LegendItem.py @@ -81,19 +81,19 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): self.layout.addItem(label, row, 1) self.updateSize() - def removeItem(self, name): + def removeItem(self, item): """ Removes one item from the legend. ============== ======================================================== **Arguments:** - title The title displayed for this item. + item The item to remove or its name. ============== ======================================================== """ # Thanks, Ulrich! # cycle for a match for sample, label in self.items: - if label.text == name: # hit + if sample.item is item or label.text == item: self.items.remove( (sample, label) ) # remove from itemlist self.layout.removeItem(sample) # remove from layout sample.close() # remove from drawing @@ -130,7 +130,8 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): if ev.button() == QtCore.Qt.LeftButton: dpos = ev.pos() - ev.lastPos() self.autoAnchor(self.pos() + dpos) - + + class ItemSample(GraphicsWidget): """ Class responsible for drawing a single item in a LegendItem (sans label). From e885236bd5671b34e2f6fd4ca0fe99bede919e68 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 28 Sep 2017 08:57:42 -0700 Subject: [PATCH 059/135] Add PlotCurveItem composition mode --- pyqtgraph/graphicsItems/PlotCurveItem.py | 32 +++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index fac9ee57..9b4e95ef 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -68,6 +68,7 @@ class PlotCurveItem(GraphicsObject): 'antialias': getConfigOption('antialias'), 'connect': 'all', 'mouseWidth': 8, # width of shape responding to mouse click + 'compositionMode': None, } self.setClickable(kargs.get('clickable', False)) self.setData(*args, **kargs) @@ -93,6 +94,24 @@ class PlotCurveItem(GraphicsObject): self._mouseShape = None self._boundingRect = None + def setCompositionMode(self, mode): + """Change the composition mode of the item (see QPainter::CompositionMode + in the Qt documentation). This is useful when overlaying multiple items. + + ============================================ ============================================================ + **Most common arguments:** + QtGui.QPainter.CompositionMode_SourceOver Default; image replaces the background if it + is opaque. Otherwise, it uses the alpha channel to blend + the image with the background. + QtGui.QPainter.CompositionMode_Overlay The image color is mixed with the background color to + reflect the lightness or darkness of the background. + QtGui.QPainter.CompositionMode_Plus Both the alpha and color of the image and background pixels + are added together. + QtGui.QPainter.CompositionMode_Multiply The output is the image color multiplied by the background. + ============================================ ============================================================ + """ + self.opts['compositionMode'] = mode + self.update() def getData(self): return self.xData, self.yData @@ -274,7 +293,7 @@ class PlotCurveItem(GraphicsObject): def setData(self, *args, **kargs): """ - ============== ======================================================== + =============== ======================================================== **Arguments:** x, y (numpy arrays) Data to show pen Pen to use when drawing. Any single argument accepted by @@ -298,7 +317,9 @@ class PlotCurveItem(GraphicsObject): to be drawn. "finite" causes segments to be omitted if they are attached to nan or inf values. For any other connectivity, specify an array of boolean values. - ============== ======================================================== + compositionMode See :func:`setCompositionMode + `. + =============== ======================================================== If non-keyword arguments are used, they will be interpreted as setData(y) for a single argument and setData(x, y) for two @@ -311,6 +332,9 @@ class PlotCurveItem(GraphicsObject): def updateData(self, *args, **kargs): profiler = debug.Profiler() + if 'compositionMode' in kargs: + self.setCompositionMode(kargs['compositionMode']) + if len(args) == 1: kargs['y'] = args[0] elif len(args) == 2: @@ -430,7 +454,6 @@ class PlotCurveItem(GraphicsObject): x = None y = None path = self.getPath() - profiler('generate path') if self._exportOpts is not False: @@ -440,6 +463,9 @@ class PlotCurveItem(GraphicsObject): p.setRenderHint(p.Antialiasing, aa) + cmode = self.opts['compositionMode'] + if cmode is not None: + p.setCompositionMode(cmode) if self.opts['brush'] is not None and self.opts['fillLevel'] is not None: if self.fillPath is None: From 3c2c970a6b74594b274d27c26b81130840a91ef3 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 28 Sep 2017 09:00:57 -0700 Subject: [PATCH 060/135] Remove spiral ROI --- pyqtgraph/graphicsItems/ROI.py | 76 +--------------------------------- 1 file changed, 2 insertions(+), 74 deletions(-) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 963ecb05..bc77e1c3 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -26,7 +26,8 @@ from .. import getConfigOption __all__ = [ 'ROI', 'TestROI', 'RectROI', 'EllipseROI', 'CircleROI', 'PolygonROI', - 'LineROI', 'MultiLineROI', 'MultiRectROI', 'LineSegmentROI', 'PolyLineROI', 'SpiralROI', 'CrosshairROI', + 'LineROI', 'MultiLineROI', 'MultiRectROI', 'LineSegmentROI', 'PolyLineROI', + 'CrosshairROI', ] @@ -2157,79 +2158,6 @@ class _PolyLineSegment(LineSegmentROI): return LineSegmentROI.hoverEvent(self, ev) -class SpiralROI(ROI): - def __init__(self, pos=None, size=None, **args): - if size == None: - size = [100e-6,100e-6] - if pos == None: - pos = [0,0] - ROI.__init__(self, pos, size, **args) - self.translateSnap = False - self.addFreeHandle([0.25,0], name='a') - self.addRotateFreeHandle([1,0], [0,0], name='r') - #self.getRadius() - #QtCore.connect(self, QtCore.SIGNAL('regionChanged'), self. - - - def getRadius(self): - radius = Point(self.handles[1]['item'].pos()).length() - #r2 = radius[1] - #r3 = r2[0] - return radius - - def boundingRect(self): - r = self.getRadius() - return QtCore.QRectF(-r*1.1, -r*1.1, 2.2*r, 2.2*r) - #return self.bounds - - #def movePoint(self, *args, **kargs): - #ROI.movePoint(self, *args, **kargs) - #self.prepareGeometryChange() - #for h in self.handles: - #h['pos'] = h['item'].pos()/self.state['size'][0] - - def stateChanged(self, finish=True): - ROI.stateChanged(self, finish=finish) - if len(self.handles) > 1: - self.path = QtGui.QPainterPath() - h0 = Point(self.handles[0]['item'].pos()).length() - a = h0/(2.0*np.pi) - theta = 30.0*(2.0*np.pi)/360.0 - self.path.moveTo(QtCore.QPointF(a*theta*cos(theta), a*theta*sin(theta))) - x0 = a*theta*cos(theta) - y0 = a*theta*sin(theta) - radius = self.getRadius() - theta += 20.0*(2.0*np.pi)/360.0 - i = 0 - while Point(x0, y0).length() < radius and i < 1000: - x1 = a*theta*cos(theta) - y1 = a*theta*sin(theta) - self.path.lineTo(QtCore.QPointF(x1,y1)) - theta += 20.0*(2.0*np.pi)/360.0 - x0 = x1 - y0 = y1 - i += 1 - - - return self.path - - - def shape(self): - p = QtGui.QPainterPath() - p.addEllipse(self.boundingRect()) - return p - - def paint(self, p, *args): - p.setRenderHint(QtGui.QPainter.Antialiasing) - #path = self.shape() - p.setPen(self.currentPen) - p.drawPath(self.path) - p.setPen(QtGui.QPen(QtGui.QColor(255,0,0))) - p.drawPath(self.shape()) - p.setPen(QtGui.QPen(QtGui.QColor(0,0,255))) - p.drawRect(self.boundingRect()) - - class CrosshairROI(ROI): """A crosshair ROI whose position is at the center of the crosshairs. By default, it is scalable, rotatable and translatable.""" From 4d0f3b5821aac9e0ee96cb645973d3f217e65728 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 28 Sep 2017 09:03:24 -0700 Subject: [PATCH 061/135] Code cleanup --- pyqtgraph/graphicsItems/ROI.py | 68 ++-------------------------------- 1 file changed, 4 insertions(+), 64 deletions(-) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index bc77e1c3..6f0a46a4 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -239,6 +239,7 @@ class ROI(GraphicsObject): if isinstance(y, bool): raise TypeError("Positional arguments to setPos() must be numerical.") pos = Point(pos, y) + self.state['pos'] = pos QtGui.QGraphicsItem.setPos(self, pos) if update: @@ -253,7 +254,7 @@ class ROI(GraphicsObject): self.state['size'] = size if update: self.stateChanged(finish=finish) - + def setAngle(self, angle, update=True, finish=True): """Set the angle of rotation (in degrees) for this ROI. See setPos() for an explanation of the update and finish arguments. @@ -757,11 +758,6 @@ class ROI(GraphicsObject): else: raise Exception("New point location must be given in either 'parent' or 'scene' coordinates.") - - ## transform p0 and p1 into parent's coordinates (same as scene coords if there is no parent). I forget why. - #p0 = self.mapSceneToParent(p0) - #p1 = self.mapSceneToParent(p1) - ## Handles with a 'center' need to know their local position relative to the center point (lp0, lp1) if 'center' in h: c = h['center'] @@ -771,8 +767,6 @@ class ROI(GraphicsObject): if h['type'] == 't': snap = True if (modifiers & QtCore.Qt.ControlModifier) else None - #if self.translateSnap or (): - #snap = Point(self.snapSize, self.snapSize) self.translate(p1-p0, snap=snap, update=False) elif h['type'] == 'f': @@ -780,7 +774,6 @@ class ROI(GraphicsObject): h['item'].setPos(newPos) h['pos'] = newPos self.freeHandleMoved = True - #self.sigRegionChanged.emit(self) ## should be taken care of by call to stateChanged() elif h['type'] == 's': ## If a handle and its center have the same x or y value, we can't scale across that axis. @@ -922,10 +915,7 @@ class ROI(GraphicsObject): r = self.stateRect(newState) if not self.maxBounds.contains(r): return - #self.setTransform(tr) - #self.setPos(newState['pos'], update=False) - #self.prepareGeometryChange() - #self.state = newState + self.setState(newState, update=False) self.stateChanged(finish=finish) @@ -1761,6 +1751,7 @@ class EllipseROI(ROI): return arr w = arr.shape[axes[0]] h = arr.shape[axes[1]] + ## generate an ellipsoidal mask mask = np.fromfunction(lambda x,y: (((x+0.5)/(w/2.)-1)**2+ ((y+0.5)/(h/2.)-1)**2)**0.5 < 1, (w, h)) @@ -2179,16 +2170,8 @@ class CrosshairROI(ROI): self.prepareGeometryChange() def boundingRect(self): - #size = self.size() - #return QtCore.QRectF(-size[0]/2., -size[1]/2., size[0], size[1]).normalized() return self.shape().boundingRect() - #def getRect(self): - ### same as boundingRect -- for internal use so that boundingRect can be re-implemented in subclasses - #size = self.size() - #return QtCore.QRectF(-size[0]/2., -size[1]/2., size[0], size[1]).normalized() - - def shape(self): if self._shape is None: radius = self.getState()['size'][1] @@ -2202,56 +2185,13 @@ class CrosshairROI(ROI): stroker.setWidth(10) outline = stroker.createStroke(p) self._shape = self.mapFromDevice(outline) - - - ##h1 = self.handles[0]['item'].pos() - ##h2 = self.handles[1]['item'].pos() - #w1 = Point(-0.5, 0)*self.size() - #w2 = Point(0.5, 0)*self.size() - #h1 = Point(0, -0.5)*self.size() - #h2 = Point(0, 0.5)*self.size() - - #dh = h2-h1 - #dw = w2-w1 - #if dh.length() == 0 or dw.length() == 0: - #return p - #pxv = self.pixelVectors(dh)[1] - #if pxv is None: - #return p - - #pxv *= 4 - - #p.moveTo(h1+pxv) - #p.lineTo(h2+pxv) - #p.lineTo(h2-pxv) - #p.lineTo(h1-pxv) - #p.lineTo(h1+pxv) - - #pxv = self.pixelVectors(dw)[1] - #if pxv is None: - #return p - - #pxv *= 4 - - #p.moveTo(w1+pxv) - #p.lineTo(w2+pxv) - #p.lineTo(w2-pxv) - #p.lineTo(w1-pxv) - #p.lineTo(w1+pxv) return self._shape def paint(self, p, *args): - #p.save() - #r = self.getRect() radius = self.getState()['size'][1] p.setRenderHint(QtGui.QPainter.Antialiasing) p.setPen(self.currentPen) - #p.translate(r.left(), r.top()) - #p.scale(r.width()/10., r.height()/10.) ## need to scale up a little because drawLine has trouble dealing with 0.5 - #p.drawLine(0,5, 10,5) - #p.drawLine(5,0, 5,10) - #p.restore() p.drawLine(Point(0, -radius), Point(0, radius)) p.drawLine(Point(-radius, 0), Point(radius, 0)) From 97b71a2b284664ed8db052fb3799434a3973f09f Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 28 Sep 2017 09:03:47 -0700 Subject: [PATCH 062/135] Add RulerROI --- pyqtgraph/graphicsItems/ROI.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 6f0a46a4..473506cf 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -2197,3 +2197,31 @@ class CrosshairROI(ROI): p.drawLine(Point(-radius, 0), Point(radius, 0)) +class RulerROI(LineSegmentROI): + def paint(self, p, *args): + LineSegmentROI.paint(self, p, *args) + h1 = self.handles[0]['item'].pos() + h2 = self.handles[1]['item'].pos() + p1 = p.transform().map(h1) + p2 = p.transform().map(h2) + + vec = Point(h2) - Point(h1) + length = vec.length() + angle = vec.angle(Point(1, 0)) + + pvec = p2 - p1 + pvecT = Point(pvec.y(), -pvec.x()) + pos = 0.5 * (p1 + p2) + pvecT * 40 / pvecT.length() + + p.resetTransform() + + txt = fn.siFormat(length, suffix='m') + '\n%0.1f deg' % angle + p.drawText(QtCore.QRectF(pos.x()-50, pos.y()-50, 100, 100), QtCore.Qt.AlignCenter | QtCore.Qt.AlignVCenter, txt) + + def boundingRect(self): + r = LineSegmentROI.boundingRect(self) + pxl = self.pixelLength(Point([1, 0])) + if pxl is None: + return r + pxw = 50 * pxl + return r.adjusted(-50, -50, 50, 50) From 0de0bf4c44e5600303c5fdf317bf498d120ac947 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 28 Sep 2017 09:05:08 -0700 Subject: [PATCH 063/135] Fix: very small ellipse/circle ROIs have bad click areas --- pyqtgraph/graphicsItems/ROI.py | 42 ++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 473506cf..850104c0 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -1725,10 +1725,18 @@ class EllipseROI(ROI): """ def __init__(self, pos, size, **args): #QtGui.QGraphicsRectItem.__init__(self, 0, 0, size[0], size[1]) + self.path = None ROI.__init__(self, pos, size, **args) + self.sigRegionChanged.connect(self._clearPath) + self._addHandles() + + def _addHandles(self): self.addRotateHandle([1.0, 0.5], [0.5, 0.5]) self.addScaleHandle([0.5*2.**-0.5 + 0.5, 0.5*2.**-0.5 + 0.5], [0.5, 0.5]) + def _clearPath(self): + self.path = None + def paint(self, p, opt, widget): r = self.boundingRect() p.setRenderHint(QtGui.QPainter.Antialiasing) @@ -1764,8 +1772,27 @@ class EllipseROI(ROI): return arr * mask def shape(self): - self.path = QtGui.QPainterPath() - self.path.addEllipse(self.boundingRect()) + if self.path is None: + path = QtGui.QPainterPath() + + # Note: Qt has a bug where very small ellipses (radius <0.001) do + # not correctly intersect with mouse position (upper-left and + # lower-right quadrants are not clickable). + #path.addEllipse(self.boundingRect()) + + # Workaround: manually draw the path. + br = self.boundingRect() + center = br.center() + r1 = br.width() / 2. + r2 = br.height() / 2. + theta = np.linspace(0, 2*np.pi, 24) + x = center.x() + r1 * np.cos(theta) + y = center.y() + r2 * np.sin(theta) + path.moveTo(x[0], y[0]) + for i in range(1, len(x)): + path.lineTo(x[i], y[i]) + self.path = path + return self.path @@ -1782,10 +1809,15 @@ class CircleROI(EllipseROI): ============== ============================================================= """ - def __init__(self, pos, size, **args): - ROI.__init__(self, pos, size, **args) + def __init__(self, pos, size=None, radius=None, **args): + if size is None: + if radius is None: + raise TypeError("Must provide either size or radius.") + size = (radius*2, radius*2) + EllipseROI.__init__(self, pos, size, **args) self.aspectLocked = True - #self.addTranslateHandle([0.5, 0.5]) + + def _addHandles(self): self.addScaleHandle([0.5*2.**-0.5 + 0.5, 0.5*2.**-0.5 + 0.5], [0.5, 0.5]) From 60ce541df6aa0bda66bc07a3894ab0e34f556a57 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 28 Sep 2017 09:05:31 -0700 Subject: [PATCH 064/135] minor argument type checking --- pyqtgraph/graphicsItems/ROI.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 850104c0..9f3dae6f 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -232,6 +232,9 @@ class ROI(GraphicsObject): multiple change functions to be called sequentially while minimizing processing overhead and repeated signals. Setting ``update=False`` also forces ``finish=False``. """ + if update not in (True, False): + raise TypeError("update argument must be bool") + if y is None: pos = Point(pos) else: @@ -249,6 +252,8 @@ class ROI(GraphicsObject): """Set the size of the ROI. May be specified as a QPoint, Point, or list of two values. See setPos() for an explanation of the update and finish arguments. """ + if update not in (True, False): + raise TypeError("update argument must be bool") size = Point(size) self.prepareGeometryChange() self.state['size'] = size @@ -259,6 +264,8 @@ class ROI(GraphicsObject): """Set the angle of rotation (in degrees) for this ROI. See setPos() for an explanation of the update and finish arguments. """ + if update not in (True, False): + raise TypeError("update argument must be bool") self.state['angle'] = angle tr = QtGui.QTransform() #tr.rotate(-angle * 180 / np.pi) From 2a56435475a1cd97b69110011e303624a4e7677e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 28 Sep 2017 09:09:17 -0700 Subject: [PATCH 065/135] MetaArray: make it possible to append multiple axis values Example use case: taking an image stack where each frame has a time value AND a position. Previously we could only append new time values. --- pyqtgraph/metaarray/MetaArray.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/pyqtgraph/metaarray/MetaArray.py b/pyqtgraph/metaarray/MetaArray.py index 66ecc460..15d374a6 100644 --- a/pyqtgraph/metaarray/MetaArray.py +++ b/pyqtgraph/metaarray/MetaArray.py @@ -748,7 +748,6 @@ class MetaArray(object): else: fd.seek(0) meta = MetaArray._readMeta(fd) - if not kwargs.get("readAllData", True): self._data = np.empty(meta['shape'], dtype=meta['type']) if 'version' in meta: @@ -1031,6 +1030,7 @@ class MetaArray(object): """Write this object to a file. The object can be restored by calling MetaArray(file=fileName) opts: appendAxis: the name (or index) of the appendable axis. Allows the array to grow. + appendKeys: a list of keys (other than "values") for metadata to append to on the appendable axis. compression: None, 'gzip' (good compression), 'lzf' (fast compression), etc. chunks: bool or tuple specifying chunk shape """ @@ -1096,7 +1096,6 @@ class MetaArray(object): 'chunks': None, 'compression': None } - ## set maximum shape to allow expansion along appendAxis append = False @@ -1125,14 +1124,19 @@ class MetaArray(object): data[tuple(sl)] = self.view(np.ndarray) ## add axis values if they are present. + axKeys = ["values"] + axKeys.extend(opts.get("appendKeys", [])) axInfo = f['info'][str(ax)] - if 'values' in axInfo: - v = axInfo['values'] - v2 = self._info[ax]['values'] - shape = list(v.shape) - shape[0] += v2.shape[0] - v.resize(shape) - v[-v2.shape[0]:] = v2 + for key in axKeys: + if key in axInfo: + v = axInfo[key] + v2 = self._info[ax][key] + shape = list(v.shape) + shape[0] += v2.shape[0] + v.resize(shape) + v[-v2.shape[0]:] = v2 + else: + raise TypeError('Cannot append to axis info key "%s"; this key is not present in the target file.' % key) f.close() else: f = h5py.File(fileName, 'w') From aad1c737c34b2bf663017493652838a516ee7a01 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 28 Sep 2017 09:11:07 -0700 Subject: [PATCH 066/135] eq(): better performance by avoiding array comparison when shapes do not match --- pyqtgraph/functions.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 1aed6ace..261727cb 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -417,7 +417,21 @@ def eq(a, b): """ if a is b: return True - + + # Avoid comparing large arrays against scalars; this is expensive and we know it should return False. + aIsArr = isinstance(a, (np.ndarray, MetaArray)) + bIsArr = isinstance(b, (np.ndarray, MetaArray)) + if (aIsArr or bIsArr) and type(a) != type(b): + return False + + # If both inputs are arrays, we can speeed up comparison if shapes / dtypes don't match + # NOTE: arrays of dissimilar type should be considered unequal even if they are numerically + # equal because they may behave differently when computed on. + if aIsArr and bIsArr and (a.shape != b.shape or a.dtype != b.dtype): + return False + + # Test for equivalence. + # If the test raises a recognized exception, then return Falase try: try: # Sometimes running catch_warnings(module=np) generates AttributeError ??? From 7c9107fa5d9954d2f59bcabf523dc2fdb77eee34 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 29 Sep 2017 08:45:33 -0700 Subject: [PATCH 067/135] use ndarray() strides argument to construct subarray previously this was done manually (and imperfectly) --- pyqtgraph/functions.py | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 261727cb..b38888fe 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -750,23 +750,15 @@ def subArray(data, offset, shape, stride): #data = data.flatten() data = data[offset:] shape = tuple(shape) - stride = tuple(stride) extraShape = data.shape[1:] - #print data.shape, offset, shape, stride - for i in range(len(shape)): - mask = (slice(None),) * i + (slice(None, shape[i] * stride[i]),) - newShape = shape[:i+1] - if i < len(shape)-1: - newShape += (stride[i],) - newShape += extraShape - #print i, mask, newShape - #print "start:\n", data.shape, data - data = data[mask] - #print "mask:\n", data.shape, data - data = data.reshape(newShape) - #print "reshape:\n", data.shape, data + + strides = list(data.strides[::-1]) + itemsize = strides[-1] + for s in stride[1::-1]: + strides.append(itemsize * s) + strides = tuple(strides[::-1]) - return data + return np.ndarray(buffer=data, shape=shape+extraShape, strides=strides, dtype=data.dtype) def transformToArray(tr): From a2bb944e789bae5cbcbcd262c0286081be8b051f Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 29 Sep 2017 08:48:35 -0700 Subject: [PATCH 068/135] Make PathButton margin customizable --- pyqtgraph/widgets/PathButton.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/widgets/PathButton.py b/pyqtgraph/widgets/PathButton.py index 52c60e20..ee2e0bca 100644 --- a/pyqtgraph/widgets/PathButton.py +++ b/pyqtgraph/widgets/PathButton.py @@ -5,9 +5,11 @@ __all__ = ['PathButton'] class PathButton(QtGui.QPushButton): - """Simple PushButton extension which paints a QPainterPath on its face""" - def __init__(self, parent=None, path=None, pen='default', brush=None, size=(30,30)): + """Simple PushButton extension that paints a QPainterPath centered on its face. + """ + def __init__(self, parent=None, path=None, pen='default', brush=None, size=(30,30), margin=7): QtGui.QPushButton.__init__(self, parent) + self.margin = margin self.path = None if pen == 'default': pen = 'k' @@ -19,7 +21,6 @@ class PathButton(QtGui.QPushButton): self.setFixedWidth(size[0]) self.setFixedHeight(size[1]) - def setBrush(self, brush): self.brush = fn.mkBrush(brush) @@ -32,7 +33,7 @@ class PathButton(QtGui.QPushButton): def paintEvent(self, ev): QtGui.QPushButton.paintEvent(self, ev) - margin = 7 + margin = self.margin geom = QtCore.QRectF(0, 0, self.width(), self.height()).adjusted(margin, margin, -margin, -margin) rect = self.path.boundingRect() scale = min(geom.width() / float(rect.width()), geom.height() / float(rect.height())) From e7a92f4720e9ddf542d24e63aa600d537a754790 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 29 Sep 2017 08:50:12 -0700 Subject: [PATCH 069/135] Add Combobox save/restoreState methods Also allow tuple as input type in addition to list --- pyqtgraph/widgets/ComboBox.py | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/widgets/ComboBox.py b/pyqtgraph/widgets/ComboBox.py index a6828959..6f184c5f 100644 --- a/pyqtgraph/widgets/ComboBox.py +++ b/pyqtgraph/widgets/ComboBox.py @@ -102,7 +102,7 @@ class ComboBox(QtGui.QComboBox): @blockIfUnchanged def setItems(self, items): """ - *items* may be a list or a dict. + *items* may be a list, a tuple, or a dict. If a dict is given, then the keys are used to populate the combo box and the values will be used for both value() and setValue(). """ @@ -191,13 +191,13 @@ class ComboBox(QtGui.QComboBox): @ignoreIndexChange @blockIfUnchanged def addItems(self, items): - if isinstance(items, list): + if isinstance(items, list) or isinstance(items, tuple): texts = items items = dict([(x, x) for x in items]) elif isinstance(items, dict): texts = list(items.keys()) else: - raise TypeError("items argument must be list or dict (got %s)." % type(items)) + raise TypeError("items argument must be list or dict or tuple (got %s)." % type(items)) for t in texts: if t in self._items: @@ -216,3 +216,30 @@ class ComboBox(QtGui.QComboBox): QtGui.QComboBox.clear(self) self.itemsChanged() + def saveState(self): + ind = self.currentIndex() + data = self.itemData(ind) + #if not data.isValid(): + if data is not None: + try: + if not data.isValid(): + data = None + else: + data = data.toInt()[0] + except AttributeError: + pass + if data is None: + return asUnicode(self.itemText(ind)) + else: + return data + + def restoreState(self, v): + if type(v) is int: + ind = self.findData(v) + if ind > -1: + self.setCurrentIndex(ind) + return + self.setCurrentIndex(self.findText(str(v))) + + def widgetGroupInterface(self): + return (self.currentIndexChanged, self.saveState, self.restoreState) From 3609f9df3edf384be7b80c7bcc15a5fb478d877a Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 29 Sep 2017 08:51:26 -0700 Subject: [PATCH 070/135] Fix colormapwidget saveState --- pyqtgraph/widgets/ColorMapWidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/widgets/ColorMapWidget.py b/pyqtgraph/widgets/ColorMapWidget.py index f6e28960..bd5668ae 100644 --- a/pyqtgraph/widgets/ColorMapWidget.py +++ b/pyqtgraph/widgets/ColorMapWidget.py @@ -152,7 +152,7 @@ class ColorMapParameter(ptree.types.GroupParameter): def restoreState(self, state): if 'fields' in state: self.setFields(state['fields']) - for itemState in state['items']: + for name, itemState in state['items'].items(): item = self.addNew(itemState['field']) item.restoreState(itemState) From 0f910c45d1338f47aaaad021790dc199fafd16f6 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 29 Sep 2017 08:54:33 -0700 Subject: [PATCH 071/135] Make parameter name,value inint args go through setValue and setName --- pyqtgraph/parametertree/Parameter.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/pyqtgraph/parametertree/Parameter.py b/pyqtgraph/parametertree/Parameter.py index 4ca80ffe..d48fee57 100644 --- a/pyqtgraph/parametertree/Parameter.py +++ b/pyqtgraph/parametertree/Parameter.py @@ -162,7 +162,11 @@ class Parameter(QtCore.QObject): 'title': None, #'limits': None, ## This is a bad plan--each parameter type may have a different data type for limits. } + value = opts.get('value', None) + name = opts.get('name', None) self.opts.update(opts) + self.opts['value'] = None # will be set later. + self.opts['name'] = None self.childs = [] self.names = {} ## map name:child @@ -172,17 +176,19 @@ class Parameter(QtCore.QObject): self.blockTreeChangeEmit = 0 #self.monitoringChildren = False ## prevent calling monitorChildren more than once - if 'value' not in self.opts: - self.opts['value'] = None - - if 'name' not in self.opts or not isinstance(self.opts['name'], basestring): + if not isinstance(name, basestring): raise Exception("Parameter must have a string name specified in opts.") - self.setName(opts['name']) + self.setName(name) self.addChildren(self.opts.get('children', [])) - - if 'value' in self.opts and 'default' not in self.opts: - self.opts['default'] = self.opts['value'] + + self.opts['value'] = None + if value is not None: + self.setValue(value) + + if 'default' not in self.opts: + self.opts['default'] = None + self.setDefault(self.opts['value']) ## Connect all state changed signals to the general sigStateChanged self.sigValueChanged.connect(lambda param, data: self.emitStateChanged('value', data)) From bf31a5ba99ca11cb75c50492bff281bb125aa09a Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 29 Sep 2017 08:55:36 -0700 Subject: [PATCH 072/135] Parameter.child raises KeyError if requested child name does not exist --- pyqtgraph/parametertree/Parameter.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/parametertree/Parameter.py b/pyqtgraph/parametertree/Parameter.py index d48fee57..e28085bf 100644 --- a/pyqtgraph/parametertree/Parameter.py +++ b/pyqtgraph/parametertree/Parameter.py @@ -653,18 +653,19 @@ class Parameter(QtCore.QObject): """Return a child parameter. Accepts the name of the child or a tuple (path, to, child) - Added in version 0.9.9. Ealier versions used the 'param' method, which is still - implemented for backward compatibility.""" + Added in version 0.9.9. Earlier versions used the 'param' method, which is still + implemented for backward compatibility. + """ try: param = self.names[names[0]] except KeyError: - raise Exception("Parameter %s has no child named %s" % (self.name(), names[0])) + raise KeyError("Parameter %s has no child named %s" % (self.name(), names[0])) if len(names) > 1: - return param.param(*names[1:]) + return param.child(*names[1:]) else: return param - + def param(self, *names): # for backward compatibility. return self.child(*names) From 09b8e662b17307eee22b969de5cbe23ef134cc04 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 29 Sep 2017 08:56:28 -0700 Subject: [PATCH 073/135] systemsolver: minor fixes --- pyqtgraph/parametertree/SystemSolver.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/parametertree/SystemSolver.py b/pyqtgraph/parametertree/SystemSolver.py index 24e35e9a..c804d50a 100644 --- a/pyqtgraph/parametertree/SystemSolver.py +++ b/pyqtgraph/parametertree/SystemSolver.py @@ -177,7 +177,7 @@ class SystemSolver(object): raise TypeError("constraint must be None, True, 'fixed', or tuple. (got %s)" % constraint) # type checking / massaging - if var[1] is np.ndarray: + if var[1] is np.ndarray and value is not None: value = np.array(value, dtype=float) elif var[1] in (int, float, tuple) and value is not None: value = var[1](value) @@ -185,9 +185,9 @@ class SystemSolver(object): # constraint checks if constraint is True and not self.check_constraint(name, value): raise ValueError("Setting %s = %s violates constraint %s" % (name, value, var[2])) - + # invalidate other dependent values - if var[0] is not None: + if var[0] is not None or value is None: # todo: we can make this more clever..(and might need to) # we just know that a value of None cannot have dependencies # (because if anyone else had asked for this value, it wouldn't be From eb1b7fc8bb07669bc0140678f09c1b934a507bbb Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 29 Sep 2017 08:56:44 -0700 Subject: [PATCH 074/135] add systemsolver copy method --- pyqtgraph/parametertree/SystemSolver.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pyqtgraph/parametertree/SystemSolver.py b/pyqtgraph/parametertree/SystemSolver.py index c804d50a..ffdabfae 100644 --- a/pyqtgraph/parametertree/SystemSolver.py +++ b/pyqtgraph/parametertree/SystemSolver.py @@ -1,5 +1,7 @@ from ..pgcollections import OrderedDict import numpy as np +import copy + class SystemSolver(object): """ @@ -73,6 +75,12 @@ class SystemSolver(object): self.__dict__['_currentGets'] = set() self.reset() + def copy(self): + sys = type(self)() + sys.__dict__['_vars'] = copy.deepcopy(self.__dict__['_vars']) + sys.__dict__['_currentGets'] = copy.deepcopy(self.__dict__['_currentGets']) + return sys + def reset(self): """ Reset all variables in the solver to their default state. From 2754427b25d172dff2d73199a470ab4d99b06709 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 29 Sep 2017 08:58:00 -0700 Subject: [PATCH 075/135] systemsolver: add method for checking constraints / DOF --- pyqtgraph/parametertree/SystemSolver.py | 37 ++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/parametertree/SystemSolver.py b/pyqtgraph/parametertree/SystemSolver.py index ffdabfae..b1d4256a 100644 --- a/pyqtgraph/parametertree/SystemSolver.py +++ b/pyqtgraph/parametertree/SystemSolver.py @@ -175,6 +175,16 @@ class SystemSolver(object): elif constraint == 'fixed': if 'f' not in var[3]: raise TypeError("Fixed constraints not allowed for '%s'" % name) + # This is nice, but not reliable because sometimes there is 1 DOF but we set 2 + # values simultaneously. + # if var[2] is None: + # try: + # self.get(name) + # # has already been computed by the system; adding a fixed constraint + # # would overspecify the system. + # raise ValueError("Cannot fix parameter '%s'; system would become overconstrained." % name) + # except RuntimeError: + # pass var[2] = constraint elif isinstance(constraint, tuple): if 'r' not in var[3]: @@ -245,6 +255,31 @@ class SystemSolver(object): for k in self._vars: getattr(self, k) + def checkOverconstraint(self): + """Check whether the system is overconstrained. If so, return the name of + the first overconstrained parameter. + + Overconstraints occur when any fixed parameter can be successfully computed by the system. + (Ideally, all parameters are either fixed by the user or constrained by the + system, but never both). + """ + for k,v in self._vars.items(): + if v[2] == 'fixed' and 'n' in v[3]: + oldval = v[:] + self.set(k, None, None) + try: + self.get(k) + return k + except RuntimeError: + pass + finally: + self._vars[k] = oldval + + return False + + + + def __repr__(self): state = OrderedDict() for name, var in self._vars.items(): @@ -386,4 +421,4 @@ if __name__ == '__main__': camera.solve() print(camera.saveState()) - \ No newline at end of file + From ce7594b6972ec6b840829d7e306aad7a2fc13c50 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 29 Sep 2017 08:59:14 -0700 Subject: [PATCH 076/135] Add GroupParameter.sigAddNew signal --- pyqtgraph/parametertree/parameterTypes.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py index 8c1e587d..d75dbba0 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -462,12 +462,15 @@ class GroupParameter(Parameter): instead of a button. """ itemClass = GroupParameterItem + + sigAddNew = QtCore.Signal(object, object) # self, type def addNew(self, typ=None): """ This method is called when the user has requested to add a new item to the group. + By default, it emits ``sigAddNew(self, typ)``. """ - raise Exception("Must override this function in subclass.") + self.sigAddNew.emit(self, typ) def setAddList(self, vals): """Change the list of options available for the user to add to the group.""" From 812a65461d8e1f7f5b6a2507031530ec31246d2b Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 29 Sep 2017 08:59:37 -0700 Subject: [PATCH 077/135] action parameter minor ui adjustment --- pyqtgraph/parametertree/parameterTypes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py index d75dbba0..d137410d 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -608,6 +608,7 @@ class ActionParameterItem(ParameterItem): ParameterItem.__init__(self, param, depth) self.layoutWidget = QtGui.QWidget() self.layout = QtGui.QHBoxLayout() + self.layout.setContentsMargins(0, 0, 0, 0) self.layoutWidget.setLayout(self.layout) self.button = QtGui.QPushButton(param.name()) #self.layout.addSpacing(100) From fcf45036711c97c51b62f65e3ae5cba930b655bb Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 2 Oct 2017 08:58:03 -0700 Subject: [PATCH 078/135] Fix: avoid division by 0 when image is single valued --- pyqtgraph/functions.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 1aed6ace..3a50eb9e 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -1079,7 +1079,9 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): minVal, maxVal = levels[i] if minVal == maxVal: maxVal += 1e-16 - newData[...,i] = rescaleData(data[...,i], scale/(maxVal-minVal), minVal, dtype=dtype) + rng = maxVal-minVal + rng = 1 if rng == 0 else rng + newData[...,i] = rescaleData(data[...,i], scale / rng, minVal, dtype=dtype) data = newData else: # Apply level scaling unless it would have no effect on the data From c3e52f15b0df0104455922512762887fdfc81e25 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 3 Oct 2017 08:16:36 -0700 Subject: [PATCH 079/135] Fix ImageItem rgb histogram calculation --- pyqtgraph/graphicsItems/ImageItem.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index 2ae8b812..34150282 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -489,14 +489,17 @@ class ImageItem(GraphicsObject): bins = [mn, mx] kwds['bins'] = bins - stepData = stepData[np.isfinite(stepData)] + if perChannel: hist = [] for i in range(stepData.shape[-1]): - h = np.histogram(stepData[..., i], **kwds) + stepChan = stepData[..., i] + stepChan = stepChan[np.isfinite(stepChan)] + h = np.histogram(stepChan, **kwds) hist.append((h[1][:-1], h[0])) return hist else: + stepData = stepData[np.isfinite(stepData)] hist = np.histogram(stepData, **kwds) return hist[1][:-1], hist[0] From faca369a8d38f00b9324051482f51e0bfe269b4a Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 3 Oct 2017 08:17:22 -0700 Subject: [PATCH 080/135] code cleanup --- pyqtgraph/graphicsItems/HistogramLUTItem.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index 6919cfba..dc6286e3 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -76,7 +76,6 @@ class HistogramLUTItem(GraphicsWidget): region.sigRegionChanged.connect(self.regionChanging) region.sigRegionChangeFinished.connect(self.regionChanged) - self.region = self.regions[0] # for backward compatibility. self.axis = AxisItem('left', linkView=self.vb, maxTickLength=-10, parent=self) @@ -87,9 +86,6 @@ class HistogramLUTItem(GraphicsWidget): self.gradient.setFlag(self.gradient.ItemStacksBehindParent) self.vb.setFlag(self.gradient.ItemStacksBehindParent) - #self.grid = GridItem() - #self.vb.addItem(self.grid) - self.gradient.sigGradientChanged.connect(self.gradientChanged) self.vb.sigRangeChanged.connect(self.viewRangeChanged) add = QtGui.QPainter.CompositionMode_Plus @@ -114,7 +110,6 @@ class HistogramLUTItem(GraphicsWidget): if image is not None: self.setImageItem(image) - #self.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Expanding) def fillHistogram(self, fill=True, level=0.0, color=(100, 100, 200)): colors = [color, (255, 0, 0, 50), (0, 255, 0, 50), (0, 0, 255, 50), (255, 255, 255, 50)] @@ -125,9 +120,6 @@ class HistogramLUTItem(GraphicsWidget): else: plot.setFillLevel(None) - #def sizeHint(self, *args): - #return QtCore.QSizeF(115, 200) - def paint(self, p, *args): if self.levelMode != 'mono': return @@ -143,8 +135,6 @@ class HistogramLUTItem(GraphicsWidget): p.drawLine(p2 - Point(0, 5), gradRect.topLeft()) p.drawLine(gradRect.topLeft(), gradRect.topRight()) p.drawLine(gradRect.bottomLeft(), gradRect.bottomRight()) - #p.drawRect(self.boundingRect()) - def setHistogramRange(self, mn, mx, padding=0.1): """Set the Y range on the histogram plot. This disables auto-scaling.""" From 21bda49a294fe133a1a594d6a33dc7d683fd5df0 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 3 Oct 2017 08:27:36 -0700 Subject: [PATCH 081/135] Docstring updates --- pyqtgraph/graphicsItems/HistogramLUTItem.py | 24 +++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index dc6286e3..85cbe9cf 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -25,11 +25,29 @@ __all__ = ['HistogramLUTItem'] class HistogramLUTItem(GraphicsWidget): """ This is a graphicsWidget which provides controls for adjusting the display of an image. + Includes: - Image histogram - Movable region over histogram to select black/white levels - Gradient editor to define color lookup table for single-channel images + + Parameters + ---------- + image : ImageItem or None + If *image* is provided, then the control will be automatically linked to + the image and changes to the control will be immediately reflected in + the image's appearance. + fillHistogram : bool + By default, the histogram is rendered with a fill. + For performance, set *fillHistogram* = False. + rgbHistogram : bool + Sets whether the histogram is computed once over all channels of the + image, or once per channel. + levelMode : 'mono' or 'rgba' + If 'mono', then only a single set of black/whilte level lines is drawn, + and the levels apply to all channels in the image. If 'rgba', then one + set of levels is drawn for each channel. """ sigLookupTableChanged = QtCore.Signal(object) @@ -37,10 +55,6 @@ class HistogramLUTItem(GraphicsWidget): sigLevelChangeFinished = QtCore.Signal(object) def __init__(self, image=None, fillHistogram=True, rgbHistogram=False, levelMode='mono'): - """ - If *image* (ImageItem) is provided, then the control will be automatically linked to the image and changes to the control will be immediately reflected in the image's appearance. - By default, the histogram is rendered with a fill. For performance, set *fillHistogram* = False. - """ GraphicsWidget.__init__(self) self.lut = None self.imageItem = lambda: None # fake a dead weakref @@ -241,6 +255,8 @@ class HistogramLUTItem(GraphicsWidget): def getLevels(self): """Return the min and max levels. + + For rgba mode, this returns a list of the levels for each channel. """ if self.levelMode == 'mono': return self.region.getRegion() From a04db637755ceafb20cc784343f2c134157928af Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 3 Oct 2017 08:27:56 -0700 Subject: [PATCH 082/135] Include level mode in save/restore --- pyqtgraph/graphicsItems/HistogramLUTItem.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index 85cbe9cf..68448c11 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -319,8 +319,10 @@ class HistogramLUTItem(GraphicsWidget): return { 'gradient': self.gradient.saveState(), 'levels': self.getLevels(), + 'mode': self.levelMode, } def restoreState(self, state): + self.setLevelMode(state['mode']) self.gradient.restoreState(state['gradient']) self.setLevels(*state['levels']) From f4c3d88251be17f6ad8bab282d4f7cc17cd74b5e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 3 Oct 2017 15:22:31 -0700 Subject: [PATCH 083/135] Add option to join nested progress dialogs into a single window --- examples/ProgressDialog.py | 53 +++++++++++ pyqtgraph/widgets/ProgressDialog.py | 136 +++++++++++++++++++++++++++- 2 files changed, 184 insertions(+), 5 deletions(-) create mode 100644 examples/ProgressDialog.py diff --git a/examples/ProgressDialog.py b/examples/ProgressDialog.py new file mode 100644 index 00000000..08cffa7e --- /dev/null +++ b/examples/ProgressDialog.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +""" +Using ProgressDialog to show progress updates in a nested process. + +""" +import initExample ## Add path to library (just for examples; you do not need this) + +import time +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui + +app = QtGui.QApplication([]) + + +def runStage(i): + """Waste time for 3 seconds while incrementing a progress bar. + """ + with pg.ProgressDialog("Running stage %s.." % i, maximum=100, nested=True) as dlg: + for j in range(100): + time.sleep(0.03) + dlg += 1 + if dlg.wasCanceled(): + print("Canceled stage %s" % i) + break + + +def runManyStages(i): + """Iterate over runStage() 3 times while incrementing a progress bar. + """ + with pg.ProgressDialog("Running stage %s.." % i, maximum=3, nested=True, wait=0) as dlg: + for j in range(1,4): + runStage('%d.%d' % (i, j)) + dlg += 1 + if dlg.wasCanceled(): + print("Canceled stage %s" % i) + break + + +with pg.ProgressDialog("Doing a multi-stage process..", maximum=5, nested=True, wait=0) as dlg1: + for i in range(1,6): + if i == 3: + # this stage will have 3 nested progress bars + runManyStages(i) + else: + # this stage will have 2 nested progress bars + runStage(i) + + dlg1 += 1 + if dlg1.wasCanceled(): + print("Canceled process") + break + + diff --git a/pyqtgraph/widgets/ProgressDialog.py b/pyqtgraph/widgets/ProgressDialog.py index 8c669be4..7c60004b 100644 --- a/pyqtgraph/widgets/ProgressDialog.py +++ b/pyqtgraph/widgets/ProgressDialog.py @@ -2,6 +2,8 @@ from ..Qt import QtGui, QtCore __all__ = ['ProgressDialog'] + + class ProgressDialog(QtGui.QProgressDialog): """ Extends QProgressDialog for use in 'with' statements. @@ -14,7 +16,10 @@ class ProgressDialog(QtGui.QProgressDialog): if dlg.wasCanceled(): raise Exception("Processing canceled by user") """ - def __init__(self, labelText, minimum=0, maximum=100, cancelText='Cancel', parent=None, wait=250, busyCursor=False, disable=False): + + allDialogs = [] + + def __init__(self, labelText, minimum=0, maximum=100, cancelText='Cancel', parent=None, wait=250, busyCursor=False, disable=False, nested=False): """ ============== ================================================================ **Arguments:** @@ -29,6 +34,9 @@ class ProgressDialog(QtGui.QProgressDialog): and calls to wasCanceled() will always return False. If ProgressDialog is entered from a non-gui thread, it will always be disabled. + nested (bool) If True, then this progress bar will be displayed inside + any pre-existing progress dialogs that also allow nesting (if + any). ============== ================================================================ """ isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread() @@ -42,20 +50,40 @@ class ProgressDialog(QtGui.QProgressDialog): noCancel = True self.busyCursor = busyCursor - + QtGui.QProgressDialog.__init__(self, labelText, cancelText, minimum, maximum, parent) - self.setMinimumDuration(wait) + + # If this will be a nested dialog, then we ignore the wait time + if nested is True and len(ProgressDialog.allDialogs) > 0: + self.setMinimumDuration(2**30) + else: + self.setMinimumDuration(wait) + self.setWindowModality(QtCore.Qt.WindowModal) self.setValue(self.minimum()) if noCancel: self.setCancelButton(None) - + # attributes used for nesting dialogs + self.nestedLayout = None + self._nestableWidgets = None + self._nestingReady = False + self._topDialog = None + def __enter__(self): if self.disabled: return self if self.busyCursor: QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor)) + + if len(ProgressDialog.allDialogs) > 0: + topDialog = ProgressDialog.allDialogs[0] + topDialog._addSubDialog(self) + self._topDialog = topDialog + topDialog.canceled.connect(self.cancel) + + ProgressDialog.allDialogs.append(self) + return self def __exit__(self, exType, exValue, exTrace): @@ -63,6 +91,12 @@ class ProgressDialog(QtGui.QProgressDialog): return if self.busyCursor: QtGui.QApplication.restoreOverrideCursor() + + if self._topDialog is not None: + self._topDialog._removeSubDialog(self) + + ProgressDialog.allDialogs.pop(-1) + self.setValue(self.maximum()) def __iadd__(self, val): @@ -72,6 +106,94 @@ class ProgressDialog(QtGui.QProgressDialog): self.setValue(self.value()+val) return self + def _addSubDialog(self, dlg): + # insert widgets from another dialog into this one. + + # set a new layout and arrange children into it (if needed). + self._prepareNesting() + + bar, btn = dlg._extractWidgets() + bar.removed = False + + # where should we insert this widget? Find the first slot with a + # "removed" widget (that was left as a placeholder) + nw = self.nestedLayout.count() + inserted = False + if nw > 1: + for i in range(1, nw): + bar2 = self.nestedLayout.itemAt(i).widget() + if bar2.removed: + self.nestedLayout.removeWidget(bar2) + bar2.hide() + bar2.setParent(None) + self.nestedLayout.insertWidget(i, bar) + inserted = True + break + if not inserted: + self.nestedLayout.addWidget(bar) + + def _removeSubDialog(self, dlg): + # don't remove the widget just yet; instead we hide it and leave it in + # as a placeholder. + bar, btn = dlg._extractWidgets() + bar.layout().setCurrentIndex(1) # causes widgets to be hidden without changing size + bar.removed = True # mark as removed so we know we can insert another bar here later + + def _prepareNesting(self): + # extract all child widgets and place into a new layout that we can add to + if self._nestingReady is False: + # top layout contains progress bars + cancel button at the bottom + self._topLayout = QtGui.QGridLayout() + self.setLayout(self._topLayout) + self._topLayout.setContentsMargins(0, 0, 0, 0) + + # A vbox to contain all progress bars + self.nestedVBox = QtGui.QWidget() + self._topLayout.addWidget(self.nestedVBox, 0, 0, 1, 2) + self.nestedLayout = QtGui.QVBoxLayout() + self.nestedVBox.setLayout(self.nestedLayout) + + # re-insert all widgets + bar, btn = self._extractWidgets() + self.nestedLayout.addWidget(bar) + self._topLayout.addWidget(btn, 1, 1, 1, 1) + self._topLayout.setColumnStretch(0, 100) + self._topLayout.setColumnStretch(1, 1) + self._topLayout.setRowStretch(0, 100) + self._topLayout.setRowStretch(1, 1) + + self._nestingReady = True + + def _extractWidgets(self): + # return a single widget containing all sub-widgets nicely arranged + if self._nestableWidgets is None: + widgets = [ch for ch in self.children() if isinstance(ch, QtGui.QWidget)] + label = [ch for ch in self.children() if isinstance(ch, QtGui.QLabel)][0] + bar = [ch for ch in self.children() if isinstance(ch, QtGui.QProgressBar)][0] + btn = [ch for ch in self.children() if isinstance(ch, QtGui.QPushButton)][0] + + # join label and bar into a stacked layout so they can be hidden + # without changing size + sw = QtGui.QWidget() + sl = QtGui.QStackedLayout() + sw.setLayout(sl) + sl.setContentsMargins(0, 0, 0, 0) + + # inside the stacked layout, the bar and label are in a vbox + w = QtGui.QWidget() + sl.addWidget(w) + l = QtGui.QVBoxLayout() + w.setLayout(l) + l.addWidget(label) + l.addWidget(bar) + + # add a blank page to the stacked layout + blank = QtGui.QWidget() + sl.addWidget(blank) + + self._nestableWidgets = (sw, btn) + + return self._nestableWidgets ## wrap all other functions to make sure they aren't being called from non-gui threads @@ -80,6 +202,11 @@ class ProgressDialog(QtGui.QProgressDialog): return QtGui.QProgressDialog.setValue(self, val) + # Qt docs say this should happen automatically, but that doesn't seem + # to be the case. + if self.windowModality() == QtCore.Qt.WindowModal: + QtGui.QApplication.processEvents() + def setLabelText(self, val): if self.disabled: return @@ -109,4 +236,3 @@ class ProgressDialog(QtGui.QProgressDialog): if self.disabled: return 0 return QtGui.QProgressDialog.minimum(self) - From e6507f860176ad6d3ce5d68e425afb8bc3de610e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 3 Oct 2017 17:14:32 -0700 Subject: [PATCH 084/135] try a different approach to managing nested bars.. --- pyqtgraph/widgets/ProgressDialog.py | 99 +++++++++++++++++------------ 1 file changed, 58 insertions(+), 41 deletions(-) diff --git a/pyqtgraph/widgets/ProgressDialog.py b/pyqtgraph/widgets/ProgressDialog.py index 7c60004b..4964771d 100644 --- a/pyqtgraph/widgets/ProgressDialog.py +++ b/pyqtgraph/widgets/ProgressDialog.py @@ -39,6 +39,13 @@ class ProgressDialog(QtGui.QProgressDialog): any). ============== ================================================================ """ + # attributes used for nesting dialogs + self.nestedLayout = None + self._nestableWidgets = None + self._nestingReady = False + self._topDialog = None + self._subBars = [] + isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread() self.disabled = disable or (not isGuiThread) if self.disabled: @@ -63,12 +70,6 @@ class ProgressDialog(QtGui.QProgressDialog): self.setValue(self.minimum()) if noCancel: self.setCancelButton(None) - - # attributes used for nesting dialogs - self.nestedLayout = None - self._nestableWidgets = None - self._nestingReady = False - self._topDialog = None def __enter__(self): if self.disabled: @@ -113,31 +114,32 @@ class ProgressDialog(QtGui.QProgressDialog): self._prepareNesting() bar, btn = dlg._extractWidgets() - bar.removed = False # where should we insert this widget? Find the first slot with a # "removed" widget (that was left as a placeholder) - nw = self.nestedLayout.count() inserted = False - if nw > 1: - for i in range(1, nw): - bar2 = self.nestedLayout.itemAt(i).widget() - if bar2.removed: - self.nestedLayout.removeWidget(bar2) - bar2.hide() - bar2.setParent(None) - self.nestedLayout.insertWidget(i, bar) - inserted = True - break + for i,bar2 in enumerate(self._subBars): + if bar2.hidden: + self._subBars.pop(i) + bar2.hide() + bar2.setParent(None) + self._subBars.insert(i, bar) + inserted = True + break if not inserted: - self.nestedLayout.addWidget(bar) - + self._subBars.append(bar) + + # reset the layout + while self.nestedLayout.count() > 0: + self.nestedLayout.takeAt(0) + for b in self._subBars: + self.nestedLayout.addWidget(b) + def _removeSubDialog(self, dlg): # don't remove the widget just yet; instead we hide it and leave it in # as a placeholder. bar, btn = dlg._extractWidgets() - bar.layout().setCurrentIndex(1) # causes widgets to be hidden without changing size - bar.removed = True # mark as removed so we know we can insert another bar here later + bar.hide() def _prepareNesting(self): # extract all child widgets and place into a new layout that we can add to @@ -156,6 +158,7 @@ class ProgressDialog(QtGui.QProgressDialog): # re-insert all widgets bar, btn = self._extractWidgets() self.nestedLayout.addWidget(bar) + self._subBars.append(bar) self._topLayout.addWidget(btn, 1, 1, 1, 1) self._topLayout.setColumnStretch(0, 100) self._topLayout.setColumnStretch(1, 1) @@ -165,31 +168,17 @@ class ProgressDialog(QtGui.QProgressDialog): self._nestingReady = True def _extractWidgets(self): - # return a single widget containing all sub-widgets nicely arranged + # return: + # 1. a single widget containing the label and progress bar + # 2. the cancel button + if self._nestableWidgets is None: widgets = [ch for ch in self.children() if isinstance(ch, QtGui.QWidget)] label = [ch for ch in self.children() if isinstance(ch, QtGui.QLabel)][0] bar = [ch for ch in self.children() if isinstance(ch, QtGui.QProgressBar)][0] btn = [ch for ch in self.children() if isinstance(ch, QtGui.QPushButton)][0] - # join label and bar into a stacked layout so they can be hidden - # without changing size - sw = QtGui.QWidget() - sl = QtGui.QStackedLayout() - sw.setLayout(sl) - sl.setContentsMargins(0, 0, 0, 0) - - # inside the stacked layout, the bar and label are in a vbox - w = QtGui.QWidget() - sl.addWidget(w) - l = QtGui.QVBoxLayout() - w.setLayout(l) - l.addWidget(label) - l.addWidget(bar) - - # add a blank page to the stacked layout - blank = QtGui.QWidget() - sl.addWidget(blank) + sw = ProgressWidget(label, bar) self._nestableWidgets = (sw, btn) @@ -202,6 +191,11 @@ class ProgressDialog(QtGui.QProgressDialog): return QtGui.QProgressDialog.setValue(self, val) + if self._topDialog is not None: + tbar = self._topDialog._extractWidgets()[0].bar + tlab = self._topDialog._extractWidgets()[0].label + print(tlab.pos(), tbar.pos()) + # Qt docs say this should happen automatically, but that doesn't seem # to be the case. if self.windowModality() == QtCore.Qt.WindowModal: @@ -236,3 +230,26 @@ class ProgressDialog(QtGui.QProgressDialog): if self.disabled: return 0 return QtGui.QProgressDialog.minimum(self) + + +class ProgressWidget(QtGui.QWidget): + def __init__(self, label, bar): + QtGui.QWidget.__init__(self) + self.hidden = False + self.layout = QtGui.QVBoxLayout() + self.setLayout(self.layout) + + self.label = label + self.bar = bar + self.layout.addWidget(label) + self.layout.addWidget(bar) + + def eventFilter(self, obj, ev): + return ev.type() == QtCore.QEvent.Paint + + def hide(self): + # hide label and bar, but continue occupying the same space in the layout + for widget in (self.label, self.bar): + widget.installEventFilter(self) + widget.update() + self.hidden = True From f1de464c460e9cbdb0987a4cf85b76930e88ad18 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 4 Oct 2017 08:30:38 -0700 Subject: [PATCH 085/135] Preserve levels when switching between mono and rgba modes --- pyqtgraph/graphicsItems/HistogramLUTItem.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index 68448c11..019fa3a7 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -288,9 +288,21 @@ class HistogramLUTItem(GraphicsWidget): Options are 'mono' or 'rgba'. """ assert mode in ('mono', 'rgba') + + oldLevels = self.getLevels() + self.levelMode = mode self._showRegions() self.imageChanged() + + # do our best to preserve old levels + if mode == 'mono': + levels = np.array(oldLevels).mean(axis=0) + self.setLevels(*levels) + else: + levels = [oldLevels] * 4 + self.setLevels(rgba=levels) + self.update() def _showRegions(self): From ce15f4530ac8a343520ccb2923bbe9a8f04b3978 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 4 Oct 2017 08:34:42 -0700 Subject: [PATCH 086/135] Fix: image levels reset to mono after drag release --- pyqtgraph/graphicsItems/HistogramLUTItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index 019fa3a7..e6d692e6 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -199,7 +199,7 @@ class HistogramLUTItem(GraphicsWidget): def regionChanged(self): if self.imageItem() is not None: - self.imageItem().setLevels(self.region.getRegion()) + self.imageItem().setLevels(self.getLevels()) self.sigLevelChangeFinished.emit(self) def regionChanging(self): From 7c1a6ecb1afcf4f1012c367380d7195732368380 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 4 Oct 2017 09:01:51 -0700 Subject: [PATCH 087/135] Prevent dialog from moving label/bar widgets on resize when nested --- examples/ProgressDialog.py | 4 ++-- pyqtgraph/widgets/ProgressDialog.py | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/examples/ProgressDialog.py b/examples/ProgressDialog.py index 08cffa7e..141d2bb4 100644 --- a/examples/ProgressDialog.py +++ b/examples/ProgressDialog.py @@ -13,11 +13,11 @@ app = QtGui.QApplication([]) def runStage(i): - """Waste time for 3 seconds while incrementing a progress bar. + """Waste time for 2 seconds while incrementing a progress bar. """ with pg.ProgressDialog("Running stage %s.." % i, maximum=100, nested=True) as dlg: for j in range(100): - time.sleep(0.03) + time.sleep(0.02) dlg += 1 if dlg.wasCanceled(): print("Canceled stage %s" % i) diff --git a/pyqtgraph/widgets/ProgressDialog.py b/pyqtgraph/widgets/ProgressDialog.py index 4964771d..de3c6dc4 100644 --- a/pyqtgraph/widgets/ProgressDialog.py +++ b/pyqtgraph/widgets/ProgressDialog.py @@ -183,6 +183,12 @@ class ProgressDialog(QtGui.QProgressDialog): self._nestableWidgets = (sw, btn) return self._nestableWidgets + + def resizeEvent(self, ev): + if self._nestingReady: + # don't let progress dialog manage widgets anymore. + return + return QtGui.QProgressDialog.resizeEvent(self, ev) ## wrap all other functions to make sure they aren't being called from non-gui threads From 384975dd464b0daa6d93dc26ec8859c24941ca79 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 4 Oct 2017 09:03:08 -0700 Subject: [PATCH 088/135] Cleanup --- pyqtgraph/widgets/ProgressDialog.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyqtgraph/widgets/ProgressDialog.py b/pyqtgraph/widgets/ProgressDialog.py index de3c6dc4..e62a6551 100644 --- a/pyqtgraph/widgets/ProgressDialog.py +++ b/pyqtgraph/widgets/ProgressDialog.py @@ -200,7 +200,6 @@ class ProgressDialog(QtGui.QProgressDialog): if self._topDialog is not None: tbar = self._topDialog._extractWidgets()[0].bar tlab = self._topDialog._extractWidgets()[0].label - print(tlab.pos(), tbar.pos()) # Qt docs say this should happen automatically, but that doesn't seem # to be the case. From d2942c7acadef3910311c21b5e72cb3cbd854aa3 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 4 Oct 2017 09:06:05 -0700 Subject: [PATCH 089/135] Fix: obey nested option --- pyqtgraph/widgets/ProgressDialog.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/widgets/ProgressDialog.py b/pyqtgraph/widgets/ProgressDialog.py index e62a6551..6bda4b95 100644 --- a/pyqtgraph/widgets/ProgressDialog.py +++ b/pyqtgraph/widgets/ProgressDialog.py @@ -45,6 +45,7 @@ class ProgressDialog(QtGui.QProgressDialog): self._nestingReady = False self._topDialog = None self._subBars = [] + self.nested = nested isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread() self.disabled = disable or (not isGuiThread) @@ -77,7 +78,7 @@ class ProgressDialog(QtGui.QProgressDialog): if self.busyCursor: QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor)) - if len(ProgressDialog.allDialogs) > 0: + if self.nested and len(ProgressDialog.allDialogs) > 0: topDialog = ProgressDialog.allDialogs[0] topDialog._addSubDialog(self) self._topDialog = topDialog From e2c991851031653ea53ab0600e1409f4f093663b Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 4 Oct 2017 09:11:44 -0700 Subject: [PATCH 090/135] docs cleanup --- pyqtgraph/widgets/ProgressDialog.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/pyqtgraph/widgets/ProgressDialog.py b/pyqtgraph/widgets/ProgressDialog.py index 6bda4b95..ae1826bb 100644 --- a/pyqtgraph/widgets/ProgressDialog.py +++ b/pyqtgraph/widgets/ProgressDialog.py @@ -6,7 +6,10 @@ __all__ = ['ProgressDialog'] class ProgressDialog(QtGui.QProgressDialog): """ - Extends QProgressDialog for use in 'with' statements. + Extends QProgressDialog: + + * Adds context management so the dialog may be used in `with` statements + * Allows nesting multiple progress dialogs Example:: @@ -35,8 +38,7 @@ class ProgressDialog(QtGui.QProgressDialog): If ProgressDialog is entered from a non-gui thread, it will always be disabled. nested (bool) If True, then this progress bar will be displayed inside - any pre-existing progress dialogs that also allow nesting (if - any). + any pre-existing progress dialogs that also allow nesting. ============== ================================================================ """ # attributes used for nesting dialogs @@ -198,10 +200,6 @@ class ProgressDialog(QtGui.QProgressDialog): return QtGui.QProgressDialog.setValue(self, val) - if self._topDialog is not None: - tbar = self._topDialog._extractWidgets()[0].bar - tlab = self._topDialog._extractWidgets()[0].label - # Qt docs say this should happen automatically, but that doesn't seem # to be the case. if self.windowModality() == QtCore.Qt.WindowModal: @@ -239,6 +237,9 @@ class ProgressDialog(QtGui.QProgressDialog): class ProgressWidget(QtGui.QWidget): + """Container for a label + progress bar that also allows its child widgets + to be hidden without changing size. + """ def __init__(self, label, bar): QtGui.QWidget.__init__(self) self.hidden = False From 9ef9f73be5b5cdb639931a4f8e68dbfe98fdbbea Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 4 Oct 2017 09:35:57 -0700 Subject: [PATCH 091/135] minor import and test corrections --- pyqtgraph/functions.py | 2 +- pyqtgraph/tests/test_functions.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index b38888fe..8ef57742 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -15,7 +15,7 @@ from .python2_3 import asUnicode, basestring from .Qt import QtGui, QtCore, USE_PYSIDE from . import getConfigOption, setConfigOptions from . import debug - +from .metaarray import MetaArray Colors = { diff --git a/pyqtgraph/tests/test_functions.py b/pyqtgraph/tests/test_functions.py index eff56635..68f3dc24 100644 --- a/pyqtgraph/tests/test_functions.py +++ b/pyqtgraph/tests/test_functions.py @@ -344,14 +344,14 @@ def test_eq(): a2 = a1 + 1 a3 = a2.astype('int') a4 = np.empty((0, 20)) - assert not eq(a1, a2) - assert not eq(a1, a3) - assert not eq(a1, a4) + assert not eq(a1, a2) # same shape/dtype, different values + assert not eq(a1, a3) # same shape, different dtype and values + assert not eq(a1, a4) # different shape (note: np.all gives True if one array has size 0) - assert eq(a2, a3) - assert not eq(a2, a4) + assert not eq(a2, a3) # same values, but different dtype + assert not eq(a2, a4) # different shape - assert not eq(a3, a4) + assert not eq(a3, a4) # different shape and dtype assert eq(a4, a4.copy()) assert not eq(a4, a4.T) From 0c28de5fd80f73394664e0eba888825c6714ad25 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 4 Oct 2017 10:24:34 -0700 Subject: [PATCH 092/135] Fix subArray when input data is discontiguous --- pyqtgraph/functions.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 8ef57742..7ad603f7 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -747,8 +747,7 @@ def subArray(data, offset, shape, stride): the input in the example above to have shape (10, 7) would cause the output to have shape (2, 3, 7). """ - #data = data.flatten() - data = data[offset:] + data = np.ascontiguousarray(data)[offset:] shape = tuple(shape) extraShape = data.shape[1:] From 8a882b516a6dddbe7ad45b118fb66150b92d56c9 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 5 Oct 2017 10:46:39 -0700 Subject: [PATCH 093/135] Fix: InvisibleRootItem is no longer a subclass of QTreeWidgetItem The __getattr__ method is supposed to wrap attributes from the internal TreeWidgetItem, but this was broken because the superclass had already implemented these. --- pyqtgraph/widgets/TreeWidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/widgets/TreeWidget.py b/pyqtgraph/widgets/TreeWidget.py index a37181cf..096227ab 100644 --- a/pyqtgraph/widgets/TreeWidget.py +++ b/pyqtgraph/widgets/TreeWidget.py @@ -350,7 +350,7 @@ class TreeWidgetItem(QtGui.QTreeWidgetItem): """ -class InvisibleRootItem(QtGui.QTreeWidgetItem): +class InvisibleRootItem(object): """Wrapper around a TreeWidget's invisible root item that calls TreeWidget.informTreeWidgetChange when child items are added/removed. """ From 5aa2a1998fac17a1972309d2ee4256a2c80ece54 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 5 Oct 2017 12:42:20 -0700 Subject: [PATCH 094/135] Override qAbort on slot exceptions for PyQt>=5.5 --- pyqtgraph/Qt.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pyqtgraph/Qt.py b/pyqtgraph/Qt.py index 2ed9d6f9..ad04cd76 100644 --- a/pyqtgraph/Qt.py +++ b/pyqtgraph/Qt.py @@ -151,6 +151,17 @@ elif QT_LIB == PYQT5: # We're using PyQt5 which has a different structure so we're going to use a shim to # recreate the Qt4 structure for Qt5 from PyQt5 import QtGui, QtCore, QtWidgets, uic + + # PyQt5, starting in v5.5, calls qAbort when an exception is raised inside + # a slot. To maintain backward compatibility (and sanity for interactive + # users), we install a global exception hook to override this behavior. + ver = QtCore.PYQT_VERSION_STR.split('.') + if int(ver[1]) >= 5: + sys_excepthook = sys.excepthook + def pyqt5_qabort_override(*args, **kwds): + return sys_excepthook(*args, **kwds) + sys.excepthook = pyqt5_qabort_override + try: from PyQt5 import QtSvg except ImportError: From d32454ebb853c7e09153bdf9a256e8bbf48687ec Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 11 Oct 2017 09:05:32 -0700 Subject: [PATCH 095/135] Don't use ORderedDict backport on python 3 --- pyqtgraph/ordereddict.py | 182 ++++++++++++++++++++------------------- 1 file changed, 93 insertions(+), 89 deletions(-) diff --git a/pyqtgraph/ordereddict.py b/pyqtgraph/ordereddict.py index 7242b506..fb37037f 100644 --- a/pyqtgraph/ordereddict.py +++ b/pyqtgraph/ordereddict.py @@ -20,108 +20,112 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. -from UserDict import DictMixin +import sys +if sys.version[0] > '2': + from collections import OrderedDict +else: + from UserDict import DictMixin -class OrderedDict(dict, DictMixin): + class OrderedDict(dict, DictMixin): - def __init__(self, *args, **kwds): - if len(args) > 1: - raise TypeError('expected at most 1 arguments, got %d' % len(args)) - try: - self.__end - except AttributeError: - self.clear() - self.update(*args, **kwds) + def __init__(self, *args, **kwds): + if len(args) > 1: + raise TypeError('expected at most 1 arguments, got %d' % len(args)) + try: + self.__end + except AttributeError: + self.clear() + self.update(*args, **kwds) - def clear(self): - self.__end = end = [] - end += [None, end, end] # sentinel node for doubly linked list - self.__map = {} # key --> [key, prev, next] - dict.clear(self) + def clear(self): + self.__end = end = [] + end += [None, end, end] # sentinel node for doubly linked list + self.__map = {} # key --> [key, prev, next] + dict.clear(self) - def __setitem__(self, key, value): - if key not in self: + def __setitem__(self, key, value): + if key not in self: + end = self.__end + curr = end[1] + curr[2] = end[1] = self.__map[key] = [key, curr, end] + dict.__setitem__(self, key, value) + + def __delitem__(self, key): + dict.__delitem__(self, key) + key, prev, next = self.__map.pop(key) + prev[2] = next + next[1] = prev + + def __iter__(self): + end = self.__end + curr = end[2] + while curr is not end: + yield curr[0] + curr = curr[2] + + def __reversed__(self): end = self.__end curr = end[1] - curr[2] = end[1] = self.__map[key] = [key, curr, end] - dict.__setitem__(self, key, value) + while curr is not end: + yield curr[0] + curr = curr[1] - def __delitem__(self, key): - dict.__delitem__(self, key) - key, prev, next = self.__map.pop(key) - prev[2] = next - next[1] = prev + def popitem(self, last=True): + if not self: + raise KeyError('dictionary is empty') + if last: + key = reversed(self).next() + else: + key = iter(self).next() + value = self.pop(key) + return key, value - def __iter__(self): - end = self.__end - curr = end[2] - while curr is not end: - yield curr[0] - curr = curr[2] + def __reduce__(self): + items = [[k, self[k]] for k in self] + tmp = self.__map, self.__end + del self.__map, self.__end + inst_dict = vars(self).copy() + self.__map, self.__end = tmp + if inst_dict: + return (self.__class__, (items,), inst_dict) + return self.__class__, (items,) - def __reversed__(self): - end = self.__end - curr = end[1] - while curr is not end: - yield curr[0] - curr = curr[1] + def keys(self): + return list(self) - def popitem(self, last=True): - if not self: - raise KeyError('dictionary is empty') - if last: - key = reversed(self).next() - else: - key = iter(self).next() - value = self.pop(key) - return key, value + setdefault = DictMixin.setdefault + update = DictMixin.update + pop = DictMixin.pop + values = DictMixin.values + items = DictMixin.items + iterkeys = DictMixin.iterkeys + itervalues = DictMixin.itervalues + iteritems = DictMixin.iteritems - def __reduce__(self): - items = [[k, self[k]] for k in self] - tmp = self.__map, self.__end - del self.__map, self.__end - inst_dict = vars(self).copy() - self.__map, self.__end = tmp - if inst_dict: - return (self.__class__, (items,), inst_dict) - return self.__class__, (items,) + def __repr__(self): + if not self: + return '%s()' % (self.__class__.__name__,) + return '%s(%r)' % (self.__class__.__name__, self.items()) - def keys(self): - return list(self) + def copy(self): + return self.__class__(self) - setdefault = DictMixin.setdefault - update = DictMixin.update - pop = DictMixin.pop - values = DictMixin.values - items = DictMixin.items - iterkeys = DictMixin.iterkeys - itervalues = DictMixin.itervalues - iteritems = DictMixin.iteritems + @classmethod + def fromkeys(cls, iterable, value=None): + d = cls() + for key in iterable: + d[key] = value + return d - def __repr__(self): - if not self: - return '%s()' % (self.__class__.__name__,) - return '%s(%r)' % (self.__class__.__name__, self.items()) - - def copy(self): - return self.__class__(self) - - @classmethod - def fromkeys(cls, iterable, value=None): - d = cls() - for key in iterable: - d[key] = value - return d - - def __eq__(self, other): - if isinstance(other, OrderedDict): - if len(self) != len(other): - return False - for p, q in zip(self.items(), other.items()): - if p != q: + def __eq__(self, other): + if isinstance(other, OrderedDict): + if len(self) != len(other): return False - return True - return dict.__eq__(self, other) + for p, q in zip(self.items(), other.items()): + if p != q: + return False + return True + return dict.__eq__(self, other) - def __ne__(self, other): - return not self == other + def __ne__(self, other): + return not self == other From 89993ce700238fcbcc046ee6e69ce98e07f45374 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 11 Oct 2017 09:11:16 -0700 Subject: [PATCH 096/135] Add simple script for invoking pytest --- test.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 test.py diff --git a/test.py b/test.py new file mode 100644 index 00000000..b07fb1cf --- /dev/null +++ b/test.py @@ -0,0 +1,24 @@ +""" +Script for invoking pytest with options to select Qt library +""" + +import sys +import pytest + +args = sys.argv[1:] +if '--pyside' in args: + args.remove('--pyside') + import PySide +elif '--pyqt4' in args: + args.remove('--pyqt4') + import PyQt4 +elif '--pyqt5' in args: + args.remove('--pyqt5') + import PyQt5 + +import pyqtgraph as pg +pg.systemInfo() + +pytest.main(args) + + \ No newline at end of file From c678094f2507302edd448ad08f9eaa6a837eece7 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 17 Oct 2017 10:47:41 -0700 Subject: [PATCH 097/135] Make TreeWidget.invisibleRootItem return a singleton --- pyqtgraph/widgets/TreeWidget.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/widgets/TreeWidget.py b/pyqtgraph/widgets/TreeWidget.py index 096227ab..b0ec54c1 100644 --- a/pyqtgraph/widgets/TreeWidget.py +++ b/pyqtgraph/widgets/TreeWidget.py @@ -17,7 +17,11 @@ class TreeWidget(QtGui.QTreeWidget): def __init__(self, parent=None): QtGui.QTreeWidget.__init__(self, parent) - #self.itemWidgets = WeakKeyDictionary() + + # wrap this item so that we can propagate tree change information + # to children. + self._invRootItem = InvisibleRootItem(QtGui.QTreeWidget.invisibleRootItem(self)) + self.setAcceptDrops(True) self.setDragEnabled(True) self.setEditTriggers(QtGui.QAbstractItemView.EditKeyPressed|QtGui.QAbstractItemView.SelectedClicked) @@ -210,9 +214,7 @@ class TreeWidget(QtGui.QTreeWidget): #self.informTreeWidgetChange(item) def invisibleRootItem(self): - # wrap this item so that we can propagate tree change information - # to children. - return InvisibleRootItem(QtGui.QTreeWidget.invisibleRootItem(self)) + return self._invRootItem def itemFromIndex(self, index): """Return the item and column corresponding to a QModelIndex. From 79eebe1c02f09527afeb61a89c8949611683b953 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 17 Oct 2017 20:42:38 -0700 Subject: [PATCH 098/135] code cleanup --- pyqtgraph/graphicsItems/ROI.py | 133 --------------------------------- 1 file changed, 133 deletions(-) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 9f3dae6f..319524e9 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -113,7 +113,6 @@ class ROI(GraphicsObject): sigRemoveRequested = QtCore.Signal(object) def __init__(self, pos, size=Point(1, 1), angle=0.0, invertible=False, maxBounds=None, snapSize=1.0, scaleSnap=False, translateSnap=False, rotateSnap=False, parent=None, pen=None, movable=True, removable=False): - #QObjectWorkaround.__init__(self) GraphicsObject.__init__(self, parent) self.setAcceptedMouseButtons(QtCore.Qt.NoButton) pos = Point(pos) @@ -148,7 +147,6 @@ class ROI(GraphicsObject): self.translateSnap = translateSnap self.rotateSnap = rotateSnap self.scaleSnap = scaleSnap - #self.setFlag(self.ItemIsSelectable, True) def getState(self): return self.stateCopy() @@ -268,7 +266,6 @@ class ROI(GraphicsObject): raise TypeError("update argument must be bool") self.state['angle'] = angle tr = QtGui.QTransform() - #tr.rotate(-angle * 180 / np.pi) tr.rotate(angle) self.setTransform(tr) if update: @@ -316,20 +313,14 @@ class ROI(GraphicsObject): newState = self.stateCopy() newState['pos'] = newState['pos'] + pt - ## snap position - #snap = kargs.get('snap', None) - #if (snap is not False) and not (snap is None and self.translateSnap is False): - snap = kargs.get('snap', None) if snap is None: snap = self.translateSnap if snap is not False: newState['pos'] = self.getSnapPosition(newState['pos'], snap=snap) - #d = ev.scenePos() - self.mapToScene(self.pressPos) if self.maxBounds is not None: r = self.stateRect(newState) - #r0 = self.sceneTransform().mapRect(self.boundingRect()) d = Point(0,0) if self.maxBounds.left() > r.left(): d[0] = self.maxBounds.left() - r.left() @@ -341,12 +332,9 @@ class ROI(GraphicsObject): d[1] = self.maxBounds.bottom() - r.bottom() newState['pos'] += d - #self.state['pos'] = newState['pos'] update = kargs.get('update', True) finish = kargs.get('finish', True) self.setPos(newState['pos'], update=update, finish=finish) - #if 'update' not in kargs or kargs['update'] is True: - #self.stateChanged() def rotate(self, angle, update=True, finish=True): """ @@ -629,7 +617,6 @@ class ROI(GraphicsObject): for h in self.handles: h['item'].hide() - def hoverEvent(self, ev): hover = False if not ev.isExit(): @@ -870,10 +857,8 @@ class ROI(GraphicsObject): r = self.stateRect(newState) if not self.maxBounds.contains(r): return - #self.setTransform(tr) self.setPos(newState['pos'], update=False) self.setAngle(ang, update=False) - #self.state = newState ## If this is a free-rotate handle, its distance from the center may change. @@ -898,7 +883,6 @@ class ROI(GraphicsObject): if ang is None: return if self.rotateSnap or (modifiers & QtCore.Qt.ControlModifier): - #ang = round(ang / (np.pi/12.)) * (np.pi/12.) ang = round(ang / 15.) * 15. hs = abs(h['pos'][scaleAxis] - c[scaleAxis]) @@ -949,9 +933,6 @@ class ROI(GraphicsObject): if h['item'] in self.childItems(): p = h['pos'] h['item'].setPos(h['pos'] * self.state['size']) - #else: - # trans = self.state['pos']-self.lastState['pos'] - # h['item'].setPos(h['pos'] + h['item'].parentItem().mapFromParent(trans)) self.update() self.sigRegionChanged.emit(self) @@ -971,12 +952,10 @@ class ROI(GraphicsObject): def stateRect(self, state): r = QtCore.QRectF(0, 0, state['size'][0], state['size'][1]) tr = QtGui.QTransform() - #tr.rotate(-state['angle'] * 180 / np.pi) tr.rotate(-state['angle']) r = tr.mapRect(r) return r.adjusted(state['pos'][0], state['pos'][1], state['pos'][0], state['pos'][1]) - def getSnapPosition(self, pos, snap=None): ## Given that pos has been requested, return the nearest snap-to position ## optionally, snap may be passed in to specify a rectangular snap grid. @@ -996,7 +975,6 @@ class ROI(GraphicsObject): return QtCore.QRectF(0, 0, self.state['size'][0], self.state['size'][1]).normalized() def paint(self, p, opt, widget): - # p.save() # Note: don't use self.boundingRect here, because subclasses may need to redefine it. r = QtCore.QRectF(0, 0, self.state['size'][0], self.state['size'][1]).normalized() @@ -1005,7 +983,6 @@ class ROI(GraphicsObject): p.translate(r.left(), r.top()) p.scale(r.width(), r.height()) p.drawRect(0, 0, 1, 1) - # p.restore() def getArraySlice(self, data, img, axes=(0,1), returnSlice=True): """Return a tuple of slice objects that can be used to slice the region @@ -1133,11 +1110,8 @@ class ROI(GraphicsObject): lvx = np.sqrt(vx.x()**2 + vx.y()**2) lvy = np.sqrt(vy.x()**2 + vy.y()**2) - #pxLen = img.width() / float(data.shape[axes[0]]) ##img.width is number of pixels, not width of item. ##need pxWidth and pxHeight instead of pxLen ? - #sx = pxLen / lvx - #sy = pxLen / lvy sx = 1.0 / lvx sy = 1.0 / lvy @@ -1167,7 +1141,6 @@ class ROI(GraphicsObject): if width == 0 or height == 0: return np.empty((width, height), dtype=float) - # QImage(width, height, format) im = QtGui.QImage(width, height, QtGui.QImage.Format_ARGB32) im.fill(0x0) p = QtGui.QPainter(im) @@ -1197,27 +1170,6 @@ class ROI(GraphicsObject): t1 = SRTTransform(relativeTo) t2 = SRTTransform(st) return t2/t1 - - - #st = self.getState() - - ### rotation - #ang = (st['angle']-relativeTo['angle']) * 180. / 3.14159265358 - #rot = QtGui.QTransform() - #rot.rotate(-ang) - - ### We need to come up with a universal transformation--one that can be applied to other objects - ### such that all maintain alignment. - ### More specifically, we need to turn the ROI's position and angle into - ### a rotation _around the origin_ and a translation. - - #p0 = Point(relativeTo['pos']) - - ### base position, rotated - #p1 = rot.map(p0) - - #trans = Point(st['pos']) - p1 - #return trans, ang def applyGlobalTransform(self, tr): st = self.getState() @@ -1239,8 +1191,6 @@ class Handle(UIGraphicsItem): Handles may be dragged to change the position, size, orientation, or other properties of the ROI they are attached to. - - """ types = { ## defines number of sides, start angle for each handle type 't': (4, np.pi/4), @@ -1255,9 +1205,6 @@ class Handle(UIGraphicsItem): sigRemoveRequested = QtCore.Signal(object) # self def __init__(self, radius, typ=None, pen=(200, 200, 220), parent=None, deletable=False): - #print " create item with parent", parent - #self.bounds = QtCore.QRectF(-1e-10, -1e-10, 2e-10, 2e-10) - #self.setFlags(self.ItemIgnoresTransformations | self.ItemSendsScenePositionChanges) self.rois = [] self.radius = radius self.typ = typ @@ -1276,7 +1223,6 @@ class Handle(UIGraphicsItem): self.deletable = deletable if deletable: self.setAcceptedMouseButtons(QtCore.Qt.RightButton) - #self.updateShape() self.setZValue(11) def connectROI(self, roi): @@ -1285,13 +1231,6 @@ class Handle(UIGraphicsItem): def disconnectROI(self, roi): self.rois.remove(roi) - #for i, r in enumerate(self.roi): - #if r[0] == roi: - #self.roi.pop(i) - - #def close(self): - #for r in self.roi: - #r.removeHandle(self) def setDeletable(self, b): self.deletable = b @@ -1317,21 +1256,12 @@ class Handle(UIGraphicsItem): else: self.currentPen = self.pen self.update() - #if (not ev.isExit()) and ev.acceptDrags(QtCore.Qt.LeftButton): - #self.currentPen = fn.mkPen(255, 255,0) - #else: - #self.currentPen = self.pen - #self.update() - - def mouseClickEvent(self, ev): ## right-click cancels drag if ev.button() == QtCore.Qt.RightButton and self.isMoving: self.isMoving = False ## prevents any further motion self.movePoint(self.startPos, finish=True) - #for r in self.roi: - #r[0].cancelMove() ev.accept() elif int(ev.button() & self.acceptedMouseButtons()) > 0: ev.accept() @@ -1340,12 +1270,6 @@ class Handle(UIGraphicsItem): self.sigClicked.emit(self, ev) else: ev.ignore() - - #elif self.deletable: - #ev.accept() - #self.raiseContextMenu(ev) - #else: - #ev.ignore() def buildMenu(self): menu = QtGui.QMenu() @@ -1416,36 +1340,10 @@ class Handle(UIGraphicsItem): self.path.lineTo(x, y) def paint(self, p, opt, widget): - ### determine rotation of transform - #m = self.sceneTransform() - ##mi = m.inverted()[0] - #v = m.map(QtCore.QPointF(1, 0)) - m.map(QtCore.QPointF(0, 0)) - #va = np.arctan2(v.y(), v.x()) - - ### Determine length of unit vector in painter's coords - ##size = mi.map(Point(self.radius, self.radius)) - mi.map(Point(0, 0)) - ##size = (size.x()*size.x() + size.y() * size.y()) ** 0.5 - #size = self.radius - - #bounds = QtCore.QRectF(-size, -size, size*2, size*2) - #if bounds != self.bounds: - #self.bounds = bounds - #self.prepareGeometryChange() p.setRenderHints(p.Antialiasing, True) p.setPen(self.currentPen) - #p.rotate(va * 180. / 3.1415926) - #p.drawPath(self.path) p.drawPath(self.shape()) - #ang = self.startAng + va - #dt = 2*np.pi / self.sides - #for i in range(0, self.sides): - #x1 = size * cos(ang) - #y1 = size * sin(ang) - #x2 = size * cos(ang+dt) - #y2 = size * sin(ang+dt) - #ang += dt - #p.drawLine(Point(x1, y1), Point(x2, y2)) def shape(self): if self._shape is None: @@ -1457,18 +1355,10 @@ class Handle(UIGraphicsItem): return self._shape def boundingRect(self): - #print 'roi:', self.roi s1 = self.shape() - #print " s1:", s1 - #s2 = self.shape() - #print " s2:", s2 - return self.shape().boundingRect() def generateShape(self): - ## determine rotation of transform - #m = self.sceneTransform() ## Qt bug: do not access sceneTransform() until we know this object has a scene. - #mi = m.inverted()[0] dt = self.deviceTransform() if dt is None: @@ -1486,22 +1376,15 @@ class Handle(UIGraphicsItem): return dti.map(tr.map(self.path)) - def viewTransformChanged(self): GraphicsObject.viewTransformChanged(self) self._shape = None ## invalidate shape, recompute later if requested. self.update() - - #def itemChange(self, change, value): - #if change == self.ItemScenePositionHasChanged: - #self.updateShape() class TestROI(ROI): def __init__(self, pos, size, **args): - #QtGui.QGraphicsRectItem.__init__(self, pos[0], pos[1], size[0], size[1]) ROI.__init__(self, pos, size, **args) - #self.addTranslateHandle([0, 0]) self.addTranslateHandle([0.5, 0.5]) self.addScaleHandle([1, 1], [0, 0]) self.addScaleHandle([0, 0], [1, 1]) @@ -1511,7 +1394,6 @@ class TestROI(ROI): self.addRotateHandle([0, 1], [1, 1]) - class RectROI(ROI): """ Rectangular ROI subclass with a single scale handle at the top-right corner. @@ -1530,14 +1412,12 @@ class RectROI(ROI): """ def __init__(self, pos, size, centered=False, sideScalers=False, **args): - #QtGui.QGraphicsRectItem.__init__(self, 0, 0, size[0], size[1]) ROI.__init__(self, pos, size, **args) if centered: center = [0.5, 0.5] else: center = [0, 0] - #self.addTranslateHandle(center) self.addScaleHandle([1, 1], center) if sideScalers: self.addScaleHandle([1, 0.5], [center[0], 0.5]) @@ -1646,7 +1526,6 @@ class MultiRectROI(QtGui.QGraphicsObject): rgn = l.getArrayRegion(arr, img, axes=axes, **kwds) if rgn is None: continue - #return None rgns.append(rgn) #print l.state['size'] @@ -1731,7 +1610,6 @@ class EllipseROI(ROI): """ def __init__(self, pos, size, **args): - #QtGui.QGraphicsRectItem.__init__(self, 0, 0, size[0], size[1]) self.path = None ROI.__init__(self, pos, size, **args) self.sigRegionChanged.connect(self._clearPath) @@ -1835,22 +1713,14 @@ class PolygonROI(ROI): if pos is None: pos = [0,0] ROI.__init__(self, pos, [1,1], **args) - #ROI.__init__(self, positions[0]) for p in positions: self.addFreeHandle(p) self.setZValue(1000) print("Warning: PolygonROI is deprecated. Use PolyLineROI instead.") - def listPoints(self): return [p['item'].pos() for p in self.handles] - #def movePoint(self, *args, **kargs): - #ROI.movePoint(self, *args, **kargs) - #self.prepareGeometryChange() - #for h in self.handles: - #h['pos'] = h['item'].pos() - def paint(self, p, *args): p.setRenderHint(QtGui.QPainter.Antialiasing) p.setPen(self.currentPen) @@ -1877,7 +1747,6 @@ class PolygonROI(ROI): sc['pos'] = Point(self.state['pos']) sc['size'] = Point(self.state['size']) sc['angle'] = self.state['angle'] - #sc['handles'] = self.handles return sc @@ -2097,7 +1966,6 @@ class LineSegmentROI(ROI): pos = [0,0] ROI.__init__(self, pos, [1,1], **args) - #ROI.__init__(self, positions[0]) if len(positions) > 2: raise Exception("LineSegmentROI must be defined by exactly 2 positions. For more points, use PolyLineROI.") @@ -2193,7 +2061,6 @@ class CrosshairROI(ROI): def __init__(self, pos=None, size=None, **kargs): if size == None: - #size = [100e-6,100e-6] size=[1,1] if pos == None: pos = [0,0] From 9b9a72e6bfdddf85287741330f9f208e68327285 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 17 Oct 2017 20:56:19 -0700 Subject: [PATCH 099/135] minor image testing edits --- pyqtgraph/tests/image_testing.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/tests/image_testing.py b/pyqtgraph/tests/image_testing.py index c8a41dec..2bd6e8d3 100644 --- a/pyqtgraph/tests/image_testing.py +++ b/pyqtgraph/tests/image_testing.py @@ -10,11 +10,13 @@ Procedure for unit-testing with images: $ PYQTGRAPH_AUDIT=1 python pyqtgraph/graphicsItems/tests/test_PlotCurveItem.py - Any failing tests will - display the test results, standard image, and the differences between the - two. If the test result is bad, then press (f)ail. If the test result is - good, then press (p)ass and the new image will be saved to the test-data - directory. + Any failing tests will display the test results, standard image, and the + differences between the two. If the test result is bad, then press (f)ail. + If the test result is good, then press (p)ass and the new image will be + saved to the test-data directory. + + To check all test results regardless of whether the test failed, set the + environment variable PYQTGRAPH_AUDIT_ALL=1. 3. After adding or changing test images, create a new commit: @@ -162,6 +164,8 @@ def assertImageApproved(image, standardFile, message=None, **kwargs): # If the test image does not match, then we go to audit if requested. try: + if stdImage is None: + raise Exception("No reference image saved for this test.") if image.shape[2] != stdImage.shape[2]: raise Exception("Test result has different channel count than standard image" "(%d vs %d)" % (image.shape[2], stdImage.shape[2])) From c6839b4708f331fdc1a16f859f658846f38b3827 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 17 Oct 2017 21:22:55 -0700 Subject: [PATCH 100/135] fix: polylineroi segment draws to wrong handle after click --- pyqtgraph/graphicsItems/ROI.py | 10 ++++++---- pyqtgraph/graphicsItems/tests/test_ROI.py | 8 ++++++++ pyqtgraph/tests/image_testing.py | 2 +- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 319524e9..9682b6b3 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -571,7 +571,6 @@ class ROI(GraphicsObject): ## Note: by default, handles are not user-removable even if this method returns True. return True - def getLocalHandlePositions(self, index=None): """Returns the position of handles in the ROI's coordinate system. @@ -1969,9 +1968,13 @@ class LineSegmentROI(ROI): if len(positions) > 2: raise Exception("LineSegmentROI must be defined by exactly 2 positions. For more points, use PolyLineROI.") - self.endpoints = [] for i, p in enumerate(positions): - self.endpoints.append(self.addFreeHandle(p, item=handles[i])) + self.addFreeHandle(p, item=handles[i]) + + @property + def endpoints(self): + # must not be cached because self.handles may change. + return [h['item'] for h in self.handles] def listPoints(self): return [p['item'].pos() for p in self.handles] @@ -2018,7 +2021,6 @@ class LineSegmentROI(ROI): See ROI.getArrayRegion() for a description of the arguments. """ - imgPts = [self.mapToItem(img, h.pos()) for h in self.endpoints] rgns = [] coords = [] diff --git a/pyqtgraph/graphicsItems/tests/test_ROI.py b/pyqtgraph/graphicsItems/tests/test_ROI.py index ddc7f173..8cc2efd5 100644 --- a/pyqtgraph/graphicsItems/tests/test_ROI.py +++ b/pyqtgraph/graphicsItems/tests/test_ROI.py @@ -208,15 +208,23 @@ def test_PolyLineROI(): # click segment mouseClick(plt, pt, QtCore.Qt.LeftButton) assertImageApproved(plt, 'roi/polylineroi/'+name+'_click_segment', 'Click mouse over segment.') + + # drag new handle + mouseMove(plt, pt+pg.Point(10, -10)) # pg bug: have to move the mouse off/on again to register hover + mouseDrag(plt, pt, pt + pg.Point(10, -10), QtCore.Qt.LeftButton) + assertImageApproved(plt, 'roi/polylineroi/'+name+'_drag_new_handle', 'Drag mouse over created handle.') + # clear all points r.clearPoints() assertImageApproved(plt, 'roi/polylineroi/'+name+'_clear', 'All points cleared.') assert len(r.getState()['points']) == 0 + # call setPoints r.setPoints(initState['points']) assertImageApproved(plt, 'roi/polylineroi/'+name+'_setpoints', 'Reset points to initial state.') assert len(r.getState()['points']) == 3 + # call setState r.setState(initState) assertImageApproved(plt, 'roi/polylineroi/'+name+'_setstate', 'Reset ROI to initial state.') assert len(r.getState()['points']) == 3 diff --git a/pyqtgraph/tests/image_testing.py b/pyqtgraph/tests/image_testing.py index 2bd6e8d3..a7552631 100644 --- a/pyqtgraph/tests/image_testing.py +++ b/pyqtgraph/tests/image_testing.py @@ -44,7 +44,7 @@ Procedure for unit-testing with images: # pyqtgraph should be tested against. When adding or changing test images, # create and push a new tag and update this variable. To test locally, begin # by creating the tag in your ~/.pyqtgraph/test-data repository. -testDataTag = 'test-data-6' +testDataTag = 'test-data-7' import time From 7be6f1e70cbbbd0839d71dca9557e38afd86f043 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 18 Oct 2017 00:18:07 -0700 Subject: [PATCH 101/135] fix: error when using SpinBox(delay) argument --- pyqtgraph/widgets/SpinBox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/widgets/SpinBox.py b/pyqtgraph/widgets/SpinBox.py index b8066cd7..499a3554 100644 --- a/pyqtgraph/widgets/SpinBox.py +++ b/pyqtgraph/widgets/SpinBox.py @@ -106,11 +106,11 @@ class SpinBox(QtGui.QAbstractSpinBox): self.skipValidate = False self.setCorrectionMode(self.CorrectToPreviousValue) self.setKeyboardTracking(False) + self.proxy = SignalProxy(self.sigValueChanging, slot=self.delayedChange, delay=self.opts['delay']) self.setOpts(**kwargs) self._updateHeight() self.editingFinished.connect(self.editingFinishedEvent) - self.proxy = SignalProxy(self.sigValueChanging, slot=self.delayedChange, delay=self.opts['delay']) def event(self, ev): ret = QtGui.QAbstractSpinBox.event(self, ev) From 60e6591608ee1e9ab26fa86408f7e86d1ef62ba6 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 18 Oct 2017 00:18:46 -0700 Subject: [PATCH 102/135] Fix verlet integration demo --- examples/verlet_chain/chain.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/verlet_chain/chain.py b/examples/verlet_chain/chain.py index 6eb3501a..1c4f2403 100644 --- a/examples/verlet_chain/chain.py +++ b/examples/verlet_chain/chain.py @@ -32,8 +32,6 @@ class ChainSim(pg.QtCore.QObject): if self.initialized: return - assert None not in [self.pos, self.mass, self.links, self.lengths] - if self.fixed is None: self.fixed = np.zeros(self.pos.shape[0], dtype=bool) if self.push is None: From 001070d9ff8e2e652e0ad29ca62e35f06e4a805f Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 18 Oct 2017 00:26:37 -0700 Subject: [PATCH 103/135] Add new fractal demo --- examples/fractal.py | 122 ++++++++++++++++++++++++++++++++++++++++++++ examples/utils.py | 1 + 2 files changed, 123 insertions(+) create mode 100644 examples/fractal.py diff --git a/examples/fractal.py b/examples/fractal.py new file mode 100644 index 00000000..eeb1bdb0 --- /dev/null +++ b/examples/fractal.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- +""" +Displays an interactive Koch fractal +""" +import initExample ## Add path to library (just for examples; you do not need this) + +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui +import numpy as np + +app = QtGui.QApplication([]) + +# Set up UI widgets +win = pg.QtGui.QWidget() +win.setWindowTitle('pyqtgraph example: fractal demo') +layout = pg.QtGui.QGridLayout() +win.setLayout(layout) +layout.setContentsMargins(0, 0, 0, 0) +depthLabel = pg.QtGui.QLabel('fractal depth:') +layout.addWidget(depthLabel, 0, 0) +depthSpin = pg.SpinBox(value=5, step=1, bounds=[1, 10], delay=0, int=True) +depthSpin.resize(100, 20) +layout.addWidget(depthSpin, 0, 1) +w = pg.GraphicsLayoutWidget() +layout.addWidget(w, 1, 0, 1, 2) +win.show() + +# Set up graphics +v = w.addViewBox() +v.setAspectLocked() +baseLine = pg.PolyLineROI([[0, 0], [1, 0], [1.5, 1], [2, 0], [3, 0]], pen=(0, 255, 0, 100), movable=False) +v.addItem(baseLine) +fc = pg.PlotCurveItem(pen=(255, 255, 255, 200), antialias=True) +v.addItem(fc) +v.autoRange() + + +transformMap = [0, 0, None] + + +def update(): + # recalculate and redraw the fractal curve + + depth = depthSpin.value() + pts = baseLine.getState()['points'] + nbseg = len(pts) - 1 + nseg = nbseg**depth + + # Get a transformation matrix for each base segment + trs = [] + v1 = pts[-1] - pts[0] + l1 = v1.length() + for i in range(len(pts)-1): + p1 = pts[i] + p2 = pts[i+1] + v2 = p2 - p1 + t = p1 - pts[0] + r = v2.angle(v1) + s = v2.length() / l1 + trs.append(pg.SRTTransform({'pos': t, 'scale': (s, s), 'angle': r})) + + basePts = [np.array(list(pt) + [1]) for pt in baseLine.getState()['points']] + baseMats = np.dstack([tr.matrix().T for tr in trs]).transpose(2, 0, 1) + + # Generate an array of matrices to transform base points + global transformMap + if transformMap[:2] != [depth, nbseg]: + # we can cache the transform index to save a little time.. + nseg = nbseg**depth + matInds = np.empty((depth, nseg), dtype=int) + for i in range(depth): + matInds[i] = np.tile(np.repeat(np.arange(nbseg), nbseg**(depth-1-i)), nbseg**i) + transformMap = [depth, nbseg, matInds] + + # Each column in matInds contains the indices referring to the base transform + # matrices that must be multiplied together to generate the final transform + # for each segment of the fractal + matInds = transformMap[2] + + # Collect all matrices needed for generating fractal curve + mats = baseMats[matInds] + + # Magic-multiply stacks of matrices together + def matmul(a, b): + return np.sum(np.transpose(a,(0,2,1))[..., None] * b[..., None, :], axis=-3) + mats = reduce(matmul, mats) + + # Transform base points through matrix array + pts = np.empty((nseg * nbseg + 1, 2)) + for l in range(len(trs)): + bp = basePts[l] + pts[l:-1:len(trs)] = np.dot(mats, bp)[:, :2] + + # Finish the curve with the last base point + pts[-1] = basePts[-1][:2] + + # update fractal curve with new points + fc.setData(pts[:,0], pts[:,1]) + + +# Update the fractal whenever the base shape or depth has changed +baseLine.sigRegionChanged.connect(update) +depthSpin.valueChanged.connect(update) + +# Initialize +update() + + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() + + + + + + + + + \ No newline at end of file diff --git a/examples/utils.py b/examples/utils.py index 88adc9c9..b004a0d3 100644 --- a/examples/utils.py +++ b/examples/utils.py @@ -32,6 +32,7 @@ examples = OrderedDict([ ('Optics', 'optics_demos.py'), ('Special relativity', 'relativity_demo.py'), ('Verlet chain', 'verlet_chain_demo.py'), + ('Koch Fractal', 'fractal.py'), ])), ('GraphicsItems', OrderedDict([ ('Scatter Plot', 'ScatterPlot.py'), From a15057835a78aedcb33e544cf2a94268bd048cf5 Mon Sep 17 00:00:00 2001 From: Petras Jokubauskas Date: Fri, 20 Oct 2017 15:35:53 +0200 Subject: [PATCH 104/135] fix: set foreground color for items which background color is statically set, so that values of those would be readable when OS uses dark theme. --- pyqtgraph/console/Console.py | 5 +++-- pyqtgraph/parametertree/parameterTypes.py | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/console/Console.py b/pyqtgraph/console/Console.py index 72164f33..c5c2e7b1 100644 --- a/pyqtgraph/console/Console.py +++ b/pyqtgraph/console/Console.py @@ -115,7 +115,7 @@ class ConsoleWidget(QtGui.QWidget): self.write("
%s\n"%encCmd, html=True) self.execMulti(cmd) else: - self.write("
%s\n"%encCmd, html=True) + self.write("
%s\n"%encCmd, html=True) self.inCmd = True self.execSingle(cmd) @@ -209,7 +209,7 @@ class ConsoleWidget(QtGui.QWidget): else: if self.inCmd: self.inCmd = False - self.output.textCursor().insertHtml("

") + self.output.textCursor().insertHtml("

") #self.stdout.write("

") self.output.insertPlainText(strn) #self.stdout.write(strn) @@ -366,6 +366,7 @@ class ConsoleWidget(QtGui.QWidget): self.ui.exceptionStackList.addItem('-- exception caught here: --') item = self.ui.exceptionStackList.item(self.ui.exceptionStackList.count()-1) item.setBackground(QtGui.QBrush(QtGui.QColor(200, 200, 200))) + item.setForeground(QtGui.QBrush(QtGui.QColor(50, 50, 50))) self.frames.append(None) # And finish the rest of the stack up to the exception diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py index d137410d..ec14faa1 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -400,6 +400,7 @@ class GroupParameterItem(ParameterItem): else: for c in [0,1]: self.setBackground(c, QtGui.QBrush(QtGui.QColor(220,220,220))) + self.setForeground(c, QtGui.QBrush(QtGui.QColor(100,100,100))) font = self.font(c) font.setBold(True) #font.setPointSize(font.pointSize()+1) From 5d0974673884e24b920847d7a4dd35e57d0e3337 Mon Sep 17 00:00:00 2001 From: miranis <33010847+miranis@users.noreply.github.com> Date: Mon, 23 Oct 2017 01:45:52 +0200 Subject: [PATCH 105/135] Update ImageItem.py Functions nanmin and nanmax are defined in the numpy module and cannot be accessed from the global namespace! --- pyqtgraph/graphicsItems/ImageItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index 9588c586..04a5c757 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -329,7 +329,7 @@ class ImageItem(GraphicsObject): sl = [slice(None)] * data.ndim sl[ax] = slice(None, None, 2) data = data[sl] - return nanmin(data), nanmax(data) + return np.nanmin(data), np.nanmax(data) def updateImage(self, *args, **kargs): ## used for re-rendering qimage from self.image. From e16948dbeb702fd46ccca1f20c0a869ca05fb814 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 1 Nov 2017 16:57:45 -0700 Subject: [PATCH 106/135] Add datetime support to configfile --- pyqtgraph/configfile.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/configfile.py b/pyqtgraph/configfile.py index 7b20db1d..e7056599 100644 --- a/pyqtgraph/configfile.py +++ b/pyqtgraph/configfile.py @@ -9,7 +9,7 @@ file format. Data structures may be nested and contain any data type as long as it can be converted to/from a string using repr and eval. """ -import re, os, sys +import re, os, sys, datetime import numpy from .pgcollections import OrderedDict from . import units @@ -143,6 +143,7 @@ def parseString(lines, start=0): local['Point'] = Point local['QtCore'] = QtCore local['ColorMap'] = ColorMap + local['datetime'] = datetime # Needed for reconstructing numpy arrays local['array'] = numpy.array for dtype in ['int8', 'uint8', From 4752b777921f54b3a23dfc2952697ddf11922112 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 2 Nov 2017 10:25:57 -0700 Subject: [PATCH 107/135] Fix parallelizer's progressdialog usage --- pyqtgraph/multiprocess/parallelizer.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/multiprocess/parallelizer.py b/pyqtgraph/multiprocess/parallelizer.py index 934bc6d0..86298023 100644 --- a/pyqtgraph/multiprocess/parallelizer.py +++ b/pyqtgraph/multiprocess/parallelizer.py @@ -101,7 +101,10 @@ class Parallelize(object): else: ## parent if self.showProgress: - self.progressDlg.__exit__(None, None, None) + try: + self.progressDlg.__exit__(None, None, None) + except Exception: + pass def runSerial(self): if self.showProgress: From 2db502a9cce1f6ff6605805c12c5d6bb90da91cd Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 16 Nov 2017 09:25:27 -0800 Subject: [PATCH 108/135] Fix bug when switching level mode --- pyqtgraph/graphicsItems/HistogramLUTItem.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index e6d692e6..90e2e790 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -293,7 +293,6 @@ class HistogramLUTItem(GraphicsWidget): self.levelMode = mode self._showRegions() - self.imageChanged() # do our best to preserve old levels if mode == 'mono': @@ -302,7 +301,12 @@ class HistogramLUTItem(GraphicsWidget): else: levels = [oldLevels] * 4 self.setLevels(rgba=levels) + + # force this because calling self.setLevels might not set the imageItem + # levels if there was no change to the region item + self.imageItem().setLevels(self.getLevels()) + self.imageChanged() self.update() def _showRegions(self): From b9c7e379f37d54902f1a6c130fa959c149c5900c Mon Sep 17 00:00:00 2001 From: James Date: Mon, 18 Dec 2017 15:57:31 +0000 Subject: [PATCH 109/135] Prevent ReferenceErrors on cleanup Prevents a ReferenceError being thrown when PyQtGraph tries to put TensorFlow stuff onto a QGraphicsScene() on cleanup - see https://github.com/pyqtgraph/pyqtgraph/issues/603 and https://stackoverflow.com/questions/41542571/pyqtgraph-tries-to-put-tensorflow-stuff-onto-a-qgraphicsscene-on-cleanup --- pyqtgraph/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index 24653207..412c8627 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -320,7 +320,7 @@ def cleanup(): 'are properly called before app shutdown (%s)\n' % (o,)) s.addItem(o) - except RuntimeError: ## occurs if a python wrapper no longer has its underlying C++ object + except (RuntimeError, ReferenceError): ## occurs if a python wrapper no longer has its underlying C++ object continue _cleanupCalled = True From 035b5a6c316d561005023af21cc0a82e589764b8 Mon Sep 17 00:00:00 2001 From: Girish Ramlugun Date: Tue, 9 Jan 2018 14:30:59 +1300 Subject: [PATCH 110/135] Play image along 't' axis instead of first axis --- pyqtgraph/imageview/ImageView.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index 5cc00f68..95ccf12c 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -469,7 +469,7 @@ class ImageView(QtGui.QWidget): n = int(self.playRate * dt) if n != 0: self.lastPlayTime += (float(n)/self.playRate) - if self.currentIndex+n > self.image.shape[0]: + if self.currentIndex+n > self.image.shape[self.axes['t']]: self.play(0) self.jumpFrames(n) From 4867149d83e0b77feb8e77df7cdcceb11357ff23 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 24 Jan 2018 09:03:44 -0800 Subject: [PATCH 111/135] Be a little more tolerant of missing Qt packages, and defer import errors until we try to use the missing package. --- pyqtgraph/Qt.py | 62 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 42 insertions(+), 20 deletions(-) diff --git a/pyqtgraph/Qt.py b/pyqtgraph/Qt.py index ad04cd76..749943f2 100644 --- a/pyqtgraph/Qt.py +++ b/pyqtgraph/Qt.py @@ -43,8 +43,29 @@ if QT_LIB is None: if QT_LIB is None: raise Exception("PyQtGraph requires one of PyQt4, PyQt5 or PySide; none of these packages could be imported.") + +class FailedImport(object): + """Used to defer ImportErrors until we are sure the module is needed. + """ + def __init__(self, err): + self.err = err + + def __getattr__(self, attr): + raise self.err + + if QT_LIB == PYSIDE: - from PySide import QtGui, QtCore, QtOpenGL, QtSvg + from PySide import QtGui, QtCore + + try: + from PySide import QtOpenGL + except ImportError as err: + QtOpenGL = FailedImport(err) + try: + from PySide import QtSvg + except ImportError as err: + QtSvg = FailedImport(err) + try: from PySide import QtTest if not hasattr(QtTest.QTest, 'qWait'): @@ -55,9 +76,9 @@ if QT_LIB == PYSIDE: while time.time() < start + msec * 0.001: QtGui.QApplication.processEvents() QtTest.QTest.qWait = qWait - - except ImportError: - pass + except ImportError as err: + QtTest = FailedImport(err) + import PySide try: from PySide import shiboken @@ -133,16 +154,16 @@ elif QT_LIB == PYQT4: from PyQt4 import QtGui, QtCore, uic try: from PyQt4 import QtSvg - except ImportError: - pass + except ImportError as err: + QtSvg = FailedImport(err) try: from PyQt4 import QtOpenGL - except ImportError: - pass + except ImportError as err: + QtOpenGL = FailedImport(err) try: from PyQt4 import QtTest - except ImportError: - pass + except ImportError as err: + QtTest = FailedImport(err) VERSION_INFO = 'PyQt4 ' + QtCore.PYQT_VERSION_STR + ' Qt ' + QtCore.QT_VERSION_STR @@ -157,24 +178,25 @@ elif QT_LIB == PYQT5: # users), we install a global exception hook to override this behavior. ver = QtCore.PYQT_VERSION_STR.split('.') if int(ver[1]) >= 5: - sys_excepthook = sys.excepthook - def pyqt5_qabort_override(*args, **kwds): - return sys_excepthook(*args, **kwds) - sys.excepthook = pyqt5_qabort_override + if sys.excepthook == sys.__excepthook__: + sys_excepthook = sys.excepthook + def pyqt5_qabort_override(*args, **kwds): + return sys_excepthook(*args, **kwds) + sys.excepthook = pyqt5_qabort_override try: from PyQt5 import QtSvg - except ImportError: - pass + except ImportError as err: + QtSvg = FailedImport(err) try: from PyQt5 import QtOpenGL - except ImportError: - pass + except ImportError as err: + QtOpenGL = FailedImport(err) try: from PyQt5 import QtTest QtTest.QTest.qWaitForWindowShown = QtTest.QTest.qWaitForWindowExposed - except ImportError: - pass + except ImportError as err: + QtTest = FailedImport(err) # Re-implement deprecated APIs From 52754d48594f713092a7e6b55a6fdcb046f76be6 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 24 Jan 2018 09:06:05 -0800 Subject: [PATCH 112/135] fix __getattr__ handling in PlotWindow --- pyqtgraph/widgets/PlotWidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/widgets/PlotWidget.py b/pyqtgraph/widgets/PlotWidget.py index 964307ae..6e10b13a 100644 --- a/pyqtgraph/widgets/PlotWidget.py +++ b/pyqtgraph/widgets/PlotWidget.py @@ -76,7 +76,7 @@ class PlotWidget(GraphicsView): m = getattr(self.plotItem, attr) if hasattr(m, '__call__'): return m - raise NameError(attr) + raise AttributeError(attr) def viewRangeChanged(self, view, range): #self.emit(QtCore.SIGNAL('viewChanged'), *args) From 09aa19873147d612388131fb15253f9c47bae16e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 24 Jan 2018 09:06:37 -0800 Subject: [PATCH 113/135] Add top level stack() function for debugging --- pyqtgraph/__init__.py | 19 +++++++++++++++++++ pyqtgraph/console/Console.py | 8 +++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index 412c8627..520ea196 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -447,6 +447,25 @@ def dbg(*args, **kwds): except NameError: consoles = [c] return c + + +def stack(*args, **kwds): + """ + Create a console window and show the current stack trace. + + All arguments are passed to :func:`ConsoleWidget.__init__() `. + """ + mkQApp() + from . import console + c = console.ConsoleWidget(*args, **kwds) + c.setStack() + c.show() + global consoles + try: + consoles.append(c) + except NameError: + consoles = [c] + return c def mkQApp(): diff --git a/pyqtgraph/console/Console.py b/pyqtgraph/console/Console.py index 72164f33..14890e0b 100644 --- a/pyqtgraph/console/Console.py +++ b/pyqtgraph/console/Console.py @@ -147,10 +147,11 @@ class ConsoleWidget(QtGui.QWidget): def currentFrame(self): ## Return the currently selected exception stack frame (or None if there is no exception) - if self.currentTraceback is None: - return None index = self.ui.exceptionStackList.currentRow() - return self.frames[index] + if index >= 0 and index < len(self.frames): + return self.frames[index] + else: + return None def execSingle(self, cmd): try: @@ -276,6 +277,7 @@ class ConsoleWidget(QtGui.QWidget): def clearExceptionClicked(self): self.currentTraceback = None + self.frames = [] self.ui.exceptionInfoLabel.setText("[No current exception]") self.ui.exceptionStackList.clear() self.ui.clearExceptionBtn.setEnabled(False) From 0653c8ec591d0c6c8fbf133ab0baeb535684b03a Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 24 Jan 2018 09:11:42 -0800 Subject: [PATCH 114/135] Add example and test demonstrating spinbox bug --- examples/SpinBox.py | 2 +- pyqtgraph/widgets/tests/test_spinbox.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/examples/SpinBox.py b/examples/SpinBox.py index 2faf10ee..ef1d0fc5 100644 --- a/examples/SpinBox.py +++ b/examples/SpinBox.py @@ -26,7 +26,7 @@ spins = [ ("Float with SI-prefixed units
(n, u, m, k, M, etc)", pg.SpinBox(value=0.9, suffix='V', siPrefix=True)), ("Float with SI-prefixed units,
dec step=0.1, minStep=0.1", - pg.SpinBox(value=1.0, suffix='V', siPrefix=True, dec=True, step=0.1, minStep=0.1)), + pg.SpinBox(value=1.0, suffix='PSI', siPrefix=True, dec=True, step=0.1, minStep=0.1)), ("Float with SI-prefixed units,
dec step=0.5, minStep=0.01", pg.SpinBox(value=1.0, suffix='V', siPrefix=True, dec=True, step=0.5, minStep=0.01)), ("Float with SI-prefixed units,
dec step=1.0, minStep=0.001", diff --git a/pyqtgraph/widgets/tests/test_spinbox.py b/pyqtgraph/widgets/tests/test_spinbox.py index 10087881..cff97da7 100644 --- a/pyqtgraph/widgets/tests/test_spinbox.py +++ b/pyqtgraph/widgets/tests/test_spinbox.py @@ -18,6 +18,8 @@ def test_spinbox_formatting(): (12345678955, '12345678955', dict(int=True, decimals=100)), (1.45e-9, '1.45e-09 A', dict(int=False, decimals=6, suffix='A', siPrefix=False)), (1.45e-9, '1.45 nA', dict(int=False, decimals=6, suffix='A', siPrefix=True)), + (1.45, '1.45 PSI', dict(int=False, decimals=6, suffix='PSI', siPrefix=True)), + (1.45e-3, '1.45 mPSI', dict(int=False, decimals=6, suffix='PSI', siPrefix=True)), (-2500.3427, '$-2500.34', dict(int=False, format='${value:0.02f}')), ] @@ -26,3 +28,14 @@ def test_spinbox_formatting(): sb.setValue(value) assert sb.value() == value assert pg.asUnicode(sb.text()) == text + + # test setting value + if not opts.get('int', False): + suf = sb.opts['suffix'] + sb.lineEdit().setText('0.1' + suf) + sb.editingFinishedEvent() + assert sb.value() == 0.1 + if suf != '': + sb.lineEdit().setText('0.1 m' + suf) + sb.editingFinishedEvent() + assert sb.value() == 0.1e-3 From a812d802da660bdb09885e6921a67fa11deffe4a Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 24 Jan 2018 09:12:10 -0800 Subject: [PATCH 115/135] Fix bug when spinbox units begin with an SI prefix (like 'PSI') --- pyqtgraph/functions.py | 25 +++++++++++++++++-------- pyqtgraph/widgets/SpinBox.py | 2 +- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 7ad603f7..0495a00c 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -110,7 +110,7 @@ def siFormat(x, precision=3, suffix='', space=True, error=None, minVal=1e-25, al return fmt % (x*p, pref, suffix, plusminus, siFormat(error, precision=precision, suffix=suffix, space=space, minVal=minVal)) -def siParse(s, regex=FLOAT_REGEX): +def siParse(s, regex=FLOAT_REGEX, suffix=None): """Convert a value written in SI notation to a tuple (number, si_prefix, suffix). Example:: @@ -118,6 +118,12 @@ def siParse(s, regex=FLOAT_REGEX): siParse('100 μV") # returns ('100', 'μ', 'V') """ s = asUnicode(s) + s = s.strip() + if suffix is not None and len(suffix) > 0: + if s[-len(suffix):] != suffix: + raise ValueError("String '%s' does not have the expected suffix '%s'" % (s, suffix)) + s = s[:-len(suffix)] + 'X' # add a fake suffix so the regex still picks up the si prefix + m = regex.match(s) if m is None: raise ValueError('Cannot parse number "%s"' % s) @@ -126,15 +132,18 @@ def siParse(s, regex=FLOAT_REGEX): except IndexError: sip = '' - try: - suf = m.group('suffix') - except IndexError: - suf = '' + if suffix is None: + try: + suf = m.group('suffix') + except IndexError: + suf = '' + else: + suf = suffix return m.group('number'), '' if sip is None else sip, '' if suf is None else suf -def siEval(s, typ=float, regex=FLOAT_REGEX): +def siEval(s, typ=float, regex=FLOAT_REGEX, suffix=None): """ Convert a value written in SI notation to its equivalent prefixless value. @@ -142,9 +151,9 @@ def siEval(s, typ=float, regex=FLOAT_REGEX): siEval("100 μV") # returns 0.0001 """ - val, siprefix, suffix = siParse(s, regex) + val, siprefix, suffix = siParse(s, regex, suffix=suffix) v = typ(val) - return siApply(val, siprefix) + return siApply(v, siprefix) def siApply(val, siprefix): diff --git a/pyqtgraph/widgets/SpinBox.py b/pyqtgraph/widgets/SpinBox.py index b8066cd7..17caea32 100644 --- a/pyqtgraph/widgets/SpinBox.py +++ b/pyqtgraph/widgets/SpinBox.py @@ -518,7 +518,7 @@ class SpinBox(QtGui.QAbstractSpinBox): # tokenize into numerical value, si prefix, and suffix try: - val, siprefix, suffix = fn.siParse(strn, self.opts['regex']) + val, siprefix, suffix = fn.siParse(strn, self.opts['regex'], suffix=self.opts['suffix']) except Exception: return False From 019c421ca102233ba2df27653c723b90a593e09e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 24 Jan 2018 17:48:03 -0800 Subject: [PATCH 116/135] Don't attempt to set same level mode again --- pyqtgraph/graphicsItems/HistogramLUTItem.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index 90e2e790..f85b64dd 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -289,8 +289,10 @@ class HistogramLUTItem(GraphicsWidget): """ assert mode in ('mono', 'rgba') - oldLevels = self.getLevels() + if mode == self.levelMode: + return + oldLevels = self.getLevels() self.levelMode = mode self._showRegions() From 7d467cb65242d5ceda510290576a1867b9c2c36a Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 26 Jan 2018 08:58:18 -0800 Subject: [PATCH 117/135] Add changelog entry describing new behavior --- CHANGELOG | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 388f51b9..33106c8f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,13 @@ +pyqtgraph-0.11.0 (in development) + + API / behavior changes: + - ArrowItem's `angle` option now rotates the arrow without affecting its coordinate system. + The result is visually the same, but children of ArrowItem are no longer rotated + (this allows screen-aligned text to be attached more easily). + To mimic the old behavior, use ArrowItem.rotate() instead of the `angle` argument. + + + pyqtgraph-0.10.0 New Features: From b6838eb8c4b60f914d0dcd3e23cba273e1360303 Mon Sep 17 00:00:00 2001 From: Terekhov Date: Sun, 28 Jan 2018 18:10:39 -0500 Subject: [PATCH 118/135] Use symbol id for a key in SymbolAtlas Add test on a custom symbol for ScatterPlotItem. In PyQt5 QPainterPath is not hashable anymore which causes SymbolAtlas to fail accept it as a custom symbol, use id instead. --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 5 ++-- .../tests/test_ScatterPlotItem.py | 26 ++++++++++++------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 597491f3..443cc220 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -126,7 +126,7 @@ class SymbolAtlas(object): keyi = None sourceRecti = None for i, rec in enumerate(opts): - key = (rec[3], rec[2], id(rec[4]), id(rec[5])) # TODO: use string indexes? + key = (id(rec[3]), rec[2], id(rec[4]), id(rec[5])) # TODO: use string indexes? if key == keyi: sourceRect[i] = sourceRecti else: @@ -136,6 +136,7 @@ class SymbolAtlas(object): newRectSrc = QtCore.QRectF() newRectSrc.pen = rec['pen'] newRectSrc.brush = rec['brush'] + newRectSrc.symbol = rec[3] self.symbolMap[key] = newRectSrc self.atlasValid = False sourceRect[i] = newRectSrc @@ -151,7 +152,7 @@ class SymbolAtlas(object): images = [] for key, sourceRect in self.symbolMap.items(): if sourceRect.width() == 0: - img = renderSymbol(key[0], key[1], sourceRect.pen, sourceRect.brush) + img = renderSymbol(sourceRect.symbol, key[1], sourceRect.pen, sourceRect.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: diff --git a/pyqtgraph/graphicsItems/tests/test_ScatterPlotItem.py b/pyqtgraph/graphicsItems/tests/test_ScatterPlotItem.py index acf6ad72..ba1fb9d7 100644 --- a/pyqtgraph/graphicsItems/tests/test_ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/tests/test_ScatterPlotItem.py @@ -1,3 +1,4 @@ +from pyqtgraph.Qt import QtGui, QtCore import pyqtgraph as pg import numpy as np app = pg.mkQApp() @@ -7,9 +8,16 @@ app.processEvents() def test_scatterplotitem(): plot = pg.PlotWidget() - # set view range equal to its bounding rect. + # set view range equal to its bounding rect. # This causes plots to look the same regardless of pxMode. plot.setRange(rect=plot.boundingRect()) + + # test SymbolAtlas accepts custom symbol + s = pg.ScatterPlotItem() + symbol = QtGui.QPainterPath() + symbol.addEllipse(QtCore.QRectF(-0.5, -0.5, 1, 1)) + s.addPoints([{'pos': [0,0], 'data': 1, 'symbol': symbol}]) + for i, pxMode in enumerate([True, False]): for j, useCache in enumerate([True, False]): s = pg.ScatterPlotItem() @@ -17,14 +25,14 @@ def test_scatterplotitem(): plot.addItem(s) s.setData(x=np.array([10,40,20,30])+i*100, y=np.array([40,60,10,30])+j*100, pxMode=pxMode) s.addPoints(x=np.array([60, 70])+i*100, y=np.array([60, 70])+j*100, size=[20, 30]) - + # Test uniform spot updates s.setSize(10) s.setBrush('r') s.setPen('g') s.setSymbol('+') app.processEvents() - + # Test list spot updates s.setSize([10] * 6) s.setBrush([pg.mkBrush('r')] * 6) @@ -55,7 +63,7 @@ def test_scatterplotitem(): def test_init_spots(): plot = pg.PlotWidget() - # set view range equal to its bounding rect. + # set view range equal to its bounding rect. # This causes plots to look the same regardless of pxMode. plot.setRange(rect=plot.boundingRect()) spots = [ @@ -63,28 +71,28 @@ def test_init_spots(): {'pos': (1, 2), 'pen': None, 'brush': None, 'data': 'zzz'}, ] s = pg.ScatterPlotItem(spots=spots) - + # Check we can display without errors plot.addItem(s) app.processEvents() plot.clear() - + # check data is correct spots = s.points() - + defPen = pg.mkPen(pg.getConfigOption('foreground')) assert spots[0].pos().x() == 0 assert spots[0].pos().y() == 1 assert spots[0].pen() == defPen assert spots[0].data() is None - + assert spots[1].pos().x() == 1 assert spots[1].pos().y() == 2 assert spots[1].pen() == pg.mkPen(None) assert spots[1].brush() == pg.mkBrush(None) assert spots[1].data() == 'zzz' - + if __name__ == '__main__': test_scatterplotitem() From 2c30f8a7db7bfb9053b903fb5e582afa21e29eb6 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 29 Jan 2018 08:48:24 -0800 Subject: [PATCH 119/135] end travis testing on python 2.6 --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index c4a67ac3..acfde8ec 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,10 +17,10 @@ env: # Enable python 2 and python 3 builds # Note that the 2.6 build doesn't get flake8, and runs old versions of # Pyglet and GLFW to make sure we deal with those correctly - - PYTHON=2.6 QT=pyqt4 TEST=standard + #- PYTHON=2.6 QT=pyqt4 TEST=standard # 2.6 support ended - PYTHON=2.7 QT=pyqt4 TEST=extra - PYTHON=2.7 QT=pyside TEST=standard - - PYTHON=3.4 QT=pyqt5 TEST=standard + - PYTHON=3.5 QT=pyqt5 TEST=standard # - PYTHON=3.4 QT=pyside TEST=standard # pyside isn't available for 3.4 with conda #- PYTHON=3.2 QT=pyqt5 TEST=standard From 551ccd105cacbbdbc2420e472e4e861b91658210 Mon Sep 17 00:00:00 2001 From: Terekhov Date: Sun, 28 Jan 2018 18:14:48 -0500 Subject: [PATCH 120/135] Add an example of using text strings as a custom smbol in ScatterPlotItem --- examples/ScatterPlot.py | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/examples/ScatterPlot.py b/examples/ScatterPlot.py index 72022acc..93f184f2 100644 --- a/examples/ScatterPlot.py +++ b/examples/ScatterPlot.py @@ -11,6 +11,7 @@ import initExample from pyqtgraph.Qt import QtGui, QtCore import pyqtgraph as pg import numpy as np +from collections import namedtuple app = QtGui.QApplication([]) mw = QtGui.QMainWindow() @@ -32,8 +33,8 @@ print("Generating data, this takes a few seconds...") ## There are a few different ways we can draw scatter plots; each is optimized for different types of data: -## 1) All spots identical and transform-invariant (top-left plot). -## In this case we can get a huge performance boost by pre-rendering the spot +## 1) All spots identical and transform-invariant (top-left plot). +## In this case we can get a huge performance boost by pre-rendering the spot ## image and just drawing that image repeatedly. n = 300 @@ -57,21 +58,41 @@ s1.sigClicked.connect(clicked) -## 2) Spots are transform-invariant, but not identical (top-right plot). -## In this case, drawing is almsot as fast as 1), but there is more startup -## overhead and memory usage since each spot generates its own pre-rendered +## 2) Spots are transform-invariant, but not identical (top-right plot). +## In this case, drawing is almsot as fast as 1), but there is more startup +## overhead and memory usage since each spot generates its own pre-rendered ## image. +TextSymbol = namedtuple("TextSymbol", "label symbol scale") + +def createLabel(label, angle): + symbol = QtGui.QPainterPath() + #symbol.addText(0, 0, QFont("San Serif", 10), label) + f = QtGui.QFont() + f.setPointSize(10) + symbol.addText(0, 0, f, label) + br = symbol.boundingRect() + scale = min(1. / br.width(), 1. / br.height()) + tr = QtGui.QTransform() + tr.scale(scale, scale) + tr.rotate(angle) + tr.translate(-br.x() - br.width()/2., -br.y() - br.height()/2.) + return TextSymbol(label, tr.map(symbol), 0.1 / scale) + +random_str = lambda : (''.join([chr(np.random.randint(ord('A'),ord('z'))) for i in range(np.random.randint(1,5))]), np.random.randint(0, 360)) + s2 = pg.ScatterPlotItem(size=10, pen=pg.mkPen('w'), pxMode=True) pos = np.random.normal(size=(2,n), scale=1e-5) spots = [{'pos': pos[:,i], 'data': 1, 'brush':pg.intColor(i, n), 'symbol': i%5, 'size': 5+i/10.} for i in range(n)] s2.addPoints(spots) +spots = [{'pos': pos[:,i], 'data': 1, 'brush':pg.intColor(i, n), 'symbol': label[1], 'size': label[2]*(5+i/10.)} for (i, label) in [(i, createLabel(*random_str())) for i in range(n)]] +s2.addPoints(spots) w2.addItem(s2) s2.sigClicked.connect(clicked) -## 3) Spots are not transform-invariant, not identical (bottom-left). -## This is the slowest case, since all spots must be completely re-drawn +## 3) Spots are not transform-invariant, not identical (bottom-left). +## This is the slowest case, since all spots must be completely re-drawn ## every time because their apparent transformation may have changed. s3 = pg.ScatterPlotItem(pxMode=False) ## Set pxMode=False to allow spots to transform with the view From 2b0559fd75d04bfcfb7b8344641a9c49f7a99878 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 31 Jan 2018 08:44:09 -0800 Subject: [PATCH 121/135] adjust group parameter fg color --- pyqtgraph/parametertree/parameterTypes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py index ec14faa1..42a18fe0 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -400,7 +400,7 @@ class GroupParameterItem(ParameterItem): else: for c in [0,1]: self.setBackground(c, QtGui.QBrush(QtGui.QColor(220,220,220))) - self.setForeground(c, QtGui.QBrush(QtGui.QColor(100,100,100))) + self.setForeground(c, QtGui.QBrush(QtGui.QColor(50,50,50))) font = self.font(c) font.setBold(True) #font.setPointSize(font.pointSize()+1) From 8d9cb79da442b895f6c2a2bdf9c000c076ad64a7 Mon Sep 17 00:00:00 2001 From: Fekete Imre Date: Wed, 31 Jan 2018 23:35:18 +0100 Subject: [PATCH 122/135] Fix issue # 366 Set the right texture for rendering, otherwise a previously set texture is overwritten. --- pyqtgraph/opengl/GLViewWidget.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyqtgraph/opengl/GLViewWidget.py b/pyqtgraph/opengl/GLViewWidget.py index e0fee046..71dae9c5 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -450,6 +450,7 @@ class GLViewWidget(QtOpenGL.QGLWidget): glfbo.glFramebufferTexture2D(glfbo.GL_FRAMEBUFFER, glfbo.GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0) self.paintGL(region=(x, h-y-h2, w2, h2), viewport=(0, 0, w2, h2)) # only render sub-region + glBindTexture(GL_TEXTURE_2D, tex) # fixes issue #366 ## read texture back to array data = glGetTexImage(GL_TEXTURE_2D, 0, format, type) From a17d4a6e151ce8492054289efe519e0a19d85164 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 1 Feb 2018 11:42:01 -0800 Subject: [PATCH 123/135] Fix ConsoleWidget to work with changed stack format in python 3 --- pyqtgraph/console/Console.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pyqtgraph/console/Console.py b/pyqtgraph/console/Console.py index 9b5b1bf7..634aab4a 100644 --- a/pyqtgraph/console/Console.py +++ b/pyqtgraph/console/Console.py @@ -357,6 +357,11 @@ class ConsoleWidget(QtGui.QWidget): # Build stack up to this point for index, line in enumerate(traceback.extract_stack(frame)): + # extract_stack return value changed in python 3.5 + if 'FrameSummary' in str(type(line)): + print(dir(line)) + line = (line.filename, line.lineno, line.name, line._line) + self.ui.exceptionStackList.addItem('File "%s", line %s, in %s()\n %s' % line) while frame is not None: self.frames.insert(0, frame) @@ -373,6 +378,11 @@ class ConsoleWidget(QtGui.QWidget): # And finish the rest of the stack up to the exception for index, line in enumerate(traceback.extract_tb(tb)): + # extract_stack return value changed in python 3.5 + if 'FrameSummary' in str(type(line)): + print(dir(line)) + line = (line.filename, line.lineno, line.name, line._line) + self.ui.exceptionStackList.addItem('File "%s", line %s, in %s()\n %s' % line) while tb is not None: self.frames.append(tb.tb_frame) From 9bff2b23fb85dbca45eef4baed310a8f5b1fcb31 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 1 Feb 2018 11:43:46 -0800 Subject: [PATCH 124/135] Add a signal for detecting scatter plot point clicks in ScatterPlotWidget --- pyqtgraph/widgets/ScatterPlotWidget.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/widgets/ScatterPlotWidget.py b/pyqtgraph/widgets/ScatterPlotWidget.py index cca40e65..e0071f24 100644 --- a/pyqtgraph/widgets/ScatterPlotWidget.py +++ b/pyqtgraph/widgets/ScatterPlotWidget.py @@ -33,6 +33,8 @@ class ScatterPlotWidget(QtGui.QSplitter): specifying multiple criteria. 4) A PlotWidget for displaying the data. """ + sigScatterPlotClicked = QtCore.Signal(object, object) + def __init__(self, parent=None): QtGui.QSplitter.__init__(self, QtCore.Qt.Horizontal) self.ctrlPanel = QtGui.QSplitter(QtCore.Qt.Vertical) @@ -211,8 +213,7 @@ class ScatterPlotWidget(QtGui.QSplitter): self.scatterPlot = self.plot.plot(xy[0], xy[1], data=data[mask], **style) self.scatterPlot.sigPointsClicked.connect(self.plotClicked) - def plotClicked(self, plot, points): - pass + self.sigScatterPlotClicked.emit(self, points) From 7e1e7bfdc25427abb5c824cb91ff2d1d6daf8ca9 Mon Sep 17 00:00:00 2001 From: Billy Su Date: Sat, 3 Feb 2018 00:07:49 +0800 Subject: [PATCH 125/135] Add bold style to the list Title --- doc/source/mouse_interaction.rst | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/doc/source/mouse_interaction.rst b/doc/source/mouse_interaction.rst index 0e149f0c..3aea2527 100644 --- a/doc/source/mouse_interaction.rst +++ b/doc/source/mouse_interaction.rst @@ -9,11 +9,11 @@ Most applications that use pyqtgraph's data visualization will generate widgets In pyqtgraph, most 2D visualizations follow the following mouse interaction: -* Left button: Interacts with items in the scene (select/move objects, etc). If there are no movable objects under the mouse cursor, then dragging with the left button will pan the scene instead. -* Right button drag: Scales the scene. Dragging left/right scales horizontally; dragging up/down scales vertically (although some scenes will have their x/y scales locked together). If there are x/y axes fisible in the scene, then right-dragging over the axis will _only_ affect that axis. -* Right button click: Clicking the right button in most cases will show a context menu with a variety of options depending on the object(s) under the mouse cursor. -* Middle button (or wheel) drag: Dragging the mouse with the wheel pressed down will always pan the scene (this is useful in instances where panning with the left button is prevented by other objects in the scene). -* Wheel spin: Zooms the scene in and out. +* **Left button:** Interacts with items in the scene (select/move objects, etc). If there are no movable objects under the mouse cursor, then dragging with the left button will pan the scene instead. +* **Right button drag:** Scales the scene. Dragging left/right scales horizontally; dragging up/down scales vertically (although some scenes will have their x/y scales locked together). If there are x/y axes fisible in the scene, then right-dragging over the axis will _only_ affect that axis. +* **Right button click:** Clicking the right button in most cases will show a context menu with a variety of options depending on the object(s) under the mouse cursor. +* **Middle button (or wheel) drag:** Dragging the mouse with the wheel pressed down will always pan the scene (this is useful in instances where panning with the left button is prevented by other objects in the scene). +* **Wheel spin:** Zooms the scene in and out. For machines where dragging with the right or middle buttons is difficult (usually Mac), another mouse interaction mode exists. In this mode, dragging with the left mouse button draws a box over a region of the scene. After the button is released, the scene is scaled and panned to fit the box. This mode can be accessed in the context menu or by calling:: @@ -38,11 +38,11 @@ The exact set of items available in the menu depends on the contents of the scen 3D visualizations use the following mouse interaction: -* Left button drag: Rotates the scene around a central point -* Middle button drag: Pan the scene by moving the central "look-at" point within the x-y plane -* Middle button drag + CTRL: Pan the scene by moving the central "look-at" point along the z axis -* Wheel spin: zoom in/out -* Wheel + CTRL: change field-of-view angle +* **Left button drag:** Rotates the scene around a central point +* **Middle button drag:** Pan the scene by moving the central "look-at" point within the x-y plane +* **Middle button drag + CTRL:** Pan the scene by moving the central "look-at" point along the z axis +* **Wheel spin:** zoom in/out +* **Wheel + CTRL:** change field-of-view angle And keyboard controls: From 708d9d252d480cc6fc5782b77f16a0d6bff5fc1e Mon Sep 17 00:00:00 2001 From: Billy Su Date: Sat, 3 Feb 2018 10:13:22 +0800 Subject: [PATCH 126/135] Add installation method using pip3 --- doc/source/installation.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/source/installation.rst b/doc/source/installation.rst index e2bf0f8d..b53e53ee 100644 --- a/doc/source/installation.rst +++ b/doc/source/installation.rst @@ -3,7 +3,10 @@ Installation PyQtGraph does not really require any installation scripts. All that is needed is for the pyqtgraph folder to be placed someplace importable. Most people will prefer to simply place this folder within a larger project folder. If you want to make pyqtgraph available system-wide, use one of the methods listed below: +* **Using pip3(or pip):** Just run "`pip3 install pyqtgraph`" on your command line. * **Debian, Ubuntu, and similar Linux:** Download the .deb file linked at the top of the pyqtgraph web page or install using apt by putting "deb http://luke.campagnola.me/debian dev/" in your /etc/apt/sources.list file and install the python-pyqtgraph package. * **Arch Linux:** Looks like someone has posted unofficial packages for Arch (thanks windel). (https://aur.archlinux.org/packages.php?ID=62577) * **Windows:** Download and run the .exe installer file linked at the top of the pyqtgraph web page. -* **Everybody (including OSX):** Download the .tar.gz source package linked at the top of the pyqtgraph web page, extract its contents, and run "python setup.py install" from within the extracted directory. +* **From Source(including OSX):** Download the .tar.gz source package linked at the top of the pyqtgraph_ web page, extract its contents, and run "`python setup.py install`" from within the extracted directory. + +.. _pyqtgraph: http://www.pyqtgraph.org/ From 447001876e6461dc14d2158d98883164b6122f8d Mon Sep 17 00:00:00 2001 From: Billy Su Date: Mon, 5 Feb 2018 22:47:45 +0800 Subject: [PATCH 127/135] API Reference add Graphics Windows --- doc/source/apireference.rst | 1 + .../graphicsWindows/graphicsWindows.rst | 22 +++++++++++++++++++ doc/source/graphicsWindows/index.rst | 11 ++++++++++ 3 files changed, 34 insertions(+) create mode 100644 doc/source/graphicsWindows/graphicsWindows.rst create mode 100644 doc/source/graphicsWindows/index.rst diff --git a/doc/source/apireference.rst b/doc/source/apireference.rst index c4dc64aa..bca04bb8 100644 --- a/doc/source/apireference.rst +++ b/doc/source/apireference.rst @@ -9,6 +9,7 @@ Contents: config_options functions graphicsItems/index + graphicsWindows/index widgets/index 3dgraphics/index colormap diff --git a/doc/source/graphicsWindows/graphicsWindows.rst b/doc/source/graphicsWindows/graphicsWindows.rst new file mode 100644 index 00000000..4ce96b36 --- /dev/null +++ b/doc/source/graphicsWindows/graphicsWindows.rst @@ -0,0 +1,22 @@ +Graphics Windows +================ + +.. autoclass:: pyqtgraph.GraphicsWindow + :members: + + .. automethod:: pyqtgraph.GraphicsWindow.__init__ + +.. autoclass:: pyqtgraph.TabWindow + :members: + + .. automethod:: pyqtgraph.TabWindow.__init__ + +.. autoclass:: pyqtgraph.PlotWindow + :members: + + .. automethod:: pyqtgraph.PlotWindow.__init__ + +.. autoclass:: pyqtgraph.ImageWindow + :members: + + .. automethod:: pyqtgraph.ImageWindow.__init__ \ No newline at end of file diff --git a/doc/source/graphicsWindows/index.rst b/doc/source/graphicsWindows/index.rst new file mode 100644 index 00000000..5187cfdc --- /dev/null +++ b/doc/source/graphicsWindows/index.rst @@ -0,0 +1,11 @@ +Graphics Windows +================ + +Convenience classes which create a new window with PlotWidget or ImageView. + +Contents: + +.. toctree:: + :maxdepth: 2 + + graphicsWindows \ No newline at end of file From 5e13e89480451c40fa2628ca0d5057b7cc93a573 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 13 Feb 2018 17:24:39 -0800 Subject: [PATCH 128/135] Update installation docs --- doc/source/installation.rst | 45 ++++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/doc/source/installation.rst b/doc/source/installation.rst index b53e53ee..bd1594da 100644 --- a/doc/source/installation.rst +++ b/doc/source/installation.rst @@ -1,12 +1,45 @@ Installation ============ -PyQtGraph does not really require any installation scripts. All that is needed is for the pyqtgraph folder to be placed someplace importable. Most people will prefer to simply place this folder within a larger project folder. If you want to make pyqtgraph available system-wide, use one of the methods listed below: +There are many different ways to install pyqtgraph, depending on your needs: -* **Using pip3(or pip):** Just run "`pip3 install pyqtgraph`" on your command line. -* **Debian, Ubuntu, and similar Linux:** Download the .deb file linked at the top of the pyqtgraph web page or install using apt by putting "deb http://luke.campagnola.me/debian dev/" in your /etc/apt/sources.list file and install the python-pyqtgraph package. -* **Arch Linux:** Looks like someone has posted unofficial packages for Arch (thanks windel). (https://aur.archlinux.org/packages.php?ID=62577) -* **Windows:** Download and run the .exe installer file linked at the top of the pyqtgraph web page. -* **From Source(including OSX):** Download the .tar.gz source package linked at the top of the pyqtgraph_ web page, extract its contents, and run "`python setup.py install`" from within the extracted directory. +* The most common way to install pyqtgraph is with pip:: + + $ pip install pyqtgraph + + Some users may need to call ``pip3`` instead. This method should work on + all platforms. +* To get access to the very latest features and bugfixes, clone pyqtgraph from + github:: + + $ git clone https://github.com/pyqtgraph/pyqtgraph + + Now you can install pyqtgraph from the source:: + + $ python setup.py install + + ..or you can simply place the pyqtgraph folder someplace importable, such as + inside the root of another project. PyQtGraph does not need to be "built" or + compiled in any way. +* Packages for pyqtgraph are also available in a few other forms: + + * **Anaconda**: ``conda install pyqtgraph`` + * **Debian, Ubuntu, and similar Linux:** Use ``apt install python-pyqtgraph`` or + download the .deb file linked at the top of the pyqtgraph web page. + * **Arch Linux:** has packages (thanks windel). (https://aur.archlinux.org/packages.php?ID=62577) + * **Windows:** Download and run the .exe installer file linked at the top of the pyqtgraph web page. + + +Requirements +============ + +PyQtGraph depends on: + +* Python 2.7 or Python 3.x +* A Qt library such as PyQt4, PyQt5, or PySide +* numpy + +The easiest way to meet these dependencies is with ``pip`` or with a scientific python +distribution like Anaconda. .. _pyqtgraph: http://www.pyqtgraph.org/ From 6562dfc892e499c5a0141c4e153aa8133961294d Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 13 Feb 2018 17:29:33 -0800 Subject: [PATCH 129/135] minor doc fix --- pyqtgraph/imageview/ImageView.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index ece99953..c64953de 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -239,7 +239,7 @@ class ImageView(QtGui.QWidget): levels. Options are 'mono', which provides a single level control for all image channels, and 'rgb' or 'rgba', which provide individual controls for each channel. - ================== ======================================================================= + ================== =========================================================================== **Notes:** From ae2f0c155f4d18c67e4d7cbddf36e912220396e1 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 14 Feb 2018 09:06:35 -0800 Subject: [PATCH 130/135] deprecate graphicsWindow classes --- CHANGELOG | 3 +- pyqtgraph/graphicsWindows.py | 18 ++++++++++-- pyqtgraph/widgets/GraphicsLayoutWidget.py | 36 ++++++++++++++++++++++- 3 files changed, 52 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 33106c8f..7b6c916b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -5,7 +5,8 @@ pyqtgraph-0.11.0 (in development) The result is visually the same, but children of ArrowItem are no longer rotated (this allows screen-aligned text to be attached more easily). To mimic the old behavior, use ArrowItem.rotate() instead of the `angle` argument. - + - Deprecated graphicsWindow classes; these have been unnecessary for many years because + widgets can be placed into a new window just by calling show(). pyqtgraph-0.10.0 diff --git a/pyqtgraph/graphicsWindows.py b/pyqtgraph/graphicsWindows.py index 1aa3f3f4..41c8b4d2 100644 --- a/pyqtgraph/graphicsWindows.py +++ b/pyqtgraph/graphicsWindows.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- """ -graphicsWindows.py - Convenience classes which create a new window with PlotWidget or ImageView. -Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +DEPRECATED: The classes below are convenience classes that create a new window +containting a single, specific widget. These classes are now unnecessary because +it is possible to place any widget into its own window by simply calling its +show() method. """ from .Qt import QtCore, QtGui @@ -20,6 +21,8 @@ def mkQApp(): class GraphicsWindow(GraphicsLayoutWidget): """ + (deprecated; use GraphicsLayoutWidget instead) + Convenience subclass of :class:`GraphicsLayoutWidget `. This class is intended for use from the interactive python prompt. @@ -34,6 +37,9 @@ class GraphicsWindow(GraphicsLayoutWidget): class TabWindow(QtGui.QMainWindow): + """ + (deprecated) + """ def __init__(self, title=None, size=(800,600)): mkQApp() QtGui.QMainWindow.__init__(self) @@ -52,6 +58,9 @@ class TabWindow(QtGui.QMainWindow): class PlotWindow(PlotWidget): + """ + (deprecated; use PlotWidget instead) + """ def __init__(self, title=None, **kargs): mkQApp() self.win = QtGui.QMainWindow() @@ -65,6 +74,9 @@ class PlotWindow(PlotWidget): class ImageWindow(ImageView): + """ + (deprecated; use ImageView instead) + """ def __init__(self, *args, **kargs): mkQApp() self.win = QtGui.QMainWindow() diff --git a/pyqtgraph/widgets/GraphicsLayoutWidget.py b/pyqtgraph/widgets/GraphicsLayoutWidget.py index ec7b9e0d..d42378d5 100644 --- a/pyqtgraph/widgets/GraphicsLayoutWidget.py +++ b/pyqtgraph/widgets/GraphicsLayoutWidget.py @@ -9,6 +9,31 @@ class GraphicsLayoutWidget(GraphicsView): ` with a single :class:`GraphicsLayout ` as its central item. + This widget is an easy starting point for generating multi-panel figures. + Example:: + + w = pg.GraphicsLayoutWidget() + p1 = w.addPlot(row=0, col=0) + p2 = w.addPlot(row=0, col=1) + v = w.addViewBox(row=1, col=0, colspan=2) + + Parameters + ---------- + parent : QWidget or None + The parent widget (see QWidget.__init__) + show : bool + If True, then immediately show the widget after it is created. + If the widget has no parent, then it will be shown inside a new window. + size : (width, height) tuple + Optionally resize the widget. Note: if this widget is placed inside a + layout, then this argument has no effect. + title : str or None + If specified, then set the window title for this widget. + kargs : + All extra arguments are passed to + :func:`GraphicsLayout.__init__() ` + + This class wraps several methods from its internal GraphicsLayout: :func:`nextRow ` :func:`nextColumn ` @@ -22,9 +47,18 @@ class GraphicsLayoutWidget(GraphicsView): :func:`itemIndex ` :func:`clear ` """ - def __init__(self, parent=None, **kargs): + def __init__(self, parent=None, show=False, size=None, title=None, **kargs): GraphicsView.__init__(self, parent) self.ci = GraphicsLayout(**kargs) for n in ['nextRow', 'nextCol', 'nextColumn', 'addPlot', 'addViewBox', 'addItem', 'getItem', 'addLayout', 'addLabel', 'removeItem', 'itemIndex', 'clear']: setattr(self, n, getattr(self.ci, n)) self.setCentralItem(self.ci) + + if size is not None: + self.resize(*size) + + if title is not None: + self.setWindowTitle(title) + + if show is True: + self.show() From afd8a6f423cb236bdcd9565ed1d893ec427e2fa7 Mon Sep 17 00:00:00 2001 From: Billy Su Date: Fri, 16 Feb 2018 12:15:32 +0800 Subject: [PATCH 131/135] Replace deprecate class in examples Using class GraphicsLayoutWidget to replace the deprecated class GraphicsWindow, cc #631. --- examples/CustomGraphItem.py | 2 +- examples/GraphItem.py | 2 +- examples/InfiniteLine.py | 2 +- examples/LogPlotTest.py | 2 +- examples/PanningPlot.py | 2 +- examples/PlotAutoRange.py | 2 +- examples/Plotting.py | 2 +- examples/ROIExamples.py | 2 +- examples/ROItypes.py | 2 +- examples/ScaleBar.py | 2 +- examples/Symbols.py | 2 +- examples/ViewBoxFeatures.py | 2 +- examples/contextMenu.py | 2 +- examples/crosshair.py | 2 +- examples/histogram.py | 2 +- examples/isocurve.py | 2 +- examples/linkedViews.py | 2 +- examples/logAxis.py | 2 +- examples/optics_demos.py | 2 +- examples/scrollingPlots.py | 2 +- 20 files changed, 20 insertions(+), 20 deletions(-) diff --git a/examples/CustomGraphItem.py b/examples/CustomGraphItem.py index 695768e2..8e494c3a 100644 --- a/examples/CustomGraphItem.py +++ b/examples/CustomGraphItem.py @@ -12,7 +12,7 @@ import numpy as np # Enable antialiasing for prettier plots pg.setConfigOptions(antialias=True) -w = pg.GraphicsWindow() +w = pg.GraphicsLayoutWidget(show=True) w.setWindowTitle('pyqtgraph example: CustomGraphItem') v = w.addViewBox() v.setAspectLocked() diff --git a/examples/GraphItem.py b/examples/GraphItem.py index c6362295..094b84bd 100644 --- a/examples/GraphItem.py +++ b/examples/GraphItem.py @@ -13,7 +13,7 @@ import numpy as np # Enable antialiasing for prettier plots pg.setConfigOptions(antialias=True) -w = pg.GraphicsWindow() +w = pg.GraphicsLayoutWidget(show=True) w.setWindowTitle('pyqtgraph example: GraphItem') v = w.addViewBox() v.setAspectLocked() diff --git a/examples/InfiniteLine.py b/examples/InfiniteLine.py index 50efbd04..55020776 100644 --- a/examples/InfiniteLine.py +++ b/examples/InfiniteLine.py @@ -10,7 +10,7 @@ import pyqtgraph as pg app = QtGui.QApplication([]) -win = pg.GraphicsWindow(title="Plotting items examples") +win = pg.GraphicsLayoutWidget(show=True, title="Plotting items examples") win.resize(1000,600) # Enable antialiasing for prettier plots diff --git a/examples/LogPlotTest.py b/examples/LogPlotTest.py index d408a2b4..5ae9d17e 100644 --- a/examples/LogPlotTest.py +++ b/examples/LogPlotTest.py @@ -12,7 +12,7 @@ import pyqtgraph as pg app = QtGui.QApplication([]) -win = pg.GraphicsWindow(title="Basic plotting examples") +win = pg.GraphicsLayoutWidget(show=True, title="Basic plotting examples") win.resize(1000,600) win.setWindowTitle('pyqtgraph example: LogPlotTest') diff --git a/examples/PanningPlot.py b/examples/PanningPlot.py index 165240b2..874bf330 100644 --- a/examples/PanningPlot.py +++ b/examples/PanningPlot.py @@ -9,7 +9,7 @@ import pyqtgraph as pg from pyqtgraph.Qt import QtCore, QtGui import numpy as np -win = pg.GraphicsWindow() +win = pg.GraphicsLayoutWidget(show=True) win.setWindowTitle('pyqtgraph example: PanningPlot') plt = win.addPlot() diff --git a/examples/PlotAutoRange.py b/examples/PlotAutoRange.py index 46aa3a44..0e3cd422 100644 --- a/examples/PlotAutoRange.py +++ b/examples/PlotAutoRange.py @@ -16,7 +16,7 @@ app = QtGui.QApplication([]) #mw = QtGui.QMainWindow() #mw.resize(800,800) -win = pg.GraphicsWindow(title="Plot auto-range examples") +win = pg.GraphicsLayoutWidget(show=True, title="Plot auto-range examples") win.resize(800,600) win.setWindowTitle('pyqtgraph example: PlotAutoRange') diff --git a/examples/Plotting.py b/examples/Plotting.py index 44996ae5..130698a4 100644 --- a/examples/Plotting.py +++ b/examples/Plotting.py @@ -17,7 +17,7 @@ app = QtGui.QApplication([]) #mw = QtGui.QMainWindow() #mw.resize(800,800) -win = pg.GraphicsWindow(title="Basic plotting examples") +win = pg.GraphicsLayoutWidget(show=True, title="Basic plotting examples") win.resize(1000,600) win.setWindowTitle('pyqtgraph example: Plotting') diff --git a/examples/ROIExamples.py b/examples/ROIExamples.py index a48fa7b5..2b922359 100644 --- a/examples/ROIExamples.py +++ b/examples/ROIExamples.py @@ -33,7 +33,7 @@ arr[8:13, 44:46] = 10 ## create GUI app = QtGui.QApplication([]) -w = pg.GraphicsWindow(size=(1000,800), border=True) +w = pg.GraphicsLayoutWidget(show=True, size=(1000,800), border=True) w.setWindowTitle('pyqtgraph example: ROI Examples') text = """Data Selection From Image.
\n diff --git a/examples/ROItypes.py b/examples/ROItypes.py index 9e67ebe1..1a064d33 100644 --- a/examples/ROItypes.py +++ b/examples/ROItypes.py @@ -13,7 +13,7 @@ pg.setConfigOptions(imageAxisOrder='row-major') ## create GUI app = QtGui.QApplication([]) -w = pg.GraphicsWindow(size=(800,800), border=True) +w = pg.GraphicsLayoutWidget(show=True, size=(800,800), border=True) v = w.addViewBox(colspan=2) v.invertY(True) ## Images usually have their Y-axis pointing downward v.setAspectLocked(True) diff --git a/examples/ScaleBar.py b/examples/ScaleBar.py index 5f9675e4..f125eb73 100644 --- a/examples/ScaleBar.py +++ b/examples/ScaleBar.py @@ -9,7 +9,7 @@ from pyqtgraph.Qt import QtCore, QtGui import numpy as np pg.mkQApp() -win = pg.GraphicsWindow() +win = pg.GraphicsLayoutWidget(show=True) win.setWindowTitle('pyqtgraph example: ScaleBar') vb = win.addViewBox() diff --git a/examples/Symbols.py b/examples/Symbols.py index 3dd28e13..417df35e 100755 --- a/examples/Symbols.py +++ b/examples/Symbols.py @@ -11,7 +11,7 @@ from pyqtgraph.Qt import QtGui, QtCore import pyqtgraph as pg app = QtGui.QApplication([]) -win = pg.GraphicsWindow(title="Scatter Plot Symbols") +win = pg.GraphicsLayoutWidget(show=True, title="Scatter Plot Symbols") win.resize(1000,600) pg.setConfigOptions(antialias=True) diff --git a/examples/ViewBoxFeatures.py b/examples/ViewBoxFeatures.py index 6388e41b..5757924b 100644 --- a/examples/ViewBoxFeatures.py +++ b/examples/ViewBoxFeatures.py @@ -16,7 +16,7 @@ x = np.arange(1000, dtype=float) y = np.random.normal(size=1000) y += 5 * np.sin(x/100) -win = pg.GraphicsWindow() +win = pg.GraphicsLayoutWidget(show=True) win.setWindowTitle('pyqtgraph example: ____') win.resize(1000, 800) win.ci.setBorder((50, 50, 100)) diff --git a/examples/contextMenu.py b/examples/contextMenu.py index c2c5918d..c08008aa 100644 --- a/examples/contextMenu.py +++ b/examples/contextMenu.py @@ -14,7 +14,7 @@ import pyqtgraph as pg from pyqtgraph.Qt import QtCore, QtGui import numpy as np -win = pg.GraphicsWindow() +win = pg.GraphicsLayoutWidget(show=True) win.setWindowTitle('pyqtgraph example: context menu') diff --git a/examples/crosshair.py b/examples/crosshair.py index 076fab49..584eced8 100644 --- a/examples/crosshair.py +++ b/examples/crosshair.py @@ -13,7 +13,7 @@ from pyqtgraph.Point import Point #generate layout app = QtGui.QApplication([]) -win = pg.GraphicsWindow() +win = pg.GraphicsLayoutWidget(show=True) win.setWindowTitle('pyqtgraph example: crosshair') label = pg.LabelItem(justify='right') win.addItem(label) diff --git a/examples/histogram.py b/examples/histogram.py index 2674ba30..a25f0947 100644 --- a/examples/histogram.py +++ b/examples/histogram.py @@ -8,7 +8,7 @@ import pyqtgraph as pg from pyqtgraph.Qt import QtCore, QtGui import numpy as np -win = pg.GraphicsWindow() +win = pg.GraphicsLayoutWidget(show=True) win.resize(800,350) win.setWindowTitle('pyqtgraph example: Histogram') plt1 = win.addPlot() diff --git a/examples/isocurve.py b/examples/isocurve.py index b401dfe1..6d89bbec 100644 --- a/examples/isocurve.py +++ b/examples/isocurve.py @@ -20,7 +20,7 @@ data = np.concatenate([data, data], axis=0) data = pg.gaussianFilter(data, (10, 10, 10))[frames/2:frames + frames/2] data[:, 15:16, 15:17] += 1 -win = pg.GraphicsWindow() +win = pg.GraphicsLayoutWidget(show=True) win.setWindowTitle('pyqtgraph example: Isocurve') vb = win.addViewBox() img = pg.ImageItem(data[0]) diff --git a/examples/linkedViews.py b/examples/linkedViews.py index e7eb18af..34f2b698 100644 --- a/examples/linkedViews.py +++ b/examples/linkedViews.py @@ -20,7 +20,7 @@ app = QtGui.QApplication([]) x = np.linspace(-50, 50, 1000) y = np.sin(x) / x -win = pg.GraphicsWindow(title="pyqtgraph example: Linked Views") +win = pg.GraphicsLayoutWidget(show=True, title="pyqtgraph example: Linked Views") win.resize(800,600) win.addLabel("Linked Views", colspan=2) diff --git a/examples/logAxis.py b/examples/logAxis.py index a0c7fc53..3b30c50b 100644 --- a/examples/logAxis.py +++ b/examples/logAxis.py @@ -11,7 +11,7 @@ import pyqtgraph as pg app = QtGui.QApplication([]) -w = pg.GraphicsWindow() +w = pg.GraphicsLayoutWidget(show=True) w.setWindowTitle('pyqtgraph example: logAxis') p1 = w.addPlot(0,0, title="X Semilog") p2 = w.addPlot(1,0, title="Y Semilog") diff --git a/examples/optics_demos.py b/examples/optics_demos.py index 36bfc7f9..b2ac5c8a 100644 --- a/examples/optics_demos.py +++ b/examples/optics_demos.py @@ -17,7 +17,7 @@ from pyqtgraph import Point app = pg.QtGui.QApplication([]) -w = pg.GraphicsWindow(border=0.5) +w = pg.GraphicsLayoutWidget(show=True, border=0.5) w.resize(1000, 900) w.show() diff --git a/examples/scrollingPlots.py b/examples/scrollingPlots.py index 313d4e8d..d370aa46 100644 --- a/examples/scrollingPlots.py +++ b/examples/scrollingPlots.py @@ -8,7 +8,7 @@ import pyqtgraph as pg from pyqtgraph.Qt import QtCore, QtGui import numpy as np -win = pg.GraphicsWindow() +win = pg.GraphicsLayoutWidget(show=True) win.setWindowTitle('pyqtgraph example: Scrolling Plots') From 927fe44ab9d7c7fdb1f0021f669e44183d48e85e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 23 Feb 2018 17:34:15 -0800 Subject: [PATCH 132/135] move mkQApp to Qt.py to make it easier to import internally GraphicsLayoutWidget now calls mkQApp --- pyqtgraph/Qt.py | 7 +++++++ pyqtgraph/__init__.py | 13 +------------ pyqtgraph/graphicsWindows.py | 8 +------- pyqtgraph/widgets/GraphicsLayoutWidget.py | 3 ++- pyqtgraph/widgets/__init__.py | 21 --------------------- 5 files changed, 11 insertions(+), 41 deletions(-) diff --git a/pyqtgraph/Qt.py b/pyqtgraph/Qt.py index 749943f2..ad2a8007 100644 --- a/pyqtgraph/Qt.py +++ b/pyqtgraph/Qt.py @@ -272,3 +272,10 @@ m = re.match(r'(\d+)\.(\d+).*', QtVersion) if m is not None and list(map(int, m.groups())) < versionReq: print(list(map(int, m.groups()))) raise Exception('pyqtgraph requires Qt version >= %d.%d (your version is %s)' % (versionReq[0], versionReq[1], QtVersion)) + + +def mkQApp(): + app = QtGui.QApplication.instance() + if app is None: + app = QtGui.QApplication([]) + return app diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index 520ea196..583aeaa0 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -10,7 +10,7 @@ __version__ = '0.10.0' ## 'Qt' is a local module; it is intended mainly to cover up the differences ## between PyQt4 and PySide. -from .Qt import QtGui +from .Qt import QtGui, mkQApp ## not really safe--If we accidentally create another QApplication, the process hangs (and it is very difficult to trace the cause) #if QtGui.QApplication.instance() is None: @@ -466,14 +466,3 @@ def stack(*args, **kwds): except NameError: consoles = [c] return c - - -def mkQApp(): - global QAPP - inst = QtGui.QApplication.instance() - if inst is None: - QAPP = QtGui.QApplication([]) - else: - QAPP = inst - return QAPP - diff --git a/pyqtgraph/graphicsWindows.py b/pyqtgraph/graphicsWindows.py index 41c8b4d2..b6598685 100644 --- a/pyqtgraph/graphicsWindows.py +++ b/pyqtgraph/graphicsWindows.py @@ -6,17 +6,11 @@ it is possible to place any widget into its own window by simply calling its show() method. """ -from .Qt import QtCore, QtGui +from .Qt import QtCore, QtGui, mkQApp from .widgets.PlotWidget import * from .imageview import * from .widgets.GraphicsLayoutWidget import GraphicsLayoutWidget from .widgets.GraphicsView import GraphicsView -QAPP = None - -def mkQApp(): - if QtGui.QApplication.instance() is None: - global QAPP - QAPP = QtGui.QApplication([]) class GraphicsWindow(GraphicsLayoutWidget): diff --git a/pyqtgraph/widgets/GraphicsLayoutWidget.py b/pyqtgraph/widgets/GraphicsLayoutWidget.py index d42378d5..3b41a3ca 100644 --- a/pyqtgraph/widgets/GraphicsLayoutWidget.py +++ b/pyqtgraph/widgets/GraphicsLayoutWidget.py @@ -1,4 +1,4 @@ -from ..Qt import QtGui +from ..Qt import QtGui, mkQApp from ..graphicsItems.GraphicsLayout import GraphicsLayout from .GraphicsView import GraphicsView @@ -48,6 +48,7 @@ class GraphicsLayoutWidget(GraphicsView): :func:`clear ` """ def __init__(self, parent=None, show=False, size=None, title=None, **kargs): + mkQApp() GraphicsView.__init__(self, parent) self.ci = GraphicsLayout(**kargs) for n in ['nextRow', 'nextCol', 'nextColumn', 'addPlot', 'addViewBox', 'addItem', 'getItem', 'addLayout', 'addLabel', 'removeItem', 'itemIndex', 'clear']: diff --git a/pyqtgraph/widgets/__init__.py b/pyqtgraph/widgets/__init__.py index a81fe391..e69de29b 100644 --- a/pyqtgraph/widgets/__init__.py +++ b/pyqtgraph/widgets/__init__.py @@ -1,21 +0,0 @@ -## just import everything from sub-modules - -#import os - -#d = os.path.split(__file__)[0] -#files = [] -#for f in os.listdir(d): - #if os.path.isdir(os.path.join(d, f)): - #files.append(f) - #elif f[-3:] == '.py' and f != '__init__.py': - #files.append(f[:-3]) - -#for modName in files: - #mod = __import__(modName, globals(), locals(), fromlist=['*']) - #if hasattr(mod, '__all__'): - #names = mod.__all__ - #else: - #names = [n for n in dir(mod) if n[0] != '_'] - #for k in names: - #print modName, k - #globals()[k] = getattr(mod, k) From 3240f7a435e6e744491a8f7a9544bc9d1f4e44f4 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 23 Feb 2018 17:40:36 -0800 Subject: [PATCH 133/135] fix qapp storage bug --- pyqtgraph/Qt.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/Qt.py b/pyqtgraph/Qt.py index ad2a8007..8c0041df 100644 --- a/pyqtgraph/Qt.py +++ b/pyqtgraph/Qt.py @@ -274,8 +274,9 @@ if m is not None and list(map(int, m.groups())) < versionReq: raise Exception('pyqtgraph requires Qt version >= %d.%d (your version is %s)' % (versionReq[0], versionReq[1], QtVersion)) +QAPP = None def mkQApp(): - app = QtGui.QApplication.instance() - if app is None: - app = QtGui.QApplication([]) - return app + global QAPP + if QtGui.QApplication.instance() is None: + QAPP = QtGui.QApplication([]) + return QAPP From e318bc041b34ac939809dcae8f11799781641e67 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 23 Feb 2018 17:50:44 -0800 Subject: [PATCH 134/135] Fix isosurface error: TypeError('only integer scalar arrays can be converted to a scalar index',) --- pyqtgraph/functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index a6c06ad4..e83237c6 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -2172,7 +2172,7 @@ def isosurface(data, level): ## compute lookup table of index: vertexes mapping faceTableI = np.zeros((len(triTable), i*3), dtype=np.ubyte) faceTableInds = np.argwhere(nTableFaces == i) - faceTableI[faceTableInds[:,0]] = np.array([triTable[j] for j in faceTableInds]) + faceTableI[faceTableInds[:,0]] = np.array([triTable[j[0]] for j in faceTableInds]) faceTableI = faceTableI.reshape((len(triTable), i, 3)) faceShiftTables.append(edgeShifts[faceTableI]) From a5276c3bd3ccf478662f7dd42ed0d37ac26c847d Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 8 Mar 2018 15:09:36 -0800 Subject: [PATCH 135/135] fix: scatterplotwidget behaves nicely when data contains infs Add methods to make it easier to programatically configure scatterplotwidget --- pyqtgraph/widgets/ColorMapWidget.py | 5 +++++ pyqtgraph/widgets/DataFilterWidget.py | 13 +++++++++---- pyqtgraph/widgets/ScatterPlotWidget.py | 19 +++++++++++++++---- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/pyqtgraph/widgets/ColorMapWidget.py b/pyqtgraph/widgets/ColorMapWidget.py index bd5668ae..7e6bfab7 100644 --- a/pyqtgraph/widgets/ColorMapWidget.py +++ b/pyqtgraph/widgets/ColorMapWidget.py @@ -42,6 +42,11 @@ class ColorMapWidget(ptree.ParameterTree): def restoreState(self, state): self.params.restoreState(state) + def addColorMap(self, name): + """Add a new color mapping and return the created parameter. + """ + return self.params.addNew(name) + class ColorMapParameter(ptree.types.GroupParameter): sigColorMapChanged = QtCore.Signal(object) diff --git a/pyqtgraph/widgets/DataFilterWidget.py b/pyqtgraph/widgets/DataFilterWidget.py index cae8be86..23cf930f 100644 --- a/pyqtgraph/widgets/DataFilterWidget.py +++ b/pyqtgraph/widgets/DataFilterWidget.py @@ -30,7 +30,12 @@ class DataFilterWidget(ptree.ParameterTree): def parameters(self): return self.params - + + def addFilter(self, name): + """Add a new filter and return the created parameter item. + """ + return self.params.addNew(name) + class DataFilterParameter(ptree.types.GroupParameter): @@ -47,10 +52,10 @@ class DataFilterParameter(ptree.types.GroupParameter): def addNew(self, name): mode = self.fields[name].get('mode', 'range') if mode == 'range': - self.addChild(RangeFilterItem(name, self.fields[name])) + child = self.addChild(RangeFilterItem(name, self.fields[name])) elif mode == 'enum': - self.addChild(EnumFilterItem(name, self.fields[name])) - + child = self.addChild(EnumFilterItem(name, self.fields[name])) + return child def fieldNames(self): return self.fields.keys() diff --git a/pyqtgraph/widgets/ScatterPlotWidget.py b/pyqtgraph/widgets/ScatterPlotWidget.py index e0071f24..bd0eb908 100644 --- a/pyqtgraph/widgets/ScatterPlotWidget.py +++ b/pyqtgraph/widgets/ScatterPlotWidget.py @@ -83,7 +83,19 @@ class ScatterPlotWidget(QtGui.QSplitter): item = self.fieldList.addItem(item) self.filter.setFields(fields) self.colorMap.setFields(fields) - + + def setSelectedFields(self, *fields): + self.fieldList.itemSelectionChanged.disconnect(self.fieldSelectionChanged) + try: + self.fieldList.clearSelection() + for f in fields: + i = self.fields.keys().index(f) + item = self.fieldList.item(i) + item.setSelected(True) + finally: + self.fieldList.itemSelectionChanged.connect(self.fieldSelectionChanged) + self.fieldSelectionChanged() + def setData(self, data): """ Set the data to be processed and displayed. @@ -114,7 +126,6 @@ class ScatterPlotWidget(QtGui.QSplitter): else: self.filterText.setText('\n'.join(desc)) self.filterText.setVisible(True) - def updatePlot(self): self.plot.clear() @@ -177,9 +188,9 @@ class ScatterPlotWidget(QtGui.QSplitter): ## mask out any nan values mask = np.ones(len(xy[0]), dtype=bool) if xy[0].dtype.kind == 'f': - mask &= ~np.isnan(xy[0]) + mask &= np.isfinite(xy[0]) if xy[1] is not None and xy[1].dtype.kind == 'f': - mask &= ~np.isnan(xy[1]) + mask &= np.isfinite(xy[1]) xy[0] = xy[0][mask] style['symbolBrush'] = colors[mask]