From f470a830d0346c8b0dec1574d81d713e862e8aec Mon Sep 17 00:00:00 2001 From: dlidstrom Date: Sat, 14 Mar 2015 17:30:56 -0600 Subject: [PATCH 01/29] Optionally provide custom PlotItem to PlotWidget. --- pyqtgraph/widgets/PlotWidget.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/widgets/PlotWidget.py b/pyqtgraph/widgets/PlotWidget.py index e27bce60..c23331a3 100644 --- a/pyqtgraph/widgets/PlotWidget.py +++ b/pyqtgraph/widgets/PlotWidget.py @@ -43,7 +43,7 @@ class PlotWidget(GraphicsView): For all other methods, use :func:`getPlotItem `. """ - def __init__(self, parent=None, background='default', **kargs): + def __init__(self, parent=None, background='default', plotItem=None, **kargs): """When initializing PlotWidget, *parent* and *background* are passed to :func:`GraphicsWidget.__init__() ` and all others are passed @@ -51,7 +51,10 @@ class PlotWidget(GraphicsView): GraphicsView.__init__(self, parent, background=background) self.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) self.enableMouse(False) - self.plotItem = PlotItem(**kargs) + if plotItem is None: + self.plotItem = PlotItem(**kargs) + else: + self.plotItem = plotItem self.setCentralItem(self.plotItem) ## Explicitly wrap methods from plotItem ## NOTE: If you change this list, update the documentation above as well. From 2d754672a0c696c0d97da9e7f2148f8ab99fe86e Mon Sep 17 00:00:00 2001 From: Karl Bedrich Date: Thu, 10 Nov 2016 17:01:15 +0000 Subject: [PATCH 02/29] NEW show/hide gradient ticks NEW link gradientEditorItem to other gradients --- pyqtgraph/graphicsItems/GradientEditorItem.py | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/GradientEditorItem.py b/pyqtgraph/graphicsItems/GradientEditorItem.py index 6ce06b61..55c689d4 100644 --- a/pyqtgraph/graphicsItems/GradientEditorItem.py +++ b/pyqtgraph/graphicsItems/GradientEditorItem.py @@ -450,7 +450,20 @@ class GradientEditorItem(TickSliderItem): self.addTick(1, QtGui.QColor(255,0,0), True) self.setColorMode('rgb') self.updateGradient() - + self.linkedGradients = {} + + def showTicks(self, show=True): + for tick in self.ticks.keys(): + if show: + tick.show() + orig = getattr(self, '_allowAdd_backup', None) + if orig: + self.allowAdd = orig + else: + self._allowAdd_backup = self.allowAdd + self.allowAdd = False #block tick creation + tick.hide() + def setOrientation(self, orientation): ## public """ @@ -753,7 +766,9 @@ class GradientEditorItem(TickSliderItem): for t in self.ticks: c = t.color ticks.append((self.ticks[t], (c.red(), c.green(), c.blue(), c.alpha()))) - state = {'mode': self.colorMode, 'ticks': ticks} + state = {'mode': self.colorMode, + 'ticks': ticks, + 'ticksVisible': next(iter(self.ticks)).isVisible()} return state def restoreState(self, state): @@ -778,6 +793,8 @@ class GradientEditorItem(TickSliderItem): for t in state['ticks']: c = QtGui.QColor(*t[1]) self.addTick(t[0], c, finish=False) + self.showTicks( state.get('ticksVisible', + next(iter(self.ticks)).isVisible()) ) self.updateGradient() self.sigGradientChangeFinished.emit(self) @@ -793,6 +810,18 @@ class GradientEditorItem(TickSliderItem): self.updateGradient() self.sigGradientChangeFinished.emit(self) + def linkGradient(self, slaveGradient, connect=True): + if connect: + fn = lambda g, slave=slaveGradient:slave.restoreState( + g.saveState()) + self.linkedGradients[id(slaveGradient)] = fn + self.sigGradientChanged.connect(fn) + self.sigGradientChanged.emit(self) + else: + fn = self.linkedGradients.get(id(slaveGradient), None) + if fn: + self.sigGradientChanged.disconnect(fn) + class Tick(QtGui.QGraphicsWidget): ## NOTE: Making this a subclass of GraphicsObject instead results in ## activating this bug: https://bugreports.qt-project.org/browse/PYSIDE-86 From 57909aab454dc5da35e7dedb5bd762589d63134c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20M=C3=BCller?= Date: Tue, 3 Dec 2019 11:51:44 +0100 Subject: [PATCH 03/29] dump ExportDialog.exporterParameters, b/c it prevents correct aspect ratio on image export (close #1087) --- pyqtgraph/GraphicsScene/exportDialog.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/pyqtgraph/GraphicsScene/exportDialog.py b/pyqtgraph/GraphicsScene/exportDialog.py index 045698fe..61f2233d 100644 --- a/pyqtgraph/GraphicsScene/exportDialog.py +++ b/pyqtgraph/GraphicsScene/exportDialog.py @@ -22,8 +22,6 @@ class ExportDialog(QtGui.QWidget): self.shown = False self.currentExporter = None self.scene = scene - - self.exporterParameters = {} self.selectBox = QtGui.QGraphicsRectItem() self.selectBox.setPen(fn.mkPen('y', width=3, style=QtCore.Qt.DashLine)) @@ -124,16 +122,7 @@ class ExportDialog(QtGui.QWidget): expClass = self.exporterClasses[str(item.text())] exp = expClass(item=self.ui.itemTree.currentItem().gitem) - if prev: - oldtext = str(prev.text()) - self.exporterParameters[oldtext] = self.currentExporter.parameters() - newtext = str(item.text()) - if newtext in self.exporterParameters.keys(): - params = self.exporterParameters[newtext] - exp.params = params - else: - params = exp.parameters() - self.exporterParameters[newtext] = params + params = exp.parameters() if params is None: self.ui.paramTree.clear() From ec66c34fc90fb18d2ed3b40fba2b63779a3f0e63 Mon Sep 17 00:00:00 2001 From: 2xB <2xB@users.noreply.github.com> Date: Tue, 14 Apr 2020 01:37:09 +0200 Subject: [PATCH 04/29] GraphicsLayout: Always call layout.activate() after adding items Items added to a `GraphicsLayout` only learn their size information after the internal `QGraphicsGridLayout` recalculates the layout. This is happening as a slot in the Qt event queue. Not having updated geometry bounds directly after adding an item leads to multiple issues when not executing the Qt event loop in time (see below). This commit fixes that by always calling `layout.activate()` after adding items, updating item sizes directly. This is a follow-up to PR #1167, where introducing a direct call to `processEvents` was suspected to be able to cause side effects. Notifying @j9ac9k and @campagnola, as they were involved in #1167. Fixes #8 Fixes #1136 --- pyqtgraph/graphicsItems/GraphicsLayout.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyqtgraph/graphicsItems/GraphicsLayout.py b/pyqtgraph/graphicsItems/GraphicsLayout.py index c3722ec0..9c209352 100644 --- a/pyqtgraph/graphicsItems/GraphicsLayout.py +++ b/pyqtgraph/graphicsItems/GraphicsLayout.py @@ -134,6 +134,9 @@ class GraphicsLayout(GraphicsWidget): item.geometryChanged.connect(self._updateItemBorder) self.layout.addItem(item, row, col, rowspan, colspan) + self.layout.activate() # Update layout, recalculating bounds. + # Allows some PyQtGraph features to also work without Qt event loop. + self.nextColumn() def getItem(self, row, col): From a697b5584a949f35725f0f21666ff03f0e24b489 Mon Sep 17 00:00:00 2001 From: Marcel Schumacher Date: Tue, 14 Apr 2020 17:24:54 +0200 Subject: [PATCH 05/29] Fixed a possible race condition with linked views --- pyqtgraph/graphicsItems/AxisItem.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 6b2c63df..dcb74c8f 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -1098,23 +1098,26 @@ class AxisItem(GraphicsWidget): self._updateHeight() def wheelEvent(self, ev): - if self.linkedView() is None: + lv = self.linkedView() + if lv is None: return if self.orientation in ['left', 'right']: - self.linkedView().wheelEvent(ev, axis=1) + lv.wheelEvent(ev, axis=1) else: - self.linkedView().wheelEvent(ev, axis=0) + lv.wheelEvent(ev, axis=0) ev.accept() def mouseDragEvent(self, event): - if self.linkedView() is None: + lv = self.linkedView() + if lv is None: return if self.orientation in ['left', 'right']: - return self.linkedView().mouseDragEvent(event, axis=1) + return lv.mouseDragEvent(event, axis=1) else: - return self.linkedView().mouseDragEvent(event, axis=0) + return lv.mouseDragEvent(event, axis=0) def mouseClickEvent(self, event): - if self.linkedView() is None: + lv = self.linkedView() + if lv is None: return - return self.linkedView().mouseClickEvent(event) + return lv.mouseClickEvent(event) From 4052f0dd11da29e35fd08c80492fafa600100278 Mon Sep 17 00:00:00 2001 From: Marko Bausch Date: Fri, 22 May 2020 15:17:33 +0200 Subject: [PATCH 06/29] Added context menu option to paramtree --- examples/parametertree.py | 10 ++++++++++ pyqtgraph/parametertree/Parameter.py | 10 +++++++++- pyqtgraph/parametertree/ParameterItem.py | 23 ++++++++++++++++++++--- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/examples/parametertree.py b/examples/parametertree.py index 8d8a7352..acfeac4d 100644 --- a/examples/parametertree.py +++ b/examples/parametertree.py @@ -96,6 +96,16 @@ params = [ {'name': 'Renamable', 'type': 'float', 'value': 1.2e6, 'siPrefix': True, 'suffix': 'Hz', 'renamable': True}, {'name': 'Removable', 'type': 'float', 'value': 1.2e6, 'siPrefix': True, 'suffix': 'Hz', 'removable': True}, ]}, + {'name': 'Custom context menu', 'type': 'group', 'children': [ + {'name': 'List contextMenu', 'type': 'float', 'value': 0, 'context': [ + 'menu1', + 'menu2' + ]}, + {'name': 'Dict contextMenu', 'type': 'float', 'value': 0, 'context': { + 'changeName': 'Title', + 'internal': 'What the user sees', + }}, + ]}, ComplexParameter(name='Custom parameter group (reciprocal values)'), ScalableGroup(name="Expandable Parameter Group", children=[ {'name': 'ScalableParam 1', 'type': 'str', 'value': "default param 1"}, diff --git a/pyqtgraph/parametertree/Parameter.py b/pyqtgraph/parametertree/Parameter.py index 654a33db..882fabaf 100644 --- a/pyqtgraph/parametertree/Parameter.py +++ b/pyqtgraph/parametertree/Parameter.py @@ -55,6 +55,7 @@ class Parameter(QtCore.QObject): sigDefaultChanged(self, default) Emitted when this parameter's default value has changed sigNameChanged(self, name) Emitted when this parameter's name has changed sigOptionsChanged(self, opts) Emitted when any of this parameter's options have changed + sigContextMenu(self, name) Emitted when a context menu was clicked =================================== ========================================================= """ ## name, type, limits, etc. @@ -81,7 +82,8 @@ class Parameter(QtCore.QObject): ## (but only if monitorChildren() is called) sigTreeStateChanged = QtCore.Signal(object, object) # self, changes # changes = [(param, change, info), ...] - + sigContextMenu = QtCore.Signal(object, object) # self, name + # bad planning. #def __new__(cls, *args, **opts): #try: @@ -199,6 +201,8 @@ class Parameter(QtCore.QObject): self.sigDefaultChanged.connect(lambda param, data: self.emitStateChanged('default', data)) self.sigNameChanged.connect(lambda param, data: self.emitStateChanged('name', data)) self.sigOptionsChanged.connect(lambda param, data: self.emitStateChanged('options', data)) + self.sigContextMenu.connect(lambda param, data: self.emitStateChanged('contextMenu', data)) + #self.watchParam(self) ## emit treechange signals if our own state changes @@ -206,6 +210,10 @@ class Parameter(QtCore.QObject): """Return the name of this Parameter.""" return self.opts['name'] + def contextMenu(self, name): + """"A context menu entry was clicked""" + self.sigContextMenu.emit(self, name) + def setName(self, name): """Attempt to change the name of this parameter; return the actual name. (The parameter may reject the name change or automatically pick a different name)""" diff --git a/pyqtgraph/parametertree/ParameterItem.py b/pyqtgraph/parametertree/ParameterItem.py index c149c411..4199b18b 100644 --- a/pyqtgraph/parametertree/ParameterItem.py +++ b/pyqtgraph/parametertree/ParameterItem.py @@ -47,6 +47,17 @@ class ParameterItem(QtGui.QTreeWidgetItem): self.contextMenu.addAction('Rename').triggered.connect(self.editName) if opts.get('removable', False): self.contextMenu.addAction("Remove").triggered.connect(self.requestRemove) + + # context menu + context = opts.get('context', None) + if isinstance(context, list): + for name in context: + self.contextMenu.addAction(name).triggered.connect( + self.contextMenuTriggered(name)) + elif isinstance(context, dict): + for name, title in context.items(): + self.contextMenu.addAction(title).triggered.connect( + self.contextMenuTriggered(name)) ## handle movable / dropEnabled options if opts.get('movable', False): @@ -57,7 +68,7 @@ class ParameterItem(QtGui.QTreeWidgetItem): ## flag used internally during name editing self.ignoreNameColumnChange = False - + def valueChanged(self, param, val): ## called when the parameter's value has changed @@ -106,7 +117,8 @@ class ParameterItem(QtGui.QTreeWidgetItem): pass def contextMenuEvent(self, ev): - if not self.param.opts.get('removable', False) and not self.param.opts.get('renamable', False): + if not self.param.opts.get('removable', False) and not self.param.opts.get('renamable', False)\ + and "context" not in self.param.opts: return self.contextMenu.popup(ev.globalPos()) @@ -149,7 +161,12 @@ class ParameterItem(QtGui.QTreeWidgetItem): #print opts if 'visible' in opts: self.setHidden(not opts['visible']) - + + def contextMenuTriggered(self, name): + def trigger(): + self.param.contextMenu(name) + return trigger + def editName(self): self.treeWidget().editItem(self, 0) From 369d7a11d20be768faefeef2d4a437f5bcc5aa66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20M=C3=A5nsson?= Date: Sun, 10 May 2020 23:10:10 +0200 Subject: [PATCH 07/29] Fix PixelVectors cache --- pyqtgraph/graphicsItems/GraphicsItem.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py index 3337a367..bc49f48d 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -188,24 +188,23 @@ class GraphicsItem(object): ## (such as when looking at unix timestamps), we can get floating-point errors. dt.setMatrix(dt.m11(), dt.m12(), 0, dt.m21(), dt.m22(), 0, 0, 0, 1) + if direction is None: + direction = QtCore.QPointF(1, 0) + elif direction.manhattanLength() == 0: + raise Exception("Cannot compute pixel length for 0-length vector.") + + key = (dt.m11(), dt.m21(), dt.m12(), dt.m22(), direction.x(), direction.y()) + ## check local cache - if direction is None and dt == self._pixelVectorCache[0]: + if key == self._pixelVectorCache[0]: return tuple(map(Point, self._pixelVectorCache[1])) ## return a *copy* - + ## check global cache - #key = (dt.m11(), dt.m21(), dt.m31(), dt.m12(), dt.m22(), dt.m32(), dt.m31(), dt.m32()) - key = (dt.m11(), dt.m21(), dt.m12(), dt.m22()) pv = self._pixelVectorGlobalCache.get(key, None) - if direction is None and pv is not None: - self._pixelVectorCache = [dt, pv] + if pv is not None: + self._pixelVectorCache = [key, pv] return tuple(map(Point,pv)) ## return a *copy* - - if direction is None: - direction = QtCore.QPointF(1, 0) - if direction.manhattanLength() == 0: - raise Exception("Cannot compute pixel length for 0-length vector.") - ## attempt to re-scale direction vector to fit within the precision of the coordinate system ## Here's the problem: we need to map the vector 'direction' from the item to the device, via transform 'dt'. ## In some extreme cases, this mapping can fail unless the length of 'direction' is cleverly chosen. From 50099613d58bb85197248bf1dff88138e95b2e9d Mon Sep 17 00:00:00 2001 From: Ogi Date: Thu, 28 May 2020 19:31:49 -0700 Subject: [PATCH 08/29] Pin PyVirtualDisplay Version --- azure-test-template.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-test-template.yml b/azure-test-template.yml index 04fd7e42..5d6c01e6 100644 --- a/azure-test-template.yml +++ b/azure-test-template.yml @@ -129,7 +129,7 @@ jobs: then source activate test-environment-$(python.version) fi - pip install pytest-xvfb + pip install PyVirtualDisplay==0.2.5 pytest-xvfb displayName: "Virtual Display Setup" condition: eq(variables['agent.os'], 'Linux' ) From 61942453220ac50f6d290277ab6054b54c0cf456 Mon Sep 17 00:00:00 2001 From: Daniel Hrisca Date: Sat, 30 May 2020 09:08:40 +0300 Subject: [PATCH 09/29] improve SymbolAtlas.getSymbolCoords and ScatterPlotItem.plot performance (#1198) --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 39 ++++++++++++++-------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index a774a30f..af6efcc8 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -15,6 +15,7 @@ from ..pgcollections import OrderedDict from .. import debug from ..python2_3 import basestring + __all__ = ['ScatterPlotItem', 'SpotItem'] @@ -128,8 +129,12 @@ class SymbolAtlas(object): sourceRecti = None symbol_map = self.symbolMap - for i, rec in enumerate(opts.tolist()): - size, symbol, pen, brush = rec[2: 6] + symbols = opts['symbol'].tolist() + sizes = opts['size'].tolist() + pens = opts['pen'].tolist() + brushes = opts['brush'].tolist() + + for symbol, size, pen, brush in zip(symbols, sizes, pens, brushes): key = id(symbol), size, id(pen), id(brush) if key == keyi: @@ -560,6 +565,7 @@ class ScatterPlotItem(GraphicsObject): self.invalidate() def updateSpots(self, dataSet=None): + if dataSet is None: dataSet = self.data @@ -610,8 +616,6 @@ class ScatterPlotItem(GraphicsObject): recs['brush'][np.equal(recs['brush'], None)] = fn.mkBrush(self.opts['brush']) return recs - - def measureSpotSizes(self, dataSet): for rec in dataSet: ## keep track of the maximum spot size and pixel size @@ -630,7 +634,6 @@ class ScatterPlotItem(GraphicsObject): self._maxSpotPxWidth = max(self._maxSpotPxWidth, pxWidth) self.bounds = [None, None] - def clear(self): """Remove all spots from the scatter plot""" #self.clearItems() @@ -757,8 +760,10 @@ class ScatterPlotItem(GraphicsObject): if self.opts['pxMode'] is True: p.resetTransform() + data = self.data + # Map point coordinates to device - pts = np.vstack([self.data['x'], self.data['y']]) + pts = np.vstack([data['x'], data['y']]) pts = self.mapPointsToDevice(pts) if pts is None: return @@ -770,25 +775,31 @@ class ScatterPlotItem(GraphicsObject): # Draw symbols from pre-rendered atlas atlas = self.fragmentAtlas.getAtlas() + target_rect = data['targetRect'] + source_rect = data['sourceRect'] + widths = data['width'] + # Update targetRects if necessary - updateMask = viewMask & np.equal(self.data['targetRect'], None) + updateMask = viewMask & np.equal(target_rect, None) if np.any(updateMask): updatePts = pts[:,updateMask] - width = self.data[updateMask]['width']*2 - self.data['targetRect'][updateMask] = list(imap(QtCore.QRectF, updatePts[0,:], updatePts[1,:], width, width)) + width = widths[updateMask] * 2 + target_rect[updateMask] = list(imap(QtCore.QRectF, updatePts[0,:], updatePts[1,:], width, width)) - data = self.data[viewMask] if QT_LIB == 'PyQt4': - p.drawPixmapFragments(data['targetRect'].tolist(), data['sourceRect'].tolist(), atlas) + p.drawPixmapFragments( + target_rect[viewMask].tolist(), + source_rect[viewMask].tolist(), + atlas + ) else: - list(imap(p.drawPixmap, data['targetRect'], repeat(atlas), data['sourceRect'])) + list(imap(p.drawPixmap, target_rect[viewMask].tolist(), repeat(atlas), source_rect[viewMask].tolist())) else: # render each symbol individually p.setRenderHint(p.Antialiasing, aa) - data = self.data[viewMask] pts = pts[:,viewMask] - for i, rec in enumerate(data): + for i, rec in enumerate(data[viewMask]): p.resetTransform() p.translate(pts[0,i] + rec['width']/2, pts[1,i] + rec['width']/2) drawSymbol(p, *self.getSpotOpts(rec, scale)) From ddb597a3ddda0f18a345792f9f802564893e22d4 Mon Sep 17 00:00:00 2001 From: christuart Date: Sat, 30 May 2020 07:35:58 +0100 Subject: [PATCH 10/29] Fix selection of FlowchartWidget input/output nodes from issue #808 (#809) Co-authored-by: Chris Stuart --- pyqtgraph/flowchart/Flowchart.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/flowchart/Flowchart.py b/pyqtgraph/flowchart/Flowchart.py index e269c62f..2c7b9d59 100644 --- a/pyqtgraph/flowchart/Flowchart.py +++ b/pyqtgraph/flowchart/Flowchart.py @@ -763,6 +763,9 @@ class FlowchartCtrlWidget(QtGui.QWidget): item = self.items[node] self.ui.ctrlList.setCurrentItem(item) + def clearSelection(self): + self.ui.ctrlList.selectionModel().clearSelection() + class FlowchartWidget(dockarea.DockArea): """Includes the actual graphical flowchart and debugging interface""" @@ -890,7 +893,10 @@ class FlowchartWidget(dockarea.DockArea): item = items[0] if hasattr(item, 'node') and isinstance(item.node, Node): n = item.node - self.ctrl.select(n) + if n in self.ctrl.items: + self.ctrl.select(n) + else: + self.ctrl.clearSelection() data = {'outputs': n.outputValues(), 'inputs': n.inputValues()} self.selNameLabel.setText(n.name()) if hasattr(n, 'nodeName'): From 3f6424cc573832476a62a758c320b3a115e8c3ed Mon Sep 17 00:00:00 2001 From: patricev Date: Sat, 30 May 2020 08:38:03 +0200 Subject: [PATCH 11/29] Update Data.py (#1071) * Update Data.py Python eval not working with python 3 - bug fix with the exec() part --- pyqtgraph/flowchart/library/Data.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/flowchart/library/Data.py b/pyqtgraph/flowchart/library/Data.py index 18f1c948..b133b159 100644 --- a/pyqtgraph/flowchart/library/Data.py +++ b/pyqtgraph/flowchart/library/Data.py @@ -2,6 +2,7 @@ from ..Node import Node from ...Qt import QtGui, QtCore import numpy as np +import sys from .common import * from ...SRTTransform import SRTTransform from ...Point import Point @@ -238,7 +239,12 @@ class EvalNode(Node): fn = "def fn(**args):\n" run = "\noutput=fn(**args)\n" text = fn + "\n".join([" "+l for l in str(self.text.toPlainText()).split('\n')]) + run - exec(text) + if sys.version_info.major == 2: + exec(text) + elif sys.version_info.major == 3: + ldict = locals() + exec(text, globals(), ldict) + output = ldict['output'] except: print("Error processing node: %s" % self.name()) raise From dfe83dc1c819477523453adb106a0ea83d9518f1 Mon Sep 17 00:00:00 2001 From: Ogi Date: Sat, 30 May 2020 07:15:46 -0700 Subject: [PATCH 12/29] Skipping this test on python 5.9 configs --- pyqtgraph/tests/test_exit_crash.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/tests/test_exit_crash.py b/pyqtgraph/tests/test_exit_crash.py index 5a10a0a3..41703ce6 100644 --- a/pyqtgraph/tests/test_exit_crash.py +++ b/pyqtgraph/tests/test_exit_crash.py @@ -67,7 +67,7 @@ def test_exit_crash(): os.remove(tmp) - +@pytest.mark.skipif(pg.Qt.QtVersion.startswith("5.9"), reason="Functionality not well supported, failing only on this config") def test_pg_exit(): # test the pg.exit() function code = textwrap.dedent(""" From 55e1f2c52082b2007de42036cc6f4f7b583d38ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20M=C3=A5nsson?= Date: Sat, 30 May 2020 16:25:43 +0200 Subject: [PATCH 13/29] Add cache for mapRectFromView --- pyqtgraph/graphicsItems/GraphicsItem.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py index bc49f48d..1a522446 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -19,6 +19,7 @@ class GraphicsItem(object): The GraphicsView system places a lot of emphasis on the notion that the graphics within the scene should be device independent--you should be able to take the same graphics and display them on screens of different resolutions, printers, export to SVG, etc. This is nice in principle, but causes me a lot of headache in practice. It means that I have to circumvent all the device-independent expectations any time I want to operate in pixel coordinates rather than arbitrary scene coordinates. A lot of the code in GraphicsItem is devoted to this task--keeping track of view widgets and device transforms, computing the size and shape of a pixel in local item coordinates, etc. Note that in item coordinates, a pixel does not have to be square or even rectangular, so just asking how to increase a bounding rect by 2px can be a rather complex task. """ _pixelVectorGlobalCache = LRUCache(100, 70) + _mapRectFromViewGlobalCache = LRUCache(100, 70) def __init__(self, register=None): if not hasattr(self, '_qtBaseClass'): @@ -367,8 +368,21 @@ class GraphicsItem(object): vt = self.viewTransform() if vt is None: return None - vt = fn.invertQTransform(vt) - return vt.mapRect(obj) + + cache = self._mapRectFromViewGlobalCache + k = ( + vt.m11(), vt.m12(), vt.m13(), + vt.m21(), vt.m22(), vt.m23(), + vt.m31(), vt.m32(), vt.m33(), + ) + + try: + inv_vt = cache[k] + except KeyError: + inv_vt = fn.invertQTransform(vt) + cache[k] = inv_vt + + return inv_vt.mapRect(obj) def pos(self): return Point(self._qtBaseClass.pos(self)) From 2a6f3f019315efe5bceeb2dbdfccecdd85b63a51 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sat, 30 May 2020 17:09:25 +0200 Subject: [PATCH 14/29] import numpy as np for lines 44 and 51 (#1161) * import numpy as np for lines 44 and 51 --- pyqtgraph/PlotData.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pyqtgraph/PlotData.py b/pyqtgraph/PlotData.py index e5faadda..f2760508 100644 --- a/pyqtgraph/PlotData.py +++ b/pyqtgraph/PlotData.py @@ -1,3 +1,4 @@ +import numpy as np class PlotData(object): @@ -50,7 +51,3 @@ class PlotData(object): mn = np.min(self[field]) self.minVals[field] = mn return mn - - - - \ No newline at end of file From 6c61e2445ece118d68824128b4c6f2da000b3a90 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Sat, 30 May 2020 09:09:01 -0700 Subject: [PATCH 15/29] Get docs version and copyright year dynamically --- doc/source/conf.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 3da573eb..a6e2cf8c 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -11,7 +11,9 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os +import sys +import os +from datetime import datetime # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the @@ -19,6 +21,7 @@ import sys, os path = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, os.path.join(path, '..', '..')) sys.path.insert(0, os.path.join(path, '..', 'extensions')) +import pyqtgraph # -- General configuration ----------------------------------------------------- @@ -43,16 +46,16 @@ master_doc = 'index' # General information about the project. project = 'pyqtgraph' -copyright = '2011, Luke Campagnola' +copyright = '2011 - {}, Luke Campagnola'.format(datetime.now().year) # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '0.10.0' +version = pyqtgraph.__version__ # The full version, including alpha/beta/rc tags. -release = '0.10.0' +release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. From 7d979bcf9440ef5a45f4da332fb68abfe938a220 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 30 May 2020 09:22:27 -0700 Subject: [PATCH 16/29] Check for missing ptree widget before accessing --- pyqtgraph/parametertree/parameterTypes.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py index a8e3781d..bb16d956 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -426,10 +426,13 @@ class GroupParameterItem(ParameterItem): def treeWidgetChanged(self): ParameterItem.treeWidgetChanged(self) - self.treeWidget().setFirstItemColumnSpanned(self, True) + tw = self.treeWidget() + if tw is None: + return + tw.setFirstItemColumnSpanned(self, True) if self.addItem is not None: - self.treeWidget().setItemWidget(self.addItem, 0, self.addWidgetBox) - self.treeWidget().setFirstItemColumnSpanned(self.addItem, True) + tw.setItemWidget(self.addItem, 0, self.addWidgetBox) + tw.setFirstItemColumnSpanned(self.addItem, True) def addChild(self, child): ## make sure added childs are actually inserted before add btn if self.addItem is not None: @@ -664,8 +667,12 @@ class TextParameterItem(WidgetParameterItem): ## TODO: fix so that superclass method can be called ## (WidgetParameter should just natively support this style) #WidgetParameterItem.treeWidgetChanged(self) - self.treeWidget().setFirstItemColumnSpanned(self.subItem, True) - self.treeWidget().setItemWidget(self.subItem, 0, self.textBox) + tw = self.treeWidget() + if tw is None: + return + + tw.setFirstItemColumnSpanned(self.subItem, True) + tw.setItemWidget(self.subItem, 0, self.textBox) # for now, these are copied from ParameterItem.treeWidgetChanged self.setHidden(not self.param.opts.get('visible', True)) From 7672b5b72596687295488fd1b1164ece113c71db Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Sat, 30 May 2020 22:01:39 +0200 Subject: [PATCH 17/29] Fix: Parameter tree ignores user-set 'expanded' state (#1175) * Fix: Parameter tree ignores user-set 'expanded' state When setting the 'expanded' state of parameters, this change is not applied in the graphically visible tree. This commit changes that behaviour by adding a clause in `ParameterItem.optsChanged` to react to that. Fixes #1130 * ParameterTree: Add option to synchronize "expanded" state As seen in #1130, there is interest in synchronizing the "expanded" state of `Parameter`s in `ParameterTree`s. As a default, this would lead to users being forced to always have multiple `ParameterTree`s to be expanded in the exact same way. Since that might not be desirable, this commit adds an option to customize whether synchronization of the "expanded" state should happen. * Fix: Sync Parameter options "renamable" and "removable" with ParameterTrees Currently, `Parameter` options `renamable` and `removable` are only considered when building a new `ParameterTree`. This commit makes changes in those options reflected in the corresponding `ParameterItem`s. * ParameterTree: Reflect changes in Parameter option 'tip' * Parameter: When setting "syncExpanded", update "expanded" state directly Co-authored-by: 2xB <2xB@users.noreply.github.com> --- pyqtgraph/parametertree/Parameter.py | 12 ++-- pyqtgraph/parametertree/ParameterItem.py | 70 +++++++++++++++-------- pyqtgraph/parametertree/ParameterTree.py | 10 ++++ pyqtgraph/parametertree/parameterTypes.py | 10 ++-- 4 files changed, 70 insertions(+), 32 deletions(-) diff --git a/pyqtgraph/parametertree/Parameter.py b/pyqtgraph/parametertree/Parameter.py index 882fabaf..9ef30477 100644 --- a/pyqtgraph/parametertree/Parameter.py +++ b/pyqtgraph/parametertree/Parameter.py @@ -137,9 +137,12 @@ class Parameter(QtCore.QObject): (default=False) removable If True, the user may remove this Parameter. (default=False) - expanded If True, the Parameter will appear expanded when - displayed in a ParameterTree (its children will be - visible). (default=True) + expanded If True, the Parameter will initially be expanded in + ParameterTrees: Its children will be visible. + (default=True) + syncExpanded If True, the `expanded` state of this Parameter is + synchronized with all ParameterTrees it is displayed in. + (default=False) title (str or None) If specified, then the parameter will be displayed to the user using this string as its name. However, the parameter will still be referred to @@ -161,6 +164,7 @@ class Parameter(QtCore.QObject): 'removable': False, 'strictNaming': False, # forces name to be usable as a python variable 'expanded': True, + 'syncExpanded': False, 'title': None, #'limits': None, ## This is a bad plan--each parameter type may have a different data type for limits. } @@ -461,7 +465,7 @@ class Parameter(QtCore.QObject): Set any arbitrary options on this parameter. The exact behavior of this function will depend on the parameter type, but most parameters will accept a common set of options: value, name, limits, - default, readonly, removable, renamable, visible, enabled, and expanded. + default, readonly, removable, renamable, visible, enabled, expanded and syncExpanded. See :func:`Parameter.__init__ ` for more information on default options. diff --git a/pyqtgraph/parametertree/ParameterItem.py b/pyqtgraph/parametertree/ParameterItem.py index 4199b18b..ecafd577 100644 --- a/pyqtgraph/parametertree/ParameterItem.py +++ b/pyqtgraph/parametertree/ParameterItem.py @@ -34,30 +34,20 @@ class ParameterItem(QtGui.QTreeWidgetItem): param.sigOptionsChanged.connect(self.optsChanged) param.sigParentChanged.connect(self.parentChanged) - opts = param.opts + self.updateFlags() + + ## flag used internally during name editing + self.ignoreNameColumnChange = False + + def updateFlags(self): + ## called when Parameter opts changed + opts = self.param.opts - ## Generate context menu for renaming/removing parameter - self.contextMenu = QtGui.QMenu() - self.contextMenu.addSeparator() flags = QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled if opts.get('renamable', False): - if param.opts.get('title', None) is not None: + if opts.get('title', None) is not None: raise Exception("Cannot make parameter with both title != None and renamable == True.") flags |= QtCore.Qt.ItemIsEditable - self.contextMenu.addAction('Rename').triggered.connect(self.editName) - if opts.get('removable', False): - self.contextMenu.addAction("Remove").triggered.connect(self.requestRemove) - - # context menu - context = opts.get('context', None) - if isinstance(context, list): - for name in context: - self.contextMenu.addAction(name).triggered.connect( - self.contextMenuTriggered(name)) - elif isinstance(context, dict): - for name, title in context.items(): - self.contextMenu.addAction(title).triggered.connect( - self.contextMenuTriggered(name)) ## handle movable / dropEnabled options if opts.get('movable', False): @@ -65,9 +55,6 @@ class ParameterItem(QtGui.QTreeWidgetItem): if opts.get('dropEnabled', False): flags |= QtCore.Qt.ItemIsDropEnabled self.setFlags(flags) - - ## flag used internally during name editing - self.ignoreNameColumnChange = False def valueChanged(self, param, val): @@ -120,7 +107,26 @@ class ParameterItem(QtGui.QTreeWidgetItem): if not self.param.opts.get('removable', False) and not self.param.opts.get('renamable', False)\ and "context" not in self.param.opts: return - + + ## Generate context menu for renaming/removing parameter + self.contextMenu = QtGui.QMenu() # Put in global name space to prevent garbage collection + self.contextMenu.addSeparator() + if self.param.opts.get('renamable', False): + self.contextMenu.addAction('Rename').triggered.connect(self.editName) + if self.param.opts.get('removable', False): + self.contextMenu.addAction("Remove").triggered.connect(self.requestRemove) + + # context menu + context = opts.get('context', None) + if isinstance(context, list): + for name in context: + self.contextMenu.addAction(name).triggered.connect( + self.contextMenuTriggered(name)) + elif isinstance(context, dict): + for name, title in context.items(): + self.contextMenu.addAction(title).triggered.connect( + self.contextMenuTriggered(name)) + self.contextMenu.popup(ev.globalPos()) def columnChangedEvent(self, col): @@ -141,6 +147,10 @@ class ParameterItem(QtGui.QTreeWidgetItem): self.nameChanged(self, newName) ## If the parameter rejects the name change, we need to set it back. finally: self.ignoreNameColumnChange = False + + def expandedChangedEvent(self, expanded): + if self.param.opts['syncExpanded']: + self.param.setOpts(expanded=expanded) def nameChanged(self, param, name): ## called when the parameter's name has changed. @@ -158,10 +168,22 @@ class ParameterItem(QtGui.QTreeWidgetItem): def optsChanged(self, param, opts): """Called when any options are changed that are not name, value, default, or limits""" - #print opts if 'visible' in opts: self.setHidden(not opts['visible']) + if 'expanded' in opts: + if self.param.opts['syncExpanded']: + if self.isExpanded() != opts['expanded']: + self.setExpanded(opts['expanded']) + + if 'syncExpanded' in opts: + if opts['syncExpanded']: + if self.isExpanded() != self.param.opts['expanded']: + self.setExpanded(self.param.opts['expanded']) + + self.updateFlags() + + def contextMenuTriggered(self, name): def trigger(): self.param.contextMenu(name) diff --git a/pyqtgraph/parametertree/ParameterTree.py b/pyqtgraph/parametertree/ParameterTree.py index ef7c1030..de6ab126 100644 --- a/pyqtgraph/parametertree/ParameterTree.py +++ b/pyqtgraph/parametertree/ParameterTree.py @@ -28,6 +28,8 @@ class ParameterTree(TreeWidget): self.header().setResizeMode(QtGui.QHeaderView.ResizeToContents) self.setHeaderHidden(not showHeader) self.itemChanged.connect(self.itemChangedEvent) + self.itemExpanded.connect(self.itemExpandedEvent) + self.itemCollapsed.connect(self.itemCollapsedEvent) self.lastSel = None self.setRootIsDecorated(False) @@ -134,6 +136,14 @@ class ParameterTree(TreeWidget): def itemChangedEvent(self, item, col): if hasattr(item, 'columnChangedEvent'): item.columnChangedEvent(col) + + def itemExpandedEvent(self, item): + if hasattr(item, 'expandedChangedEvent'): + item.expandedChangedEvent(True) + + def itemCollapsedEvent(self, item): + if hasattr(item, 'expandedChangedEvent'): + item.expandedChangedEvent(False) def selectionChanged(self, *args): sel = self.selectedItems() diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py index bb16d956..f1c05179 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -44,10 +44,6 @@ class WidgetParameterItem(ParameterItem): self.widget = w self.eventProxy = EventProxy(w, self.widgetEventFilter) - opts = self.param.opts - if 'tip' in opts: - w.setToolTip(opts['tip']) - self.defaultBtn = QtGui.QPushButton() self.defaultBtn.setFixedWidth(20) self.defaultBtn.setFixedHeight(20) @@ -73,6 +69,7 @@ class WidgetParameterItem(ParameterItem): w.sigChanging.connect(self.widgetValueChanging) ## update value shown in widget. + opts = self.param.opts if opts.get('value', None) is not None: self.valueChanged(self, opts['value'], force=True) else: @@ -80,6 +77,8 @@ class WidgetParameterItem(ParameterItem): self.widgetValueChanged() self.updateDefaultBtn() + + self.optsChanged(self.param, self.param.opts) def makeWidget(self): """ @@ -280,6 +279,9 @@ class WidgetParameterItem(ParameterItem): if isinstance(self.widget, (QtGui.QCheckBox,ColorButton)): self.widget.setEnabled(not opts['readonly']) + if 'tip' in opts: + self.widget.setToolTip(opts['tip']) + ## If widget is a SpinBox, pass options straight through if isinstance(self.widget, SpinBox): # send only options supported by spinbox From 949df4da16db563840d17d5287cb0a685fbfc544 Mon Sep 17 00:00:00 2001 From: Israel Brewster Date: Sat, 30 May 2020 12:09:09 -0800 Subject: [PATCH 18/29] Fix aspectRatio and zoom range issues when zooming (#1093) * Check and enforce view limits in the setRange function * Check limits when setting aspectRatio - This change is required due to moving the limit checking out of the updateViewRange function. - If the original logic remained, aspect ratio could be lost due to "squshing" the requested view into the viewBox * Add tests for ViewBox zooming limits and aspect ratio * - Move test code to proper location and fix instantiation of QApplication Co-authored-by: Israel Brewster --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 176 ++++++++------- .../ViewBox/tests/test_ViewBoxZoom.py | 200 ++++++++++++++++++ 2 files changed, 297 insertions(+), 79 deletions(-) create mode 100644 pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBoxZoom.py diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index bf2bb5b5..94aa6243 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -282,7 +282,7 @@ class ViewBox(GraphicsWidget): #if scene is not None and hasattr(scene, 'sigPrepareForPaint'): #scene.sigPrepareForPaint.connect(self.prepareForPaint) #return ret - + def update(self, *args, **kwargs): self.prepareForPaint() GraphicsWidget.update(self, *args, **kwargs) @@ -398,12 +398,12 @@ class ViewBox(GraphicsWidget): """ if item.zValue() < self.zValue(): item.setZValue(self.zValue()+1) - + scene = self.scene() if scene is not None and scene is not item.scene(): scene.addItem(item) ## Necessary due to Qt bug: https://bugreports.qt-project.org/browse/QTBUG-18616 item.setParentItem(self.childGroup) - + if not ignoreBounds: self.addedItems.append(item) self.updateAutoRange() @@ -414,12 +414,12 @@ class ViewBox(GraphicsWidget): self.addedItems.remove(item) except: pass - + scene = self.scene() if scene is not None: scene.removeItem(item) item.setParentItem(None) - + self.updateAutoRange() def clear(self): @@ -431,19 +431,19 @@ class ViewBox(GraphicsWidget): def resizeEvent(self, ev): self._matrixNeedsUpdate = True self.updateMatrix() - + self.linkedXChanged() self.linkedYChanged() - + self.updateAutoRange() self.updateViewRange() - + self._matrixNeedsUpdate = True self.updateMatrix() - + self.background.setRect(self.rect()) self.borderRect.setRect(self.rect()) - + self.sigStateChanged.emit(self) self.sigResized.emit(self) self.childGroup.prepareGeometryChange() @@ -536,7 +536,11 @@ class ViewBox(GraphicsWidget): yOff = False if setRequested[1] else None self.enableAutoRange(x=xOff, y=yOff) changed.append(True) - + + limits = (self.state['limits']['xLimits'], self.state['limits']['yLimits']) + minRng = [self.state['limits']['xRange'][0], self.state['limits']['yRange'][0]] + maxRng = [self.state['limits']['xRange'][1], self.state['limits']['yRange'][1]] + for ax, range in changes.items(): mn = min(range) mx = max(range) @@ -564,6 +568,39 @@ class ViewBox(GraphicsWidget): mn -= p mx += p + # max range cannot be larger than bounds, if they are given + if limits[ax][0] is not None and limits[ax][1] is not None: + if maxRng[ax] is not None: + maxRng[ax] = min(maxRng[ax], limits[ax][1] - limits[ax][0]) + else: + maxRng[ax] = limits[ax][1] - limits[ax][0] + + # If we have limits, we will have at least a max range as well + if maxRng[ax] is not None or minRng[ax] is not None: + diff = mx - mn + if maxRng[ax] is not None and diff > maxRng[ax]: + delta = maxRng[ax] - diff + elif minRng[ax] is not None and diff < minRng[ax]: + delta = minRng[ax] - diff + else: + delta = 0 + + mn -= delta / 2. + mx += delta / 2. + + # Make sure our requested area is within limits, if any + if limits[ax][0] is not None or limits[ax][1] is not None: + lmn, lmx = limits[ax] + if lmn is not None and mn < lmn: + delta = lmn - mn # Shift the requested view to match our lower limit + mn = lmn + mx += delta + elif lmx is not None and mx > lmx: + delta = lmx - mx + mx = lmx + mn += delta + + # Set target range if self.state['targetRange'][ax] != [mn, mx]: self.state['targetRange'][ax] = [mn, mx] @@ -1443,40 +1480,6 @@ class ViewBox(GraphicsWidget): aspect = self.state['aspectLocked'] # size ratio / view ratio tr = self.targetRect() bounds = self.rect() - if aspect is not False and 0 not in [aspect, tr.height(), bounds.height(), bounds.width()]: - - ## This is the view range aspect ratio we have requested - targetRatio = tr.width() / tr.height() if tr.height() != 0 else 1 - ## This is the view range aspect ratio we need to obey aspect constraint - viewRatio = (bounds.width() / bounds.height() if bounds.height() != 0 else 1) / aspect - viewRatio = 1 if viewRatio == 0 else viewRatio - - # Decide which range to keep unchanged - #print self.name, "aspect:", aspect, "changed:", changed, "auto:", self.state['autoRange'] - if forceX: - ax = 0 - elif forceY: - ax = 1 - else: - # if we are not required to keep a particular axis unchanged, - # then make the entire target range visible - ax = 0 if targetRatio > viewRatio else 1 - - if ax == 0: - ## view range needs to be taller than target - dy = 0.5 * (tr.width() / viewRatio - tr.height()) - if dy != 0: - changed[1] = True - viewRange[1] = [self.state['targetRange'][1][0] - dy, self.state['targetRange'][1][1] + dy] - else: - ## view range needs to be wider than target - dx = 0.5 * (tr.height() * viewRatio - tr.width()) - if dx != 0: - changed[0] = True - viewRange[0] = [self.state['targetRange'][0][0] - dx, self.state['targetRange'][0][1] + dx] - - - # ----------- Make corrections for view limits ----------- limits = (self.state['limits']['xLimits'], self.state['limits']['yLimits']) minRng = [self.state['limits']['xRange'][0], self.state['limits']['yRange'][0]] @@ -1489,43 +1492,58 @@ class ViewBox(GraphicsWidget): # max range cannot be larger than bounds, if they are given if limits[axis][0] is not None and limits[axis][1] is not None: if maxRng[axis] is not None: - maxRng[axis] = min(maxRng[axis], limits[axis][1]-limits[axis][0]) + maxRng[axis] = min(maxRng[axis], limits[axis][1] - limits[axis][0]) else: - maxRng[axis] = limits[axis][1]-limits[axis][0] + maxRng[axis] = limits[axis][1] - limits[axis][0] - #print "\nLimits for axis %d: range=%s min=%s max=%s" % (axis, limits[axis], minRng[axis], maxRng[axis]) - #print "Starting range:", viewRange[axis] + if aspect is not False and 0 not in [aspect, tr.height(), bounds.height(), bounds.width()]: - # Apply xRange, yRange - diff = viewRange[axis][1] - viewRange[axis][0] - if maxRng[axis] is not None and diff > maxRng[axis]: - delta = maxRng[axis] - diff - changed[axis] = True - elif minRng[axis] is not None and diff < minRng[axis]: - delta = minRng[axis] - diff - changed[axis] = True + ## This is the view range aspect ratio we have requested + targetRatio = tr.width() / tr.height() if tr.height() != 0 else 1 + ## This is the view range aspect ratio we need to obey aspect constraint + viewRatio = (bounds.width() / bounds.height() if bounds.height() != 0 else 1) / aspect + viewRatio = 1 if viewRatio == 0 else viewRatio + + # Calculate both the x and y ranges that would be needed to obtain the desired aspect ratio + dy = 0.5 * (tr.width() / viewRatio - tr.height()) + dx = 0.5 * (tr.height() * viewRatio - tr.width()) + + rangeY = [self.state['targetRange'][1][0] - dy, self.state['targetRange'][1][1] + dy] + rangeX = [self.state['targetRange'][0][0] - dx, self.state['targetRange'][0][1] + dx] + + canidateRange = [rangeX, rangeY] + + # Decide which range to try to keep unchanged + #print self.name, "aspect:", aspect, "changed:", changed, "auto:", self.state['autoRange'] + if forceX: + ax = 0 + elif forceY: + ax = 1 else: - delta = 0 + # if we are not required to keep a particular axis unchanged, + # then try to make the entire target range visible + ax = 0 if targetRatio > viewRatio else 1 + target = 0 if ax == 1 else 1 + # See if this choice would cause out-of-range issues + if maxRng is not None or minRng is not None: + diff = canidateRange[target][1] - canidateRange[target][0] + if maxRng[target] is not None and diff > maxRng[target] or \ + minRng[target] is not None and diff < minRng[target]: + # tweak the target range down so we can still pan properly + self.state['targetRange'][ax] = canidateRange[ax] + ax = target # Switch the "fixed" axes - viewRange[axis][0] -= delta/2. - viewRange[axis][1] += delta/2. + if ax == 0: + ## view range needs to be taller than target + if dy != 0: + changed[1] = True + viewRange[1] = rangeY + else: + ## view range needs to be wider than target + if dx != 0: + changed[0] = True + viewRange[0] = rangeX - #print "after applying min/max:", viewRange[axis] - - # Apply xLimits, yLimits - mn, mx = limits[axis] - if mn is not None and viewRange[axis][0] < mn: - delta = mn - viewRange[axis][0] - viewRange[axis][0] += delta - viewRange[axis][1] += delta - changed[axis] = True - elif mx is not None and viewRange[axis][1] > mx: - delta = mx - viewRange[axis][1] - viewRange[axis][0] += delta - viewRange[axis][1] += delta - changed[axis] = True - - #print "after applying edge limits:", viewRange[axis] changed = [(viewRange[i][0] != self.state['viewRange'][i][0]) or (viewRange[i][1] != self.state['viewRange'][i][1]) for i in (0,1)] self.state['viewRange'] = viewRange @@ -1605,13 +1623,13 @@ class ViewBox(GraphicsWidget): self.window() except RuntimeError: ## this view has already been deleted; it will probably be collected shortly. return - + def view_key(view): return (view.window() is self.window(), view.name) - + ## make a sorted list of all named views nv = sorted(ViewBox.NamedViews.values(), key=view_key) - + if self in nv: nv.remove(self) diff --git a/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBoxZoom.py b/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBoxZoom.py new file mode 100644 index 00000000..4bad9ee1 --- /dev/null +++ b/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBoxZoom.py @@ -0,0 +1,200 @@ +# -*- coding: utf-8 -*- +import pyqtgraph as pg +import pytest + +app = pg.mkQApp() + +def test_zoom_normal(): + vb = pg.ViewBox() + testRange = pg.QtCore.QRect(0, 0, 10, 20) + vb.setRange(testRange, padding=0) + vbViewRange = vb.getState()['viewRange'] + assert vbViewRange == [[testRange.left(), testRange.right()], + [testRange.top(), testRange.bottom()]] + +def test_zoom_limit(): + """Test zooming with X and Y limits set""" + vb = pg.ViewBox() + vb.setLimits(xMin=0, xMax=10, yMin=0, yMax=10) + + # Try zooming within limits. Should return unmodified + testRange = pg.QtCore.QRect(0, 0, 9, 9) + vb.setRange(testRange, padding=0) + vbViewRange = vb.getState()['viewRange'] + assert vbViewRange == [[testRange.left(), testRange.right()], + [testRange.top(), testRange.bottom()]] + + # And outside limits. both view range and targetRange should be set to limits + testRange = pg.QtCore.QRect(-5, -5, 16, 20) + vb.setRange(testRange, padding=0) + + expected = [[0, 10], [0, 10]] + vbState = vb.getState() + + assert vbState['targetRange'] == expected + assert vbState['viewRange'] == expected + +def test_zoom_range_limit(): + """Test zooming with XRange and YRange limits set, but no X and Y limits""" + vb = pg.ViewBox() + vb.setLimits(minXRange=5, maxXRange=10, minYRange=5, maxYRange=10) + + # Try something within limits + testRange = pg.QtCore.QRect(-15, -15, 7, 7) + vb.setRange(testRange, padding=0) + + expected = [[testRange.left(), testRange.right()], + [testRange.top(), testRange.bottom()]] + + vbViewRange = vb.getState()['viewRange'] + assert vbViewRange == expected + + # and outside limits + testRange = pg.QtCore.QRect(-15, -15, 17, 17) + + # Code should center the required width reduction, so move each side by 3 + expected = [[testRange.left() + 3, testRange.right() - 3], + [testRange.top() + 3, testRange.bottom() - 3]] + + vb.setRange(testRange, padding=0) + vbViewRange = vb.getState()['viewRange'] + vbTargetRange = vb.getState()['targetRange'] + + assert vbViewRange == expected + assert vbTargetRange == expected + +def test_zoom_ratio(): + """Test zooming with a fixed aspect ratio set""" + vb = pg.ViewBox(lockAspect=1) + + # Give the viewbox a size of the proper aspect ratio to keep things easy + vb.setFixedHeight(10) + vb.setFixedWidth(10) + + # request a range with a good ratio + testRange = pg.QtCore.QRect(0, 0, 10, 10) + vb.setRange(testRange, padding=0) + expected = [[testRange.left(), testRange.right()], + [testRange.top(), testRange.bottom()]] + + viewRange = vb.getState()['viewRange'] + viewWidth = viewRange[0][1] - viewRange[0][0] + viewHeight = viewRange[1][1] - viewRange[1][0] + + # Assert that the width and height are equal, since we locked the aspect ratio at 1 + assert viewWidth == viewHeight + + # and for good measure, that it is the same as the test range + assert viewRange == expected + + # Now try to set to something with a different aspect ratio + testRange = pg.QtCore.QRect(0, 0, 10, 20) + vb.setRange(testRange, padding=0) + + viewRange = vb.getState()['viewRange'] + viewWidth = viewRange[0][1] - viewRange[0][0] + viewHeight = viewRange[1][1] - viewRange[1][0] + + # Don't really care what we got here, as long as the width and height are the same + assert viewWidth == viewHeight + +def test_zoom_ratio2(): + """Slightly more complicated zoom ratio test, where the view box shape does not match the ratio""" + vb = pg.ViewBox(lockAspect=1) + + # twice as wide as tall + vb.setFixedHeight(10) + vb.setFixedWidth(20) + + # more or less random requested range + testRange = pg.QtCore.QRect(0, 0, 10, 15) + vb.setRange(testRange, padding=0) + + viewRange = vb.getState()['viewRange'] + viewWidth = viewRange[0][1] - viewRange[0][0] + viewHeight = viewRange[1][1] - viewRange[1][0] + + # View width should be twice as wide as the height, + # since the viewbox is twice as wide as it is tall. + assert viewWidth == 2 * viewHeight + +def test_zoom_ratio_with_limits1(): + """Test zoom with both ratio and limits set""" + vb = pg.ViewBox(lockAspect=1) + + # twice as wide as tall + vb.setFixedHeight(10) + vb.setFixedWidth(20) + + # set some limits + vb.setLimits(xMin=-5, xMax=5, yMin=-5, yMax=5) + + # Try to zoom too tall + testRange = pg.QtCore.QRect(0, 0, 6, 10) + vb.setRange(testRange, padding=0) + + viewRange = vb.getState()['viewRange'] + viewWidth = viewRange[0][1] - viewRange[0][0] + viewHeight = viewRange[1][1] - viewRange[1][0] + + # Make sure our view is within limits and the proper aspect ratio + assert viewRange[0][0] >= -5 + assert viewRange[0][1] <= 5 + assert viewRange[1][0] >= -5 + assert viewRange[1][1] <= 5 + assert viewWidth == 2 * viewHeight + +def test_zoom_ratio_with_limits2(): + vb = pg.ViewBox(lockAspect=1) + + # twice as wide as tall + vb.setFixedHeight(10) + vb.setFixedWidth(20) + + # set some limits + vb.setLimits(xMin=-5, xMax=5, yMin=-5, yMax=5) + + # Same thing, but out-of-range the other way + testRange = pg.QtCore.QRect(0, 0, 16, 6) + vb.setRange(testRange, padding=0) + + viewRange = vb.getState()['viewRange'] + viewWidth = viewRange[0][1] - viewRange[0][0] + viewHeight = viewRange[1][1] - viewRange[1][0] + + # Make sure our view is within limits and the proper aspect ratio + assert viewRange[0][0] >= -5 + assert viewRange[0][1] <= 5 + assert viewRange[1][0] >= -5 + assert viewRange[1][1] <= 5 + assert viewWidth == 2 * viewHeight + +def test_zoom_ratio_with_limits_out_of_range(): + vb = pg.ViewBox(lockAspect=1) + + # twice as wide as tall + vb.setFixedHeight(10) + vb.setFixedWidth(20) + + # set some limits + vb.setLimits(xMin=-5, xMax=5, yMin=-5, yMax=5) + + # Request something completely out-of-range and out-of-aspect + testRange = pg.QtCore.QRect(10, 10, 25, 100) + vb.setRange(testRange, padding=0) + + viewRange = vb.getState()['viewRange'] + viewWidth = viewRange[0][1] - viewRange[0][0] + viewHeight = viewRange[1][1] - viewRange[1][0] + + # Make sure our view is within limits and the proper aspect ratio + assert viewRange[0][0] >= -5 + assert viewRange[0][1] <= 5 + assert viewRange[1][0] >= -5 + assert viewRange[1][1] <= 5 + assert viewWidth == 2 * viewHeight + + +if __name__ == "__main__": + setup_module(None) + test_zoom_ratio() From e08ac110f594a7cbe55f38608542237e0a855ef5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20M=C3=BCller?= Date: Sat, 30 May 2020 22:53:38 +0200 Subject: [PATCH 19/29] pretty-print log-scale axes labels (#1097) * pretty-print log-scale axes labels * only pretty-print in python 3 --- pyqtgraph/graphicsItems/AxisItem.py | 33 ++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 44541673..29f3ad62 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -4,6 +4,7 @@ from ..python2_3 import asUnicode import numpy as np from ..Point import Point from .. import debug as debug +import sys import weakref from .. import functions as fn from .. import getConfigOption @@ -813,7 +814,37 @@ class AxisItem(GraphicsWidget): return strings def logTickStrings(self, values, scale, spacing): - return ["%0.1g"%x for x in 10 ** np.array(values).astype(float) * np.array(scale)] + estrings = ["%0.1g"%x for x in 10 ** np.array(values).astype(float) * np.array(scale)] + + if sys.version_info < (3, 0): + # python 2 does not support unicode strings like that + return estrings + else: # python 3+ + convdict = {"0": "⁰", + "1": "¹", + "2": "²", + "3": "³", + "4": "⁴", + "5": "⁵", + "6": "⁶", + "7": "⁷", + "8": "⁸", + "9": "⁹", + } + dstrings = [] + for e in estrings: + if e.count("e"): + v, p = e.split("e") + sign = "⁻" if p[0] == "-" else "" + pot = "".join([convdict[pp] for pp in p[1:].lstrip("0")]) + if v == "1": + v = "" + else: + v = v + "·" + dstrings.append(v + "10" + sign + pot) + else: + dstrings.append(e) + return dstrings def generateDrawSpecs(self, p): """ From 173a755b6c28ee276caf36fa1578fff6844e9c82 Mon Sep 17 00:00:00 2001 From: Ogi Date: Sat, 30 May 2020 21:13:20 -0700 Subject: [PATCH 20/29] Encode csv export header as unicode --- pyqtgraph/exporters/CSVExporter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/exporters/CSVExporter.py b/pyqtgraph/exporters/CSVExporter.py index c7591932..33c6ec69 100644 --- a/pyqtgraph/exporters/CSVExporter.py +++ b/pyqtgraph/exporters/CSVExporter.py @@ -3,6 +3,7 @@ from ..Qt import QtGui, QtCore from .Exporter import Exporter from ..parametertree import Parameter from .. import PlotItem +from ..python2_3 import asUnicode __all__ = ['CSVExporter'] @@ -57,7 +58,7 @@ class CSVExporter(Exporter): sep = '\t' with open(fileName, 'w') as fd: - fd.write(sep.join(header) + '\n') + fd.write(sep.join(map(asUnicode, header)) + '\n') i = 0 numFormat = '%%0.%dg' % self.params['precision'] numRows = max([len(d[0]) for d in data]) From ed009d3779d6d2f4f31b9c2dca255993f9162e01 Mon Sep 17 00:00:00 2001 From: ChristophRose <42769515+ChristophRose@users.noreply.github.com> Date: Fri, 31 Aug 2018 09:16:37 +0200 Subject: [PATCH 21/29] Check lastDownsample in viewTransformChanged Add a check in the viewTransformChanged function to only force a rerender when the downsampling factor changed. Previously simply moving the image around or zooming in/out without changing the downsampling factor would force a complete rerendering of the image, which was very slow with large images. This way, the expensive rerender is only forced if necessary. --- pyqtgraph/graphicsItems/ImageItem.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index b05c2f70..4b3a94cc 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -51,6 +51,7 @@ class ImageItem(GraphicsObject): self.levels = None ## [min, max] or [[redMin, redMax], ...] self.lut = None self.autoDownsample = False + self._lastDownsample = (1, 1) self.axisOrder = getConfigOption('imageAxisOrder') @@ -551,8 +552,19 @@ class ImageItem(GraphicsObject): def viewTransformChanged(self): if self.autoDownsample: - self.qimage = None - self.update() + o = self.mapToDevice(QtCore.QPointF(0,0)) + x = self.mapToDevice(QtCore.QPointF(1,0)) + y = self.mapToDevice(QtCore.QPointF(0,1)) + w = Point(x-o).length() + h = Point(y-o).length() + if w == 0 or h == 0: + self.qimage = None + return + xds = max(1, int(1.0 / w)) + yds = max(1, int(1.0 / h)) + if (xds, yds) != self._lastDownsample: + self.qimage = None + self.update() def mouseDragEvent(self, ev): if ev.button() != QtCore.Qt.LeftButton: From 1f9ccccfd08086b25d3d944437282662ef6d8dc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Font=C3=A1n=20Correa?= Date: Mon, 3 Jul 2017 10:30:39 +0200 Subject: [PATCH 22/29] Fix Dock close event QLabel still running with no parent --- pyqtgraph/dockarea/Dock.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/pyqtgraph/dockarea/Dock.py b/pyqtgraph/dockarea/Dock.py index a7234073..15c87652 100644 --- a/pyqtgraph/dockarea/Dock.py +++ b/pyqtgraph/dockarea/Dock.py @@ -5,10 +5,10 @@ from ..widgets.VerticalLabel import VerticalLabel from ..python2_3 import asUnicode class Dock(QtGui.QWidget, DockDrop): - + sigStretchChanged = QtCore.Signal() sigClosed = QtCore.Signal(object) - + def __init__(self, name, area=None, size=(10, 10), widget=None, hideTitle=False, autoOrientation=True, closable=False): QtGui.QWidget.__init__(self) DockDrop.__init__(self) @@ -68,9 +68,9 @@ class Dock(QtGui.QWidget, DockDrop): }""" self.setAutoFillBackground(False) self.widgetArea.setStyleSheet(self.hStyle) - + self.setStretch(*size) - + if widget is not None: self.addWidget(widget) @@ -82,7 +82,7 @@ class Dock(QtGui.QWidget, DockDrop): return ['dock'] else: return name == 'dock' - + def setStretch(self, x=None, y=None): """ Set the 'target' size for this Dock. @@ -109,7 +109,7 @@ class Dock(QtGui.QWidget, DockDrop): if 'center' in self.allowedAreas: self.allowedAreas.remove('center') self.updateStyle() - + def showTitleBar(self): """ Show the title bar for this Dock. @@ -130,7 +130,7 @@ class Dock(QtGui.QWidget, DockDrop): Sets the text displayed in title bar for this Dock. """ self.label.setText(text) - + def setOrientation(self, o='auto', force=False): """ Sets the orientation of the title bar for this Dock. @@ -149,7 +149,7 @@ class Dock(QtGui.QWidget, DockDrop): self.orientation = o self.label.setOrientation(o) self.updateStyle() - + def updateStyle(self): ## updates orientation and appearance of title bar if self.labelHidden: @@ -192,7 +192,7 @@ class Dock(QtGui.QWidget, DockDrop): self.update() action = self.drag.exec_() self.updateStyle() - + def float(self): self.area.floatDock(self) @@ -223,6 +223,7 @@ class Dock(QtGui.QWidget, DockDrop): def close(self): """Remove this dock from the DockArea it lives inside.""" self.setParent(None) + QtGui.QLabel.close(self.label) self.label.setParent(None) self._container.apoptose() self._container = None @@ -247,10 +248,10 @@ class Dock(QtGui.QWidget, DockDrop): class DockLabel(VerticalLabel): - + sigClicked = QtCore.Signal(object, object) sigCloseClicked = QtCore.Signal() - + def __init__(self, text, dock, showCloseButton): self.dim = False self.fixedWidth = False @@ -277,7 +278,7 @@ class DockLabel(VerticalLabel): fg = '#fff' bg = '#66c' border = '#55B' - + if self.orientation == 'vertical': self.vStyle = """DockLabel { background-color : %s; @@ -311,7 +312,7 @@ class DockLabel(VerticalLabel): if self.dim != d: self.dim = d self.updateStyle() - + def setOrientation(self, o): VerticalLabel.setOrientation(self, o) self.updateStyle() @@ -321,12 +322,12 @@ class DockLabel(VerticalLabel): self.pressPos = ev.pos() self.startedDrag = False ev.accept() - + def mouseMoveEvent(self, ev): if not self.startedDrag and (ev.pos() - self.pressPos).manhattanLength() > QtGui.QApplication.startDragDistance(): self.dock.startDrag() ev.accept() - + def mouseReleaseEvent(self, ev): ev.accept() if not self.startedDrag: @@ -335,7 +336,7 @@ class DockLabel(VerticalLabel): def mouseDoubleClickEvent(self, ev): if ev.button() == QtCore.Qt.LeftButton: self.dock.float() - + def resizeEvent (self, ev): if self.closeButton: if self.orientation == 'vertical': From c90354667920db2f439dd4b8884f19f719e18760 Mon Sep 17 00:00:00 2001 From: Zach Lowry Date: Sun, 31 May 2020 19:39:51 -0500 Subject: [PATCH 23/29] Fix duplicate menus in GradientEditorItem (#444) * Fix duplicate menus in GradientEditorItem Add call to ev.accept in Tivk.mouseClickEvent to prevent parent menu from opening on a right click of a Tick. Co-authored-by: Ogi --- pyqtgraph/graphicsItems/GradientEditorItem.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/graphicsItems/GradientEditorItem.py b/pyqtgraph/graphicsItems/GradientEditorItem.py index 12233aad..4bd51aa7 100644 --- a/pyqtgraph/graphicsItems/GradientEditorItem.py +++ b/pyqtgraph/graphicsItems/GradientEditorItem.py @@ -874,8 +874,8 @@ class Tick(QtGui.QGraphicsWidget): ## NOTE: Making this a subclass of GraphicsO self.view().tickMoveFinished(self) def mouseClickEvent(self, ev): - if ev.button() == QtCore.Qt.RightButton and self.moving: - ev.accept() + ev.accept() + if ev.button() == QtCore.Qt.RightButton and self.moving: self.setPos(self.startPosition) self.view().tickMoved(self, self.startPosition) self.moving = False @@ -883,7 +883,6 @@ class Tick(QtGui.QGraphicsWidget): ## NOTE: Making this a subclass of GraphicsO self.sigMoved.emit(self) else: self.view().tickClicked(self, ev) - ##remove def hoverEvent(self, ev): if (not ev.isExit()) and ev.acceptDrags(QtCore.Qt.LeftButton): From 245d89033eeeb481c3531bc90410ca609f19271c Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Mon, 1 Jun 2020 00:09:16 -0700 Subject: [PATCH 24/29] Identify pyqt5 515 ci issue (#1221) * move forward pyvirtualdisplay * Try installing things per QTBUG-84489 * Debug plugins to 1 * Removing all the other packages, adding libxcb-xfixes0 * adding libxcb-icccm4 per plugin debug * adding libxcb-image0, restoring pyvirtualdisplay to older version * now adding libxcb-keysyms1 * libxcb-randr0 * adding libxcb-render-util0 * adding libxcb-xinerama0 * Restore Configs, Properly Name Latest Pipeline --- azure-test-template.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/azure-test-template.yml b/azure-test-template.yml index 5d6c01e6..5cdae342 100644 --- a/azure-test-template.yml +++ b/azure-test-template.yml @@ -26,7 +26,7 @@ jobs: python.version: "3.7" qt.bindings: "pyside2" install.method: "conda" - Python38-PyQt-5.14: + Python38-PyQt-Latest: python.version: '3.8' qt.bindings: "PyQt5" install.method: "pip" @@ -124,7 +124,9 @@ jobs: displayName: 'Install Wheel' - bash: | - sudo apt-get install -y libxkbcommon-x11-0 # herbstluftwm + sudo apt-get install -y libxkbcommon-x11-dev + # workaround for QTBUG-84489 + sudo apt-get install -y libxcb-xfixes0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 if [ $(install.method) == "conda" ] then source activate test-environment-$(python.version) @@ -134,6 +136,7 @@ jobs: condition: eq(variables['agent.os'], 'Linux' ) - bash: | + export QT_DEBUG_PLUGINS=1 if [ $(install.method) == "conda" ] then source activate test-environment-$(python.version) From 68b8dbac1aba2b456fa584b48640f144c5ece411 Mon Sep 17 00:00:00 2001 From: Karl Georg Bedrich Date: Mon, 1 Jun 2020 20:05:39 +0200 Subject: [PATCH 25/29] moved some functionality from method 'export' to new method (#390) * moved some functionality from method 'export' to new method 'getSupportedFormats' making it accessible from outside --- pyqtgraph/exporters/ImageExporter.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/pyqtgraph/exporters/ImageExporter.py b/pyqtgraph/exporters/ImageExporter.py index a8d235a8..cacddee1 100644 --- a/pyqtgraph/exporters/ImageExporter.py +++ b/pyqtgraph/exporters/ImageExporter.py @@ -44,15 +44,20 @@ class ImageExporter(Exporter): def parameters(self): return self.params - + + @staticmethod + def getSupportedImageFormats(): + filter = ["*."+f.data().decode('utf-8') for f in QtGui.QImageWriter.supportedImageFormats()] + preferred = ['*.png', '*.tif', '*.jpg'] + for p in preferred[::-1]: + if p in filter: + filter.remove(p) + filter.insert(0, p) + return filter + def export(self, fileName=None, toBytes=False, copy=False): if fileName is None and not toBytes and not copy: - filter = ["*."+f.data().decode('utf-8') for f in QtGui.QImageWriter.supportedImageFormats()] - preferred = ['*.png', '*.tif', '*.jpg'] - for p in preferred[::-1]: - if p in filter: - filter.remove(p) - filter.insert(0, p) + filter = self.getSupportedImageFormats() self.fileSaveDialog(filter=filter) return From bb21791c710ccd11c889a6641672adc7fbbdcf3e Mon Sep 17 00:00:00 2001 From: Karl Georg Bedrich Date: Mon, 1 Jun 2020 20:12:52 +0200 Subject: [PATCH 26/29] changed structure to redefine axis via plotitem.setAxes (#391) * changed structure to redefine axis via plotitem.setAxes * cleanuup * remove old axesitems before adding new ones * DEBUGGED plotitem.setAxes NEW AxisItem.setOrientation (needed by plotitem.setAxes) show/hide right axes after .setAxes() Co-authored-by: Ogi Moore --- pyqtgraph/graphicsItems/AxisItem.py | 29 +++++++++-- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 51 +++++++++++++++++--- 2 files changed, 68 insertions(+), 12 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 29f3ad62..c02e6e0c 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -45,11 +45,8 @@ class AxisItem(GraphicsWidget): GraphicsWidget.__init__(self, parent) self.label = QtGui.QGraphicsTextItem(self) self.picture = None - self.orientation = orientation - if orientation not in ['left', 'right', 'top', 'bottom']: - raise Exception("Orientation argument must be one of 'left', 'right', 'top', or 'bottom'.") - if orientation in ['left', 'right']: - self.label.rotate(-90) + self.orientation = None + self.setOrientation(orientation) self.style = { 'tickTextOffset': [5, 2], ## (horizontal, vertical) spacing between text and axis @@ -111,6 +108,27 @@ class AxisItem(GraphicsWidget): self.grid = False #self.setCacheMode(self.DeviceCoordinateCache) + def setOrientation(self, orientation): + """ + orientation = 'left', 'right', 'top', 'bottom' + """ + if orientation != self.orientation: + if orientation not in ['left', 'right', 'top', 'bottom']: + raise Exception("Orientation argument must be one of 'left', 'right', 'top', or 'bottom'.") + #rotate absolute allows to change orientation multiple times: + if orientation in ['left', 'right']: + self.label.setRotation(-90) + if self.orientation: + self._updateWidth() + self.setMaximumHeight(16777215) + else: + self.label.setRotation(0) + if self.orientation: + self._updateHeight() + self.setMaximumWidth(16777215) + self.orientation = orientation + + def setStyle(self, **kwds): """ Set various style options. @@ -514,6 +532,7 @@ class AxisItem(GraphicsWidget): self.unlinkFromView() self._linkedView = weakref.ref(view) + if self.orientation in ['right', 'left']: view.sigYRangeChanged.connect(self.linkedViewChanged) else: diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index 73aa29cb..4142fa3f 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -153,10 +153,10 @@ class PlotItem(GraphicsWidget): self.legend = None - # Initialize axis items + ## Create and place axis items self.axes = {} - self.setAxisItems(axisItems) - + self.setAxes(axisItems) + self.titleLabel = LabelItem('', size='11pt', parent=self) self.layout.addItem(self.titleLabel, 0, 1) self.setTitle(None) ## hide @@ -242,7 +242,7 @@ class PlotItem(GraphicsWidget): self.ctrl.maxTracesCheck.toggled.connect(self.updateDecimation) self.ctrl.maxTracesSpin.valueChanged.connect(self.updateDecimation) - + if labels is None: labels = {} for label in list(self.axes.keys()): @@ -258,8 +258,45 @@ class PlotItem(GraphicsWidget): self.setTitle(title) if len(kargs) > 0: - self.plot(**kargs) + self.plot(**kargs) + def setAxes(self, axisItems): + """ + Create and place axis items + For valid values for axisItems see __init__ + """ + for v in self.axes.values(): + item = v['item'] + self.layout.removeItem(item) + self.vb.removeItem(item) + + self.axes = {} + if axisItems is None: + axisItems = {} + for k, pos in (('top', (1,1)), ('bottom', (3,1)), ('left', (2,0)), ('right', (2,2))): + axis = axisItems.get(k, None) + if axis: + axis.setOrientation(k) + else: + axis = AxisItem(orientation=k) + axis.linkToView(self.vb) + self.axes[k] = {'item': axis, 'pos': pos} + self.layout.addItem(axis, *pos) + axis.setZValue(-1000) + axis.setFlag(axis.ItemNegativeZStacksBehindParent) + #show/hide axes: + all_dir = ['left', 'bottom', 'right', 'top'] + if axisItems: + to_show = list(axisItems.keys()) + to_hide = [a for a in all_dir if a not in to_show] + else: + to_show = all_dir[:2] + to_hide = all_dir[2:] + for a in to_hide: + self.hideAxis(a) + for a in to_show: + self.showAxis(a) + def implements(self, interface=None): return interface in ['ViewBoxWrapper'] @@ -1123,8 +1160,8 @@ class PlotItem(GraphicsWidget): Show or hide one of the plot's axes. axis must be one of 'left', 'bottom', 'right', or 'top' """ - s = self.getScale(axis) - p = self.axes[axis]['pos'] + s = self.getAxis(axis) + #p = self.axes[axis]['pos'] if show: s.show() else: From 983cc1695e1b011e961a9d92a062355697010405 Mon Sep 17 00:00:00 2001 From: Adam Strzelecki Date: Mon, 1 Jun 2020 20:23:18 +0200 Subject: [PATCH 27/29] Patch/window handling (#468) * Do not wrap PlotView/ImageView There is no need to wrap PlotView/ImageView into QMainWindow, since only purpose of the QMainWindow is some default menu toolbar & menu handling, that is not used by PyQtGraph anyway. Moreover, every parent-less Qt widget can become window, so this change just use PlotView/ImageView as windows, removing extra complexity, eg. method forwarding, self.win property. Another benefit of this change, it that these windows get initial dimensions and titles as they were designed in .ui file. * Properly cleanup on ImageView.close() We should not close explicitly child widgets or clear scene, otherwise Qt will deallocate children views, and cause "wrapped C/C++ object of type ImageItem has been deleted" error next time we call close() and/or some other methods. All children, including self.ui.roiPlot, self.ui.graphicsView will be closed together with its parent, so there is no need to close them explicitly. So the purpose of close it to reclaim the memory, but not to make the existing ImageView object dysfunctional. * Remove references to plot & image windows after close PyQtGraph images and plots module list variables are currently holding references to all plots and image windows returned directly from main module. This does not seem to be documented however, and causes the Qt windows to be not released from memory, even if user releases all own references. This change removes the references from images/plots list once window is closed, so when there is no other reference, window and all related memory is reclaimed. * Change all UI forms title from Form to PyQtGraph Co-authored-by: Ogi Moore --- examples/ScatterPlotSpeedTestTemplate.ui | 2 +- examples/ScatterPlotSpeedTestTemplate_pyqt.py | 2 +- .../ScatterPlotSpeedTestTemplate_pyside.py | 2 +- examples/designerExample.ui | 2 +- examples/exampleLoaderTemplate.ui | 2 +- examples/exampleLoaderTemplate_pyqt.py | 2 +- examples/exampleLoaderTemplate_pyqt5.py | 2 +- examples/exampleLoaderTemplate_pyside.py | 2 +- pyqtgraph/__init__.py | 18 +++++++++- pyqtgraph/canvas/CanvasTemplate.ui | 2 +- pyqtgraph/canvas/CanvasTemplate_pyqt.py | 2 +- pyqtgraph/canvas/CanvasTemplate_pyqt5.py | 2 +- pyqtgraph/canvas/CanvasTemplate_pyside.py | 2 +- pyqtgraph/canvas/TransformGuiTemplate.ui | 2 +- pyqtgraph/canvas/TransformGuiTemplate_pyqt.py | 2 +- .../canvas/TransformGuiTemplate_pyqt5.py | 2 +- .../canvas/TransformGuiTemplate_pyside.py | 2 +- pyqtgraph/flowchart/FlowchartCtrlTemplate.ui | 2 +- .../flowchart/FlowchartCtrlTemplate_pyqt.py | 2 +- .../flowchart/FlowchartCtrlTemplate_pyqt5.py | 2 +- .../flowchart/FlowchartCtrlTemplate_pyside.py | 2 +- pyqtgraph/flowchart/FlowchartTemplate.ui | 2 +- pyqtgraph/flowchart/FlowchartTemplate_pyqt.py | 2 +- .../flowchart/FlowchartTemplate_pyqt5.py | 2 +- .../flowchart/FlowchartTemplate_pyside.py | 2 +- .../PlotItem/plotConfigTemplate.ui | 2 +- .../PlotItem/plotConfigTemplate_pyqt.py | 2 +- .../PlotItem/plotConfigTemplate_pyqt5.py | 2 +- .../PlotItem/plotConfigTemplate_pyside.py | 2 +- .../graphicsItems/ViewBox/axisCtrlTemplate.ui | 2 +- .../ViewBox/axisCtrlTemplate_pyqt.py | 2 +- .../ViewBox/axisCtrlTemplate_pyqt5.py | 2 +- .../ViewBox/axisCtrlTemplate_pyside.py | 2 +- pyqtgraph/graphicsWindows.py | 33 ++++++++++--------- pyqtgraph/imageview/ImageView.py | 8 ++--- pyqtgraph/imageview/ImageViewTemplate.ui | 2 +- pyqtgraph/imageview/ImageViewTemplate_pyqt.py | 2 +- .../imageview/ImageViewTemplate_pyqt5.py | 2 +- .../imageview/ImageViewTemplate_pyside.py | 2 +- pyqtgraph/tests/uictest.ui | 2 +- 40 files changed, 74 insertions(+), 59 deletions(-) diff --git a/examples/ScatterPlotSpeedTestTemplate.ui b/examples/ScatterPlotSpeedTestTemplate.ui index 6b87e85d..5cdccf0f 100644 --- a/examples/ScatterPlotSpeedTestTemplate.ui +++ b/examples/ScatterPlotSpeedTestTemplate.ui @@ -11,7 +11,7 @@ - Form + PyQtGraph diff --git a/examples/ScatterPlotSpeedTestTemplate_pyqt.py b/examples/ScatterPlotSpeedTestTemplate_pyqt.py index 22136690..896525eb 100644 --- a/examples/ScatterPlotSpeedTestTemplate_pyqt.py +++ b/examples/ScatterPlotSpeedTestTemplate_pyqt.py @@ -41,7 +41,7 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) + Form.setWindowTitle(QtGui.QApplication.translate("Form", "PyQtGraph", None, QtGui.QApplication.UnicodeUTF8)) self.pixelModeCheck.setText(QtGui.QApplication.translate("Form", "pixel mode", None, QtGui.QApplication.UnicodeUTF8)) self.label.setText(QtGui.QApplication.translate("Form", "Size", None, QtGui.QApplication.UnicodeUTF8)) self.randCheck.setText(QtGui.QApplication.translate("Form", "Randomize", None, QtGui.QApplication.UnicodeUTF8)) diff --git a/examples/ScatterPlotSpeedTestTemplate_pyside.py b/examples/ScatterPlotSpeedTestTemplate_pyside.py index 690b0990..798ebccd 100644 --- a/examples/ScatterPlotSpeedTestTemplate_pyside.py +++ b/examples/ScatterPlotSpeedTestTemplate_pyside.py @@ -36,7 +36,7 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) + Form.setWindowTitle(QtGui.QApplication.translate("Form", "PyQtGraph", None, QtGui.QApplication.UnicodeUTF8)) self.pixelModeCheck.setText(QtGui.QApplication.translate("Form", "pixel mode", None, QtGui.QApplication.UnicodeUTF8)) self.label.setText(QtGui.QApplication.translate("Form", "Size", None, QtGui.QApplication.UnicodeUTF8)) self.randCheck.setText(QtGui.QApplication.translate("Form", "Randomize", None, QtGui.QApplication.UnicodeUTF8)) diff --git a/examples/designerExample.ui b/examples/designerExample.ui index 41d06089..0f1695af 100644 --- a/examples/designerExample.ui +++ b/examples/designerExample.ui @@ -11,7 +11,7 @@ - Form + PyQtGraph diff --git a/examples/exampleLoaderTemplate.ui b/examples/exampleLoaderTemplate.ui index f12459ba..c26dbddf 100644 --- a/examples/exampleLoaderTemplate.ui +++ b/examples/exampleLoaderTemplate.ui @@ -11,7 +11,7 @@ - Form + PyQtGraph diff --git a/examples/exampleLoaderTemplate_pyqt.py b/examples/exampleLoaderTemplate_pyqt.py index 732a3ea1..f5521a8f 100644 --- a/examples/exampleLoaderTemplate_pyqt.py +++ b/examples/exampleLoaderTemplate_pyqt.py @@ -89,7 +89,7 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(_translate("Form", "Form", None)) + Form.setWindowTitle(_translate("Form", "PyQtGraph", None)) self.graphicsSystemCombo.setItemText(0, _translate("Form", "default", None)) self.graphicsSystemCombo.setItemText(1, _translate("Form", "native", None)) self.graphicsSystemCombo.setItemText(2, _translate("Form", "raster", None)) diff --git a/examples/exampleLoaderTemplate_pyqt5.py b/examples/exampleLoaderTemplate_pyqt5.py index 14ded4d9..090447c2 100644 --- a/examples/exampleLoaderTemplate_pyqt5.py +++ b/examples/exampleLoaderTemplate_pyqt5.py @@ -78,7 +78,7 @@ class Ui_Form(object): def retranslateUi(self, Form): _translate = QtCore.QCoreApplication.translate - Form.setWindowTitle(_translate("Form", "Form")) + Form.setWindowTitle(_translate("Form", "PyQtGraph")) self.graphicsSystemCombo.setItemText(0, _translate("Form", "default")) self.graphicsSystemCombo.setItemText(1, _translate("Form", "native")) self.graphicsSystemCombo.setItemText(2, _translate("Form", "raster")) diff --git a/examples/exampleLoaderTemplate_pyside.py b/examples/exampleLoaderTemplate_pyside.py index 62296827..d1705d23 100644 --- a/examples/exampleLoaderTemplate_pyside.py +++ b/examples/exampleLoaderTemplate_pyside.py @@ -78,7 +78,7 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) + Form.setWindowTitle(QtGui.QApplication.translate("Form", "PyQtGraph", None, QtGui.QApplication.UnicodeUTF8)) self.graphicsSystemCombo.setItemText(0, QtGui.QApplication.translate("Form", "default", None, QtGui.QApplication.UnicodeUTF8)) self.graphicsSystemCombo.setItemText(1, QtGui.QApplication.translate("Form", "native", None, QtGui.QApplication.UnicodeUTF8)) self.graphicsSystemCombo.setItemText(2, QtGui.QApplication.translate("Form", "raster", None, QtGui.QApplication.UnicodeUTF8)) diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index 45e00c83..f834e637 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -413,12 +413,20 @@ def plot(*args, **kargs): dataArgs[k] = kargs[k] w = PlotWindow(**pwArgs) + w.sigClosed.connect(_plotWindowClosed) if len(args) > 0 or len(dataArgs) > 0: w.plot(*args, **dataArgs) plots.append(w) w.show() return w - + +def _plotWindowClosed(w): + w.close() + try: + plots.remove(w) + except ValueError: + pass + def image(*args, **kargs): """ Create and return an :class:`ImageWindow ` @@ -429,11 +437,19 @@ def image(*args, **kargs): """ mkQApp() w = ImageWindow(*args, **kargs) + w.sigClosed.connect(_imageWindowClosed) images.append(w) w.show() return w show = image ## for backward compatibility +def _imageWindowClosed(w): + w.close() + try: + images.remove(w) + except ValueError: + pass + def dbg(*args, **kwds): """ Create a console window and begin watching for exceptions. diff --git a/pyqtgraph/canvas/CanvasTemplate.ui b/pyqtgraph/canvas/CanvasTemplate.ui index bfdacf38..15fdf7a9 100644 --- a/pyqtgraph/canvas/CanvasTemplate.ui +++ b/pyqtgraph/canvas/CanvasTemplate.ui @@ -11,7 +11,7 @@ - Form + PyQtGraph diff --git a/pyqtgraph/canvas/CanvasTemplate_pyqt.py b/pyqtgraph/canvas/CanvasTemplate_pyqt.py index 3569c8e7..823265aa 100644 --- a/pyqtgraph/canvas/CanvasTemplate_pyqt.py +++ b/pyqtgraph/canvas/CanvasTemplate_pyqt.py @@ -91,7 +91,7 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(_translate("Form", "Form", None)) + Form.setWindowTitle(_translate("Form", "PyQtGraph", None)) self.autoRangeBtn.setText(_translate("Form", "Auto Range", None)) self.redirectCheck.setToolTip(_translate("Form", "Check to display all local items in a remote canvas.", None)) self.redirectCheck.setText(_translate("Form", "Redirect", None)) diff --git a/pyqtgraph/canvas/CanvasTemplate_pyqt5.py b/pyqtgraph/canvas/CanvasTemplate_pyqt5.py index 03310d39..83afc772 100644 --- a/pyqtgraph/canvas/CanvasTemplate_pyqt5.py +++ b/pyqtgraph/canvas/CanvasTemplate_pyqt5.py @@ -79,7 +79,7 @@ class Ui_Form(object): def retranslateUi(self, Form): _translate = QtCore.QCoreApplication.translate - Form.setWindowTitle(_translate("Form", "Form")) + Form.setWindowTitle(_translate("Form", "PyQtGraph")) self.autoRangeBtn.setText(_translate("Form", "Auto Range")) self.redirectCheck.setToolTip(_translate("Form", "Check to display all local items in a remote canvas.")) self.redirectCheck.setText(_translate("Form", "Redirect")) diff --git a/pyqtgraph/canvas/CanvasTemplate_pyside.py b/pyqtgraph/canvas/CanvasTemplate_pyside.py index 570d5bd1..c728efac 100644 --- a/pyqtgraph/canvas/CanvasTemplate_pyside.py +++ b/pyqtgraph/canvas/CanvasTemplate_pyside.py @@ -80,7 +80,7 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) + Form.setWindowTitle(QtGui.QApplication.translate("Form", "PyQtGraph", None, QtGui.QApplication.UnicodeUTF8)) self.autoRangeBtn.setText(QtGui.QApplication.translate("Form", "Auto Range", None, QtGui.QApplication.UnicodeUTF8)) self.redirectCheck.setToolTip(QtGui.QApplication.translate("Form", "Check to display all local items in a remote canvas.", None, QtGui.QApplication.UnicodeUTF8)) self.redirectCheck.setText(QtGui.QApplication.translate("Form", "Redirect", None, QtGui.QApplication.UnicodeUTF8)) diff --git a/pyqtgraph/canvas/TransformGuiTemplate.ui b/pyqtgraph/canvas/TransformGuiTemplate.ui index d8312388..c63979e0 100644 --- a/pyqtgraph/canvas/TransformGuiTemplate.ui +++ b/pyqtgraph/canvas/TransformGuiTemplate.ui @@ -17,7 +17,7 @@ - Form + PyQtGraph diff --git a/pyqtgraph/canvas/TransformGuiTemplate_pyqt.py b/pyqtgraph/canvas/TransformGuiTemplate_pyqt.py index c6cf82e4..7cbb3652 100644 --- a/pyqtgraph/canvas/TransformGuiTemplate_pyqt.py +++ b/pyqtgraph/canvas/TransformGuiTemplate_pyqt.py @@ -59,7 +59,7 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(_translate("Form", "Form", None)) + Form.setWindowTitle(_translate("Form", "PyQtGraph", None)) self.translateLabel.setText(_translate("Form", "Translate:", None)) self.rotateLabel.setText(_translate("Form", "Rotate:", None)) self.scaleLabel.setText(_translate("Form", "Scale:", None)) diff --git a/pyqtgraph/canvas/TransformGuiTemplate_pyqt5.py b/pyqtgraph/canvas/TransformGuiTemplate_pyqt5.py index 6b1f239b..2af0499a 100644 --- a/pyqtgraph/canvas/TransformGuiTemplate_pyqt5.py +++ b/pyqtgraph/canvas/TransformGuiTemplate_pyqt5.py @@ -46,7 +46,7 @@ class Ui_Form(object): def retranslateUi(self, Form): _translate = QtCore.QCoreApplication.translate - Form.setWindowTitle(_translate("Form", "Form")) + Form.setWindowTitle(_translate("Form", "PyQtGraph")) self.translateLabel.setText(_translate("Form", "Translate:")) self.rotateLabel.setText(_translate("Form", "Rotate:")) self.scaleLabel.setText(_translate("Form", "Scale:")) diff --git a/pyqtgraph/canvas/TransformGuiTemplate_pyside.py b/pyqtgraph/canvas/TransformGuiTemplate_pyside.py index e430b61a..76620342 100644 --- a/pyqtgraph/canvas/TransformGuiTemplate_pyside.py +++ b/pyqtgraph/canvas/TransformGuiTemplate_pyside.py @@ -46,7 +46,7 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) + Form.setWindowTitle(QtGui.QApplication.translate("Form", "PyQtGraph", None, QtGui.QApplication.UnicodeUTF8)) self.translateLabel.setText(QtGui.QApplication.translate("Form", "Translate:", None, QtGui.QApplication.UnicodeUTF8)) self.rotateLabel.setText(QtGui.QApplication.translate("Form", "Rotate:", None, QtGui.QApplication.UnicodeUTF8)) self.scaleLabel.setText(QtGui.QApplication.translate("Form", "Scale:", None, QtGui.QApplication.UnicodeUTF8)) diff --git a/pyqtgraph/flowchart/FlowchartCtrlTemplate.ui b/pyqtgraph/flowchart/FlowchartCtrlTemplate.ui index 0361ad3e..6a9a203a 100644 --- a/pyqtgraph/flowchart/FlowchartCtrlTemplate.ui +++ b/pyqtgraph/flowchart/FlowchartCtrlTemplate.ui @@ -11,7 +11,7 @@ - Form + PyQtGraph diff --git a/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyqt.py b/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyqt.py index 8afd43f8..3d8bcf56 100644 --- a/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyqt.py +++ b/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyqt.py @@ -69,7 +69,7 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(_translate("Form", "Form", None)) + Form.setWindowTitle(_translate("Form", "PyQtGraph", None)) self.loadBtn.setText(_translate("Form", "Load..", None)) self.saveBtn.setText(_translate("Form", "Save", None)) self.saveAsBtn.setText(_translate("Form", "As..", None)) diff --git a/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyqt5.py b/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyqt5.py index b661918d..958f2aaf 100644 --- a/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyqt5.py +++ b/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyqt5.py @@ -56,7 +56,7 @@ class Ui_Form(object): def retranslateUi(self, Form): _translate = QtCore.QCoreApplication.translate - Form.setWindowTitle(_translate("Form", "Form")) + Form.setWindowTitle(_translate("Form", "PyQtGraph")) self.loadBtn.setText(_translate("Form", "Load..")) self.saveBtn.setText(_translate("Form", "Save")) self.saveAsBtn.setText(_translate("Form", "As..")) diff --git a/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyside.py b/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyside.py index b722000e..2db10f6a 100644 --- a/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyside.py +++ b/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyside.py @@ -55,7 +55,7 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) + Form.setWindowTitle(QtGui.QApplication.translate("Form", "PyQtGraph", None, QtGui.QApplication.UnicodeUTF8)) self.loadBtn.setText(QtGui.QApplication.translate("Form", "Load..", None, QtGui.QApplication.UnicodeUTF8)) self.saveBtn.setText(QtGui.QApplication.translate("Form", "Save", None, QtGui.QApplication.UnicodeUTF8)) self.saveAsBtn.setText(QtGui.QApplication.translate("Form", "As..", None, QtGui.QApplication.UnicodeUTF8)) diff --git a/pyqtgraph/flowchart/FlowchartTemplate.ui b/pyqtgraph/flowchart/FlowchartTemplate.ui index 8b0c19da..22934b91 100644 --- a/pyqtgraph/flowchart/FlowchartTemplate.ui +++ b/pyqtgraph/flowchart/FlowchartTemplate.ui @@ -11,7 +11,7 @@ - Form + PyQtGraph diff --git a/pyqtgraph/flowchart/FlowchartTemplate_pyqt.py b/pyqtgraph/flowchart/FlowchartTemplate_pyqt.py index 06b10bfe..e6084eee 100644 --- a/pyqtgraph/flowchart/FlowchartTemplate_pyqt.py +++ b/pyqtgraph/flowchart/FlowchartTemplate_pyqt.py @@ -62,7 +62,7 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(_translate("Form", "Form", None)) + Form.setWindowTitle(_translate("Form", "PyQtGraph", None)) from ..flowchart.FlowchartGraphicsView import FlowchartGraphicsView from ..widgets.DataTreeWidget import DataTreeWidget diff --git a/pyqtgraph/flowchart/FlowchartTemplate_pyqt5.py b/pyqtgraph/flowchart/FlowchartTemplate_pyqt5.py index ba754305..448a00ff 100644 --- a/pyqtgraph/flowchart/FlowchartTemplate_pyqt5.py +++ b/pyqtgraph/flowchart/FlowchartTemplate_pyqt5.py @@ -49,7 +49,7 @@ class Ui_Form(object): def retranslateUi(self, Form): _translate = QtCore.QCoreApplication.translate - Form.setWindowTitle(_translate("Form", "Form")) + Form.setWindowTitle(_translate("Form", "PyQtGraph")) from ..widgets.DataTreeWidget import DataTreeWidget from ..flowchart.FlowchartGraphicsView import FlowchartGraphicsView diff --git a/pyqtgraph/flowchart/FlowchartTemplate_pyside.py b/pyqtgraph/flowchart/FlowchartTemplate_pyside.py index 2c693c60..47f97f85 100644 --- a/pyqtgraph/flowchart/FlowchartTemplate_pyside.py +++ b/pyqtgraph/flowchart/FlowchartTemplate_pyside.py @@ -48,7 +48,7 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) + Form.setWindowTitle(QtGui.QApplication.translate("Form", "PyQtGraph", None, QtGui.QApplication.UnicodeUTF8)) from ..flowchart.FlowchartGraphicsView import FlowchartGraphicsView from ..widgets.DataTreeWidget import DataTreeWidget diff --git a/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate.ui b/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate.ui index dffc62d0..12d8033e 100644 --- a/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate.ui +++ b/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate.ui @@ -11,7 +11,7 @@ - Form + PyQtGraph diff --git a/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyqt.py b/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyqt.py index e09c9978..5ecc0438 100644 --- a/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyqt.py +++ b/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyqt.py @@ -148,7 +148,7 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(_translate("Form", "Form", None)) + Form.setWindowTitle(_translate("Form", "PyQtGraph", None)) self.averageGroup.setToolTip(_translate("Form", "Display averages of the curves displayed in this plot. The parameter list allows you to choose parameters to average over (if any are available).", None)) self.averageGroup.setTitle(_translate("Form", "Average", None)) self.clipToViewCheck.setToolTip(_translate("Form", "Plot only the portion of each curve that is visible. This assumes X values are uniformly spaced.", None)) diff --git a/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyqt5.py b/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyqt5.py index e9fdff24..817221f2 100644 --- a/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyqt5.py +++ b/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyqt5.py @@ -135,7 +135,7 @@ class Ui_Form(object): def retranslateUi(self, Form): _translate = QtCore.QCoreApplication.translate - Form.setWindowTitle(_translate("Form", "Form")) + Form.setWindowTitle(_translate("Form", "PyQtGraph")) self.averageGroup.setToolTip(_translate("Form", "Display averages of the curves displayed in this plot. The parameter list allows you to choose parameters to average over (if any are available).")) self.averageGroup.setTitle(_translate("Form", "Average")) self.clipToViewCheck.setToolTip(_translate("Form", "Plot only the portion of each curve that is visible. This assumes X values are uniformly spaced.")) diff --git a/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyside.py b/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyside.py index aff31211..d0fd1edd 100644 --- a/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyside.py +++ b/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyside.py @@ -134,7 +134,7 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) + Form.setWindowTitle(QtGui.QApplication.translate("Form", "PyQtGraph", None, QtGui.QApplication.UnicodeUTF8)) self.averageGroup.setToolTip(QtGui.QApplication.translate("Form", "Display averages of the curves displayed in this plot. The parameter list allows you to choose parameters to average over (if any are available).", None, QtGui.QApplication.UnicodeUTF8)) self.averageGroup.setTitle(QtGui.QApplication.translate("Form", "Average", None, QtGui.QApplication.UnicodeUTF8)) self.clipToViewCheck.setToolTip(QtGui.QApplication.translate("Form", "Plot only the portion of each curve that is visible. This assumes X values are uniformly spaced.", None, QtGui.QApplication.UnicodeUTF8)) diff --git a/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate.ui b/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate.ui index 297fce75..01bdf93e 100644 --- a/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate.ui +++ b/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate.ui @@ -17,7 +17,7 @@ - Form + PyQtGraph diff --git a/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyqt.py b/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyqt.py index 5d952741..b54153fc 100644 --- a/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyqt.py +++ b/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyqt.py @@ -78,7 +78,7 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(_translate("Form", "Form", None)) + Form.setWindowTitle(_translate("Form", "PyQtGraph", None)) self.label.setText(_translate("Form", "Link Axis:", None)) self.linkCombo.setToolTip(_translate("Form", "

Links this axis with another view. When linked, both views will display the same data range.

", None)) self.autoPercentSpin.setToolTip(_translate("Form", "

Percent of data to be visible when auto-scaling. It may be useful to decrease this value for data with spiky noise.

", None)) diff --git a/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyqt5.py b/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyqt5.py index 78da6eea..0a28e7f6 100644 --- a/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyqt5.py +++ b/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyqt5.py @@ -65,7 +65,7 @@ class Ui_Form(object): def retranslateUi(self, Form): _translate = QtCore.QCoreApplication.translate - Form.setWindowTitle(_translate("Form", "Form")) + Form.setWindowTitle(_translate("Form", "PyQtGraph")) self.label.setText(_translate("Form", "Link Axis:")) self.linkCombo.setToolTip(_translate("Form", "

Links this axis with another view. When linked, both views will display the same data range.

")) self.autoPercentSpin.setToolTip(_translate("Form", "

Percent of data to be visible when auto-scaling. It may be useful to decrease this value for data with spiky noise.

")) diff --git a/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyside.py b/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyside.py index 9ddeb5d1..c90206b5 100644 --- a/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyside.py +++ b/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyside.py @@ -64,7 +64,7 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) + Form.setWindowTitle(QtGui.QApplication.translate("Form", "PyQtGraph", None, QtGui.QApplication.UnicodeUTF8)) self.label.setText(QtGui.QApplication.translate("Form", "Link Axis:", None, QtGui.QApplication.UnicodeUTF8)) self.linkCombo.setToolTip(QtGui.QApplication.translate("Form", "

Links this axis with another view. When linked, both views will display the same data range.

", None, QtGui.QApplication.UnicodeUTF8)) self.autoPercentSpin.setToolTip(QtGui.QApplication.translate("Form", "

Percent of data to be visible when auto-scaling. It may be useful to decrease this value for data with spiky noise.

", None, QtGui.QApplication.UnicodeUTF8)) diff --git a/pyqtgraph/graphicsWindows.py b/pyqtgraph/graphicsWindows.py index 4033baf3..aa62f4f1 100644 --- a/pyqtgraph/graphicsWindows.py +++ b/pyqtgraph/graphicsWindows.py @@ -48,38 +48,39 @@ class TabWindow(QtGui.QMainWindow): class PlotWindow(PlotWidget): + sigClosed = QtCore.Signal(object) + """ (deprecated; use :class:`~pyqtgraph.PlotWidget` instead) """ def __init__(self, title=None, **kargs): mkQApp() - self.win = QtGui.QMainWindow() PlotWidget.__init__(self, **kargs) - self.win.setCentralWidget(self) - for m in ['resize']: - setattr(self, m, getattr(self.win, m)) if title is not None: - self.win.setWindowTitle(title) - self.win.show() + self.setWindowTitle(title) + self.show() + + def closeEvent(self, event): + PlotWidget.closeEvent(self, event) + self.sigClosed.emit(self) class ImageWindow(ImageView): + sigClosed = QtCore.Signal(object) + """ (deprecated; use :class:`~pyqtgraph.ImageView` instead) """ def __init__(self, *args, **kargs): mkQApp() - self.win = QtGui.QMainWindow() - self.win.resize(800,600) + ImageView.__init__(self) if 'title' in kargs: - self.win.setWindowTitle(kargs['title']) + self.setWindowTitle(kargs['title']) del kargs['title'] - ImageView.__init__(self, self.win) if len(args) > 0 or len(kargs) > 0: self.setImage(*args, **kargs) - self.win.setCentralWidget(self) - for m in ['resize']: - setattr(self, m, getattr(self.win, m)) - #for m in ['setImage', 'autoRange', 'addItem', 'removeItem', 'blackLevel', 'whiteLevel', 'imageItem']: - #setattr(self, m, getattr(self.cw, m)) - self.win.show() + self.show() + + def closeEvent(self, event): + ImageView.closeEvent(self, event) + self.sigClosed.emit(self) diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index daa9b06d..e9058bdb 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -411,11 +411,9 @@ class ImageView(QtGui.QWidget): def close(self): """Closes the widget nicely, making sure to clear the graphics scene and release memory.""" - self.ui.roiPlot.close() - self.ui.graphicsView.close() - self.scene.clear() - del self.image - del self.imageDisp + self.clear() + self.imageDisp = None + self.imageItem.setParent(None) super(ImageView, self).close() self.setParent(None) diff --git a/pyqtgraph/imageview/ImageViewTemplate.ui b/pyqtgraph/imageview/ImageViewTemplate.ui index 927bda30..ece77864 100644 --- a/pyqtgraph/imageview/ImageViewTemplate.ui +++ b/pyqtgraph/imageview/ImageViewTemplate.ui @@ -11,7 +11,7 @@
- Form + PyQtGraph diff --git a/pyqtgraph/imageview/ImageViewTemplate_pyqt.py b/pyqtgraph/imageview/ImageViewTemplate_pyqt.py index 8c9d5633..8a34c1d8 100644 --- a/pyqtgraph/imageview/ImageViewTemplate_pyqt.py +++ b/pyqtgraph/imageview/ImageViewTemplate_pyqt.py @@ -146,7 +146,7 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(_translate("Form", "Form", None)) + Form.setWindowTitle(_translate("Form", "PyQtGraph", None)) self.roiBtn.setText(_translate("Form", "ROI", None)) self.menuBtn.setText(_translate("Form", "Menu", None)) self.normGroup.setTitle(_translate("Form", "Normalization", None)) diff --git a/pyqtgraph/imageview/ImageViewTemplate_pyqt5.py b/pyqtgraph/imageview/ImageViewTemplate_pyqt5.py index 1d076a9e..87f3f254 100644 --- a/pyqtgraph/imageview/ImageViewTemplate_pyqt5.py +++ b/pyqtgraph/imageview/ImageViewTemplate_pyqt5.py @@ -134,7 +134,7 @@ class Ui_Form(object): def retranslateUi(self, Form): _translate = QtCore.QCoreApplication.translate - Form.setWindowTitle(_translate("Form", "Form")) + Form.setWindowTitle(_translate("Form", "PyQtGraph")) self.roiBtn.setText(_translate("Form", "ROI")) self.menuBtn.setText(_translate("Form", "Norm")) self.normGroup.setTitle(_translate("Form", "Normalization")) diff --git a/pyqtgraph/imageview/ImageViewTemplate_pyside.py b/pyqtgraph/imageview/ImageViewTemplate_pyside.py index 6d6c9632..9980e2ba 100644 --- a/pyqtgraph/imageview/ImageViewTemplate_pyside.py +++ b/pyqtgraph/imageview/ImageViewTemplate_pyside.py @@ -132,7 +132,7 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) + Form.setWindowTitle(QtGui.QApplication.translate("Form", "PyQtGraph", None, QtGui.QApplication.UnicodeUTF8)) self.roiBtn.setText(QtGui.QApplication.translate("Form", "ROI", None, QtGui.QApplication.UnicodeUTF8)) self.menuBtn.setText(QtGui.QApplication.translate("Form", "Menu", None, QtGui.QApplication.UnicodeUTF8)) self.normGroup.setTitle(QtGui.QApplication.translate("Form", "Normalization", None, QtGui.QApplication.UnicodeUTF8)) diff --git a/pyqtgraph/tests/uictest.ui b/pyqtgraph/tests/uictest.ui index 25d14f2b..a183bdae 100644 --- a/pyqtgraph/tests/uictest.ui +++ b/pyqtgraph/tests/uictest.ui @@ -11,7 +11,7 @@ - Form + PyQtGraph From ca9b0c7910e999a774af38ae98f114bc32810468 Mon Sep 17 00:00:00 2001 From: Karl Georg Bedrich Date: Mon, 1 Jun 2020 20:24:18 +0200 Subject: [PATCH 28/29] new method 'getAxpectRatio' with code taken from 'setAspectLocked' (#392) Co-authored-by: Ogi Moore --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 94aa6243..e665deef 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -233,6 +233,17 @@ class ViewBox(GraphicsWidget): if name is None: self.updateViewLists() + def getAspectRatio(self): + '''return the current aspect ratio''' + rect = self.rect() + vr = self.viewRect() + if rect.height() == 0 or vr.width() == 0 or vr.height() == 0: + currentRatio = 1.0 + else: + currentRatio = (rect.width()/float(rect.height())) / ( + vr.width()/vr.height()) + return currentRatio + def register(self, name): """ Add this ViewBox to the registered list of views. @@ -1134,12 +1145,7 @@ class ViewBox(GraphicsWidget): return self.state['aspectLocked'] = False else: - rect = self.rect() - vr = self.viewRect() - if rect.height() == 0 or vr.width() == 0 or vr.height() == 0: - currentRatio = 1.0 - else: - currentRatio = (rect.width()/float(rect.height())) / (vr.width()/vr.height()) + currentRatio = self.getAspectRatio() if ratio is None: ratio = currentRatio if self.state['aspectLocked'] == ratio: # nothing to change From ed36a0194baa88fa4e7440e8d67e84633caf7d31 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 1 Jun 2020 18:38:50 -0700 Subject: [PATCH 29/29] py3 fix for scatterplotwidget.setselectedfields --- pyqtgraph/widgets/ScatterPlotWidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/widgets/ScatterPlotWidget.py b/pyqtgraph/widgets/ScatterPlotWidget.py index bf8a0f42..08f6d02b 100644 --- a/pyqtgraph/widgets/ScatterPlotWidget.py +++ b/pyqtgraph/widgets/ScatterPlotWidget.py @@ -96,7 +96,7 @@ class ScatterPlotWidget(QtGui.QSplitter): try: self.fieldList.clearSelection() for f in fields: - i = self.fields.keys().index(f) + i = list(self.fields.keys()).index(f) item = self.fieldList.item(i) item.setSelected(True) finally: